diff --git a/src/skills/builtin/finding-agents/scripts/find-agents.ts b/src/skills/builtin/finding-agents/scripts/find-agents.ts index 9905bb3..845b199 100644 --- a/src/skills/builtin/finding-agents/scripts/find-agents.ts +++ b/src/skills/builtin/finding-agents/scripts/find-agents.ts @@ -1,9 +1,12 @@ -#!/usr/bin/env npx ts-node +#!/usr/bin/env npx tsx /** * Find Agents - Search for agents with various filters * + * This script is standalone and can be run outside the CLI process. + * It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json. + * * Usage: - * npx ts-node find-agents.ts [options] + * npx tsx find-agents.ts [options] * * Options: * --name Exact name match @@ -17,9 +20,17 @@ * Raw API response from GET /v1/agents */ -import type Letta from "@letta-ai/letta-client"; -import { getClient } from "../../../../agent/client"; -import { settingsManager } from "../../../../settings-manager"; +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected +// (ES module imports don't respect NODE_PATH, but require does) +const require = createRequire(import.meta.url); +const Letta = require("@letta-ai/letta-client") + .default as typeof import("@letta-ai/letta-client").default; +type LettaClient = InstanceType; interface FindAgentsOptions { name?: string; @@ -30,6 +41,38 @@ interface FindAgentsOptions { limit?: number; } +/** + * Get API key from env var or settings file + */ +function getApiKey(): string { + // First check env var (set by CLI's getShellEnv) + if (process.env.LETTA_API_KEY) { + return process.env.LETTA_API_KEY; + } + + // Fall back to settings file + const settingsPath = join(homedir(), ".letta", "settings.json"); + try { + const settings = JSON.parse(readFileSync(settingsPath, "utf-8")); + if (settings.env?.LETTA_API_KEY) { + return settings.env.LETTA_API_KEY; + } + } catch { + // Settings file doesn't exist or is invalid + } + + throw new Error( + "No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.", + ); +} + +/** + * Create a Letta client with auth from env/settings + */ +function createClient(): LettaClient { + return new Letta({ apiKey: getApiKey() }); +} + /** * Find agents matching the given criteria * @param client - Letta client instance @@ -37,7 +80,7 @@ interface FindAgentsOptions { * @returns Array of agent objects from the API */ export async function findAgents( - client: Letta, + client: LettaClient, options: FindAgentsOptions = {}, ): Promise>> { const params: Parameters[0] = { @@ -103,13 +146,13 @@ function parseArgs(args: string[]): FindAgentsOptions { return options; } -// CLI entry point -if (require.main === module) { +// CLI entry point - check if this file is being run directly +const isMainModule = import.meta.url === `file://${process.argv[1]}`; +if (isMainModule) { (async () => { try { const options = parseArgs(process.argv.slice(2)); - await settingsManager.initialize(); - const client = await getClient(); + const client = createClient(); const result = await findAgents(client, options); console.log(JSON.stringify(result, null, 2)); } catch (error) { @@ -118,7 +161,7 @@ if (require.main === module) { error instanceof Error ? error.message : String(error), ); console.error(` -Usage: npx ts-node find-agents.ts [options] +Usage: npx tsx find-agents.ts [options] Options: --name Exact name match diff --git a/src/skills/builtin/migrating-memory/scripts/attach-block.ts b/src/skills/builtin/migrating-memory/scripts/attach-block.ts index 0d8e2d2..27b0215 100644 --- a/src/skills/builtin/migrating-memory/scripts/attach-block.ts +++ b/src/skills/builtin/migrating-memory/scripts/attach-block.ts @@ -1,24 +1,79 @@ -#!/usr/bin/env npx ts-node +#!/usr/bin/env npx tsx /** * Attach Block - Attaches an existing memory block to an agent (sharing) * + * This script is standalone and can be run outside the CLI process. + * It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json. + * It reads agent ID from LETTA_AGENT_ID env var or --agent-id arg. + * * Usage: - * npx ts-node attach-block.ts --block-id --target-agent-id [--read-only] + * npx tsx attach-block.ts --block-id [--agent-id ] [--read-only] * * This attaches an existing block to another agent, making it shared. * Changes to the block will be visible to all agents that have it attached. * * Options: - * --read-only Target agent can read but not modify the block + * --agent-id Target agent ID (overrides LETTA_AGENT_ID env var) + * --read-only Target agent can read but not modify the block * * Output: * Raw API response from the attach operation */ -import type Letta from "@letta-ai/letta-client"; -import { getClient } from "../../../../agent/client"; -import { getCurrentAgentId } from "../../../../agent/context"; -import { settingsManager } from "../../../../settings-manager"; +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected +// (ES module imports don't respect NODE_PATH, but require does) +const require = createRequire(import.meta.url); +const Letta = require("@letta-ai/letta-client") + .default as typeof import("@letta-ai/letta-client").default; +type LettaClient = InstanceType; + +/** + * Get API key from env var or settings file + */ +function getApiKey(): string { + if (process.env.LETTA_API_KEY) { + return process.env.LETTA_API_KEY; + } + + const settingsPath = join(homedir(), ".letta", "settings.json"); + try { + const settings = JSON.parse(readFileSync(settingsPath, "utf-8")); + if (settings.env?.LETTA_API_KEY) { + return settings.env.LETTA_API_KEY; + } + } catch { + // Settings file doesn't exist or is invalid + } + + throw new Error( + "No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.", + ); +} + +/** + * Get agent ID from CLI arg, env var, or throw + */ +function getAgentId(cliArg?: string): string { + if (cliArg) return cliArg; + if (process.env.LETTA_AGENT_ID) { + return process.env.LETTA_AGENT_ID; + } + throw new Error( + "No agent ID provided. Use --agent-id or ensure LETTA_AGENT_ID env var is set.", + ); +} + +/** + * Create a Letta client with auth from env/settings + */ +function createClient(): LettaClient { + return new Letta({ apiKey: getApiKey() }); +} /** * Attach an existing block to the current agent (sharing it) @@ -29,13 +84,13 @@ import { settingsManager } from "../../../../settings-manager"; * @returns API response from the attach operation */ export async function attachBlock( - client: Letta, + client: LettaClient, blockId: string, readOnly = false, targetAgentId?: string, ): Promise>> { // Get current agent ID (the agent calling this script) or use provided ID - const currentAgentId = targetAgentId ?? getCurrentAgentId(); + const currentAgentId = getAgentId(targetAgentId); const result = await client.agents.blocks.attach(blockId, { agent_id: currentAgentId, @@ -58,8 +113,10 @@ export async function attachBlock( function parseArgs(args: string[]): { blockId: string; readOnly: boolean; + agentId?: string; } { const blockIdIndex = args.indexOf("--block-id"); + const agentIdIndex = args.indexOf("--agent-id"); const readOnly = args.includes("--read-only"); if (blockIdIndex === -1 || blockIdIndex + 1 >= args.length) { @@ -69,17 +126,21 @@ function parseArgs(args: string[]): { return { blockId: args[blockIdIndex + 1] as string, readOnly, + agentId: + agentIdIndex !== -1 && agentIdIndex + 1 < args.length + ? (args[agentIdIndex + 1] as string) + : undefined, }; } -// CLI entry point -if (require.main === module) { +// CLI entry point - check if this file is being run directly +const isMainModule = import.meta.url === `file://${process.argv[1]}`; +if (isMainModule) { (async () => { try { - const { blockId, readOnly } = parseArgs(process.argv.slice(2)); - await settingsManager.initialize(); - const client = await getClient(); - const result = await attachBlock(client, blockId, readOnly); + const { blockId, readOnly, agentId } = parseArgs(process.argv.slice(2)); + const client = createClient(); + const result = await attachBlock(client, blockId, readOnly, agentId); console.log(JSON.stringify(result, null, 2)); } catch (error) { console.error( @@ -91,7 +152,7 @@ if (require.main === module) { error.message.includes("Missing required argument") ) { console.error( - "\nUsage: npx ts-node attach-block.ts --block-id [--read-only]", + "\nUsage: npx tsx attach-block.ts --block-id [--agent-id ] [--read-only]", ); } process.exit(1); diff --git a/src/skills/builtin/migrating-memory/scripts/copy-block.ts b/src/skills/builtin/migrating-memory/scripts/copy-block.ts index 27b527a..9a6edd5 100644 --- a/src/skills/builtin/migrating-memory/scripts/copy-block.ts +++ b/src/skills/builtin/migrating-memory/scripts/copy-block.ts @@ -1,12 +1,17 @@ -#!/usr/bin/env npx ts-node +#!/usr/bin/env npx tsx /** * Copy Block - Copies a memory block to create a new independent block for the current agent * + * This script is standalone and can be run outside the CLI process. + * It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json. + * It reads agent ID from LETTA_AGENT_ID env var or --agent-id arg. + * * Usage: - * npx ts-node copy-block.ts --block-id [--label ] + * npx tsx copy-block.ts --block-id [--label ] [--agent-id ] * * Options: - * --label Override the block label (required if you already have a block with that label) + * --label Override the block label (required if you already have a block with that label) + * --agent-id Target agent ID (overrides LETTA_AGENT_ID env var) * * This creates a new block with the same content as the source block, * then attaches it to the current agent. Changes to the new block @@ -16,17 +21,65 @@ * Raw API response from each step (retrieve, create, attach) */ -import type Letta from "@letta-ai/letta-client"; -import { getClient } from "../../../../agent/client"; -import { getCurrentAgentId } from "../../../../agent/context"; -import { settingsManager } from "../../../../settings-manager"; +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected +// (ES module imports don't respect NODE_PATH, but require does) +const require = createRequire(import.meta.url); +const Letta = require("@letta-ai/letta-client") + .default as typeof import("@letta-ai/letta-client").default; +type LettaClient = InstanceType; interface CopyBlockResult { - sourceBlock: Awaited>; - newBlock: Awaited>; - attachResult: Awaited< - ReturnType - >; + sourceBlock: Awaited>; + newBlock: Awaited>; + attachResult: Awaited>; +} + +/** + * Get API key from env var or settings file + */ +function getApiKey(): string { + if (process.env.LETTA_API_KEY) { + return process.env.LETTA_API_KEY; + } + + const settingsPath = join(homedir(), ".letta", "settings.json"); + try { + const settings = JSON.parse(readFileSync(settingsPath, "utf-8")); + if (settings.env?.LETTA_API_KEY) { + return settings.env.LETTA_API_KEY; + } + } catch { + // Settings file doesn't exist or is invalid + } + + throw new Error( + "No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.", + ); +} + +/** + * Get agent ID from CLI arg, env var, or throw + */ +function getAgentId(cliArg?: string): string { + if (cliArg) return cliArg; + if (process.env.LETTA_AGENT_ID) { + return process.env.LETTA_AGENT_ID; + } + throw new Error( + "No agent ID provided. Use --agent-id or ensure LETTA_AGENT_ID env var is set.", + ); +} + +/** + * Create a Letta client with auth from env/settings + */ +function createClient(): LettaClient { + return new Letta({ apiKey: getApiKey() }); } /** @@ -37,12 +90,12 @@ interface CopyBlockResult { * @returns Object containing source block, new block, and attach result */ export async function copyBlock( - client: Letta, + client: LettaClient, blockId: string, options?: { labelOverride?: string; targetAgentId?: string }, ): Promise { // Get current agent ID (the agent calling this script) or use provided ID - const currentAgentId = options?.targetAgentId ?? getCurrentAgentId(); + const currentAgentId = getAgentId(options?.targetAgentId); // 1. Get source block details const sourceBlock = await client.blocks.retrieve(blockId); @@ -63,9 +116,14 @@ export async function copyBlock( return { sourceBlock, newBlock, attachResult }; } -function parseArgs(args: string[]): { blockId: string; label?: string } { +function parseArgs(args: string[]): { + blockId: string; + label?: string; + agentId?: string; +} { const blockIdIndex = args.indexOf("--block-id"); const labelIndex = args.indexOf("--label"); + const agentIdIndex = args.indexOf("--agent-id"); if (blockIdIndex === -1 || blockIdIndex + 1 >= args.length) { throw new Error("Missing required argument: --block-id "); @@ -77,17 +135,24 @@ function parseArgs(args: string[]): { blockId: string; label?: string } { labelIndex !== -1 && labelIndex + 1 < args.length ? (args[labelIndex + 1] as string) : undefined, + agentId: + agentIdIndex !== -1 && agentIdIndex + 1 < args.length + ? (args[agentIdIndex + 1] as string) + : undefined, }; } -// CLI entry point -if (require.main === module) { +// CLI entry point - check if this file is being run directly +const isMainModule = import.meta.url === `file://${process.argv[1]}`; +if (isMainModule) { (async () => { try { - const { blockId, label } = parseArgs(process.argv.slice(2)); - await settingsManager.initialize(); - const client = await getClient(); - const result = await copyBlock(client, blockId, { labelOverride: label }); + const { blockId, label, agentId } = parseArgs(process.argv.slice(2)); + const client = createClient(); + const result = await copyBlock(client, blockId, { + labelOverride: label, + targetAgentId: agentId, + }); console.log(JSON.stringify(result, null, 2)); } catch (error) { console.error( @@ -99,7 +164,7 @@ if (require.main === module) { error.message.includes("Missing required argument") ) { console.error( - "\nUsage: npx ts-node copy-block.ts --block-id [--label ]", + "\nUsage: npx tsx copy-block.ts --block-id [--label ] [--agent-id ]", ); } process.exit(1); diff --git a/src/skills/builtin/migrating-memory/scripts/get-agent-blocks.ts b/src/skills/builtin/migrating-memory/scripts/get-agent-blocks.ts index c9e72b3..6866856 100644 --- a/src/skills/builtin/migrating-memory/scripts/get-agent-blocks.ts +++ b/src/skills/builtin/migrating-memory/scripts/get-agent-blocks.ts @@ -1,17 +1,58 @@ -#!/usr/bin/env npx ts-node +#!/usr/bin/env npx tsx /** * Get Agent Blocks - Retrieves memory blocks from a specific agent * + * This script is standalone and can be run outside the CLI process. + * It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json. + * * Usage: - * npx ts-node get-agent-blocks.ts --agent-id + * npx tsx get-agent-blocks.ts --agent-id * * Output: * Raw API response from GET /v1/agents/{id}/core-memory/blocks */ -import type Letta from "@letta-ai/letta-client"; -import { getClient } from "../../../../agent/client"; -import { settingsManager } from "../../../../settings-manager"; +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected +// (ES module imports don't respect NODE_PATH, but require does) +const require = createRequire(import.meta.url); +const Letta = require("@letta-ai/letta-client") + .default as typeof import("@letta-ai/letta-client").default; +type LettaClient = InstanceType; + +/** + * Get API key from env var or settings file + */ +function getApiKey(): string { + if (process.env.LETTA_API_KEY) { + return process.env.LETTA_API_KEY; + } + + const settingsPath = join(homedir(), ".letta", "settings.json"); + try { + const settings = JSON.parse(readFileSync(settingsPath, "utf-8")); + if (settings.env?.LETTA_API_KEY) { + return settings.env.LETTA_API_KEY; + } + } catch { + // Settings file doesn't exist or is invalid + } + + throw new Error( + "No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.", + ); +} + +/** + * Create a Letta client with auth from env/settings + */ +function createClient(): LettaClient { + return new Letta({ apiKey: getApiKey() }); +} /** * Get memory blocks for a specific agent @@ -20,7 +61,7 @@ import { settingsManager } from "../../../../settings-manager"; * @returns Array of block objects from the API */ export async function getAgentBlocks( - client: Letta, + client: LettaClient, agentId: string, ): Promise>> { return await client.agents.blocks.list(agentId); @@ -34,13 +75,13 @@ function parseArgs(args: string[]): { agentId: string } { return { agentId: args[agentIdIndex + 1] as string }; } -// CLI entry point -if (require.main === module) { +// CLI entry point - check if this file is being run directly +const isMainModule = import.meta.url === `file://${process.argv[1]}`; +if (isMainModule) { (async () => { try { const { agentId } = parseArgs(process.argv.slice(2)); - await settingsManager.initialize(); - const client = await getClient(); + const client = createClient(); const result = await getAgentBlocks(client, agentId); console.log(JSON.stringify(result, null, 2)); } catch (error) { @@ -53,7 +94,7 @@ if (require.main === module) { error.message.includes("Missing required argument") ) { console.error( - "\nUsage: npx ts-node get-agent-blocks.ts --agent-id ", + "\nUsage: npx tsx get-agent-blocks.ts --agent-id ", ); } process.exit(1); diff --git a/src/skills/builtin/searching-messages/scripts/get-messages.ts b/src/skills/builtin/searching-messages/scripts/get-messages.ts index 0a2f16c..f43b791 100644 --- a/src/skills/builtin/searching-messages/scripts/get-messages.ts +++ b/src/skills/builtin/searching-messages/scripts/get-messages.ts @@ -26,9 +26,16 @@ */ import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; import { homedir } from "node:os"; import { join } from "node:path"; -import Letta from "@letta-ai/letta-client"; + +// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected +// (ES module imports don't respect NODE_PATH, but require does) +const require = createRequire(import.meta.url); +const Letta = require("@letta-ai/letta-client") + .default as typeof import("@letta-ai/letta-client").default; +type LettaClient = InstanceType; interface GetMessagesOptions { startDate?: string; @@ -85,7 +92,7 @@ function getAgentId(cliArg?: string): string { /** * Create a Letta client with auth from env/settings */ -function createClient(): Letta { +function createClient(): LettaClient { return new Letta({ apiKey: getApiKey() }); } @@ -96,7 +103,7 @@ function createClient(): Letta { * @returns Array of messages in chronological order */ export async function getMessages( - client: Letta, + client: LettaClient, options: GetMessagesOptions = {}, ): Promise { const agentId = getAgentId(options.agentId); diff --git a/src/skills/builtin/searching-messages/scripts/search-messages.ts b/src/skills/builtin/searching-messages/scripts/search-messages.ts index 83ac4c1..bdd7ac3 100644 --- a/src/skills/builtin/searching-messages/scripts/search-messages.ts +++ b/src/skills/builtin/searching-messages/scripts/search-messages.ts @@ -24,9 +24,16 @@ */ import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; import { homedir } from "node:os"; import { join } from "node:path"; -import Letta from "@letta-ai/letta-client"; + +// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected +// (ES module imports don't respect NODE_PATH, but require does) +const require = createRequire(import.meta.url); +const Letta = require("@letta-ai/letta-client") + .default as typeof import("@letta-ai/letta-client").default; +type LettaClient = InstanceType; interface SearchMessagesOptions { query: string; @@ -83,7 +90,7 @@ function getAgentId(cliArg?: string): string { /** * Create a Letta client with auth from env/settings */ -function createClient(): Letta { +function createClient(): LettaClient { return new Letta({ apiKey: getApiKey() }); } @@ -94,7 +101,7 @@ function createClient(): Letta { * @returns Array of search results with scores */ export async function searchMessages( - client: Letta, + client: LettaClient, options: SearchMessagesOptions, ): Promise>> { // Default to current agent unless --all-agents is specified diff --git a/src/tools/impl/shellEnv.ts b/src/tools/impl/shellEnv.ts index 76d7a68..a1156d3 100644 --- a/src/tools/impl/shellEnv.ts +++ b/src/tools/impl/shellEnv.ts @@ -26,6 +26,24 @@ function getRipgrepBinDir(): string | undefined { } } +/** + * Get the node_modules directory containing this package's dependencies. + * Skill scripts use createRequire with NODE_PATH to resolve dependencies. + */ +function getPackageNodeModulesDir(): string | undefined { + try { + const __filename = fileURLToPath(import.meta.url); + const require = createRequire(__filename); + // Find where letta-client is installed + const clientPath = require.resolve("@letta-ai/letta-client"); + // Extract node_modules path: /a/b/node_modules/@letta-ai/letta-client/... -> /a/b/node_modules + const match = clientPath.match(/^(.+[/\\]node_modules)[/\\]/); + return match ? match[1] : undefined; + } catch { + return undefined; + } +} + /** * Get enhanced environment variables for shell execution. * Includes bundled tools (like ripgrep) in PATH and Letta context for skill scripts. @@ -59,5 +77,15 @@ export function getShellEnv(): NodeJS.ProcessEnv { } } + // Add NODE_PATH for skill scripts to resolve @letta-ai/letta-client + // ES modules don't respect NODE_PATH, but createRequire does + const nodeModulesDir = getPackageNodeModulesDir(); + if (nodeModulesDir) { + const currentNodePath = env.NODE_PATH || ""; + env.NODE_PATH = currentNodePath + ? `${nodeModulesDir}${path.delimiter}${currentNodePath}` + : nodeModulesDir; + } + return env; }