fix: make skill scripts work when installed via npm (#460)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-04 17:38:34 -08:00
committed by GitHub
parent 8f32e1f7f6
commit 56b40465dd
7 changed files with 318 additions and 66 deletions

View File

@@ -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 <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<typeof Letta>;
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<Awaited<ReturnType<typeof client.agents.list>>> {
const params: Parameters<typeof client.agents.list>[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 <name> Exact name match

View File

@@ -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 <block-id> --target-agent-id <agent-id> [--read-only]
* npx tsx attach-block.ts --block-id <block-id> [--agent-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<typeof Letta>;
/**
* 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<Awaited<ReturnType<typeof client.agents.blocks.attach>>> {
// 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 <block-id> [--read-only]",
"\nUsage: npx tsx attach-block.ts --block-id <block-id> [--agent-id <agent-id>] [--read-only]",
);
}
process.exit(1);

View File

@@ -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 <block-id> [--label <new-label>]
* npx tsx copy-block.ts --block-id <block-id> [--label <new-label>] [--agent-id <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<typeof Letta>;
interface CopyBlockResult {
sourceBlock: Awaited<ReturnType<typeof Letta.prototype.blocks.retrieve>>;
newBlock: Awaited<ReturnType<typeof Letta.prototype.blocks.create>>;
attachResult: Awaited<
ReturnType<typeof Letta.prototype.agents.blocks.attach>
>;
sourceBlock: Awaited<ReturnType<LettaClient["blocks"]["retrieve"]>>;
newBlock: Awaited<ReturnType<LettaClient["blocks"]["create"]>>;
attachResult: Awaited<ReturnType<LettaClient["agents"]["blocks"]["attach"]>>;
}
/**
* 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<CopyBlockResult> {
// 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 <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 <block-id> [--label <new-label>]",
"\nUsage: npx tsx copy-block.ts --block-id <block-id> [--label <new-label>] [--agent-id <agent-id>]",
);
}
process.exit(1);

View File

@@ -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 <agent-id>
* npx tsx get-agent-blocks.ts --agent-id <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<typeof Letta>;
/**
* 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<Awaited<ReturnType<typeof client.agents.blocks.list>>> {
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 <agent-id>",
"\nUsage: npx tsx get-agent-blocks.ts --agent-id <agent-id>",
);
}
process.exit(1);

View File

@@ -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<typeof Letta>;
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<unknown[]> {
const agentId = getAgentId(options.agentId);

View File

@@ -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<typeof Letta>;
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<Awaited<ReturnType<typeof client.messages.search>>> {
// Default to current agent unless --all-agents is specified

View File

@@ -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;
}