From ea313159ce7fd1b816a1aa5b1cc4cc2373cabc3b Mon Sep 17 00:00:00 2001 From: Devansh Jain <31609257+devanshrj@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:00:01 -0800 Subject: [PATCH] feat: Support for skills (#76) --- .gitignore | 3 + README.md | 69 ++++++ src/agent/create.ts | 54 ++++- src/agent/memory.ts | 2 +- src/agent/promptAssets.ts | 2 + src/agent/prompts/skills.mdx | 6 + src/agent/prompts/system_prompt.txt | 16 +- src/agent/skills.ts | 336 ++++++++++++++++++++++++++++ src/cli/components/InputRich.tsx | 1 - src/headless.ts | 30 ++- src/index.ts | 18 +- src/models.json | 83 ++++++- 12 files changed, 596 insertions(+), 24 deletions(-) create mode 100644 src/agent/prompts/skills.mdx create mode 100644 src/agent/skills.ts diff --git a/.gitignore b/.gitignore index 8eeff71..53ac828 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ .letta/settings.local.json .letta +# User-defined skills +.skills + .idea node_modules bun.lockb diff --git a/README.md b/README.md index 74245cb..bf0aa97 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,75 @@ Letta Code uses a hierarchical memory system: Memory blocks are highly configurable — see our [docs](https://docs.letta.com/guides/agents/memory-blocks) for advanced configuration options. Join our [Discord](https://discord.gg/letta) to share feedback on persistence patterns for coding agents. +## Skills + +**Skills are automatically discovered from a `.skills` directory in your project.** + +Skills allow you to define custom capabilities that the agent can reference and use. When you start a new session, Letta Code recursively scans for `SKILL.MD` files and loads any skill definitions found. + +### Creating Skills + +Create a `.skills` directory in your project root and organize skills in subdirectories: + +```bash +mkdir -p .skills/data-analysis +``` + +Each skill is defined in a file named `SKILL.MD`. The directory structure determines the skill ID: + +``` +.skills/ +├── data-analysis/ +│ └── SKILL.MD # skill id: "data-analysis" +└── web/ + └── scraper/ + └── SKILL.MD # skill id: "web/scraper" +``` + +Create a skill file (`.skills/data-analysis/SKILL.MD`): + +```markdown +--- +name: Data Analysis Skill +description: Analyzes CSV files and generates statistical reports +category: Data Processing +tags: + - analytics + - statistics + - csv +--- + +# Data Analysis Skill + +This skill analyzes data files and generates comprehensive reports. + +## Usage + +Use this skill to analyze CSV files and generate statistical summaries... +``` + +**Skill File Format:** + +- **File name:** Must be named `SKILL.MD` (case-insensitive) +- **Required frontmatter:** + - `name` - Display name for the skill + - `description` - Brief description of what the skill does +- **Optional frontmatter:** + - `category` - Category for organizing skills (skills are grouped by category in the agent's memory) + - `tags` - Array of tags for filtering/searching +- **Body:** Additional details and documentation about the skill + +Skills are automatically loaded into the agent's memory on startup, making them available for reference throughout your session. + +### Custom Skills Directory + +You can specify a custom skills directory using the `--skills` flag: + +```bash +letta --skills /path/to/custom/skills +letta -p "Use the custom skills" --skills ~/my-skills +``` + ## Usage ### Interactive Mode diff --git a/src/agent/create.ts b/src/agent/create.ts index e2b5d93..9dea07f 100644 --- a/src/agent/create.ts +++ b/src/agent/create.ts @@ -2,6 +2,7 @@ * Utilities for creating an agent on the Letta API backend **/ +import { join } from "node:path"; import type { AgentType } from "@letta-ai/letta-client/resources/agents/agents"; import type { BlockResponse, @@ -11,9 +12,14 @@ import { settingsManager } from "../settings-manager"; import { getToolNames } from "../tools/manager"; import { getClient } from "./client"; import { getDefaultMemoryBlocks } from "./memory"; -import { formatAvailableModels, resolveModel } from "./model"; +import { + formatAvailableModels, + getModelUpdateArgs, + resolveModel, +} from "./model"; import { updateAgentLLMConfig } from "./modify"; import { SYSTEM_PROMPT } from "./promptAssets"; +import { discoverSkills, formatSkillsForMemory, SKILLS_DIR } from "./skills"; export async function createAgent( name = "letta-cli-agent", @@ -21,6 +27,7 @@ export async function createAgent( embeddingModel = "openai/text-embedding-3-small", updateArgs?: Record, forceNewBlocks = false, + skillsDirectory?: string, ) { // Resolve model identifier to handle let modelHandle: string; @@ -52,6 +59,36 @@ export async function createAgent( // Load memory blocks from .mdx files const defaultMemoryBlocks = await getDefaultMemoryBlocks(); + // Resolve absolute path for skills directory + const resolvedSkillsDirectory = + skillsDirectory || join(process.cwd(), SKILLS_DIR); + + // Discover skills from .skills directory and populate skills memory block + try { + const { skills, errors } = await discoverSkills(resolvedSkillsDirectory); + + // Log any errors encountered during skill discovery + if (errors.length > 0) { + console.warn("Errors encountered during skill discovery:"); + for (const error of errors) { + console.warn(` ${error.path}: ${error.message}`); + } + } + + // Find and update the skills memory block with discovered skills + const skillsBlock = defaultMemoryBlocks.find((b) => b.label === "skills"); + if (skillsBlock) { + skillsBlock.value = formatSkillsForMemory( + skills, + resolvedSkillsDirectory, + ); + } + } catch (error) { + console.warn( + `Failed to discover skills: ${error instanceof Error ? error.message : String(error)}`, + ); + } + // Load global shared memory blocks from user settings const settings = settingsManager.getSettings(); const globalSharedBlockIds = settings.globalSharedBlockIds; @@ -123,8 +160,8 @@ export async function createAgent( } blockIds.push(createdBlock.id); - // Categorize: style is local, persona/human are global - if (label === "project") { + // Categorize: project/skills are local, persona/human are global + if (label === "project" || label === "skills") { newLocalBlockIds[label] = createdBlock.id; } else { newGlobalBlockIds[label] = createdBlock.id; @@ -158,6 +195,10 @@ export async function createAgent( ); } + // Get the model's context window from its configuration + const modelUpdateArgs = getModelUpdateArgs(modelHandle); + const contextWindow = (modelUpdateArgs?.context_window as number) || 200_000; + // Create agent with all block IDs (existing + newly created) const agent = await client.agents.create({ agent_type: "letta_v1_agent" as AgentType, @@ -165,7 +206,7 @@ export async function createAgent( name, embedding: embeddingModel, model: modelHandle, - context_window_limit: 200_000, + context_window_limit: contextWindow, tools: toolNames, block_ids: blockIds, tags: ["origin:letta-code"], @@ -182,9 +223,8 @@ export async function createAgent( // Apply updateArgs if provided (e.g., reasoningEffort, contextWindow, etc.) if (updateArgs && Object.keys(updateArgs).length > 0) { await updateAgentLLMConfig(agent.id, modelHandle, updateArgs); - // Refresh agent state to get updated config - return await client.agents.retrieve(agent.id); } - return agent; // { id, ... } + // Always retrieve the agent to ensure we get the full state with populated memory blocks + return await client.agents.retrieve(agent.id); } diff --git a/src/agent/memory.ts b/src/agent/memory.ts index 225a4dd..b91473e 100644 --- a/src/agent/memory.ts +++ b/src/agent/memory.ts @@ -43,7 +43,7 @@ function parseMdxFrontmatter(content: string): { async function loadMemoryBlocksFromMdx(): Promise { const memoryBlocks: CreateBlock[] = []; - const mdxFiles = ["persona.mdx", "human.mdx", "project.mdx"]; + const mdxFiles = ["persona.mdx", "human.mdx", "project.mdx", "skills.mdx"]; // const mdxFiles = ["persona.mdx", "human.mdx", "style.mdx"]; // const mdxFiles = ["persona_kawaii.mdx", "human.mdx", "style.mdx"]; diff --git a/src/agent/promptAssets.ts b/src/agent/promptAssets.ts index 8c46256..d9b1552 100644 --- a/src/agent/promptAssets.ts +++ b/src/agent/promptAssets.ts @@ -4,6 +4,7 @@ import personaPrompt from "./prompts/persona.mdx"; import personaKawaiiPrompt from "./prompts/persona_kawaii.mdx"; import planModeReminder from "./prompts/plan_mode_reminder.txt"; import projectPrompt from "./prompts/project.mdx"; +import skillsPrompt from "./prompts/skills.mdx"; import stylePrompt from "./prompts/style.mdx"; import systemPrompt from "./prompts/system_prompt.txt"; @@ -14,6 +15,7 @@ export const MEMORY_PROMPTS: Record = { "persona.mdx": personaPrompt, "human.mdx": humanPrompt, "project.mdx": projectPrompt, + "skills.mdx": skillsPrompt, "style.mdx": stylePrompt, "persona_kawaii.mdx": personaKawaiiPrompt, }; diff --git a/src/agent/prompts/skills.mdx b/src/agent/prompts/skills.mdx new file mode 100644 index 0000000..6f24260 --- /dev/null +++ b/src/agent/prompts/skills.mdx @@ -0,0 +1,6 @@ +--- +label: skills +description: A memory block to store all available Skills with their metadata (name and description). Whenever a new Skill is discovered / created or an existing Skill is updated, I should store it here. I should always check the `.skills` directory for an updated skill list. +--- + +[CURRENTLY EMPTY: CREATE A LIST OF AVAILABLE SKILLS (DIRECTORIES WITH SKILL.MD FILE) WITH DESCRIPTIONS IN THIS MEMORY BLOCK. ALWAYS CHECK THE `.skills` DIRECTORY TO MAKE SURE YOU HAVE AN UPDATED LIST OF SKILLS] \ No newline at end of file diff --git a/src/agent/prompts/system_prompt.txt b/src/agent/prompts/system_prompt.txt index 1ff5402..b220b23 100644 --- a/src/agent/prompts/system_prompt.txt +++ b/src/agent/prompts/system_prompt.txt @@ -22,4 +22,18 @@ Your memory consists of memory blocks and external memory: Memory management tools allow you to edit existing memory blocks and query for external memories. Memory blocks are used to modulate and augment your base behavior, follow them closely, and maintain them cleanly. -They are the foundation which makes you *you*. \ No newline at end of file +They are the foundation which makes you *you*. + +# Skills +You have access to Skills—folders of instructions, scripts, and resources that you can load dynamically to improve performance on specialized tasks. Skills teach you how to complete specific tasks in a repeatable way. Skills work through progressive disclosure—you should determine which Skills are relevant to complete a task and load them, helping to prevent context window overload. +Each Skill directory includes: +- `SKILL.md` file that starts with YAML frontmatter containing required metadata: name and description +- Additional files within the Skill directory referenced by name from `SKILL.md`. These additional linked files should be navigated and discovered only as needed. +How to use Skills: +- Skills are automatically discovered on bootup. The Skills directory and any available Skills are stored in the `skills` memory block. +- Review available Skills from the `skills` block when you are asked to complete a task. +- If a Skill is relevant, first load the Skill by reading its full `SKILL.md` into context. +- Then, navigate and discover additional linked files in its directory as needed. Don't load additional files immediately, only load them when needed. +- When you load / use Skills, explicitly mention which Skills are loaded / used. +- When the task is completed or when you load new Skills, unload irrelevant Skills by removing them from the context. +Remember to always keep only the full `SKILL.md` into context for all Skills relevant to the current task. Use additional files as needed. \ No newline at end of file diff --git a/src/agent/skills.ts b/src/agent/skills.ts new file mode 100644 index 0000000..8e3f293 --- /dev/null +++ b/src/agent/skills.ts @@ -0,0 +1,336 @@ +/** + * Skills module - provides skill discovery and management functionality + */ + +import { existsSync } from "node:fs"; +import { readdir, readFile } from "node:fs/promises"; +import { join } from "node:path"; + +/** + * Represents a skill that can be used by the agent + */ +export interface Skill { + /** Unique identifier for the skill */ + id: string; + /** Human-readable name of the skill */ + name: string; + /** Description of what the skill does */ + description: string; + /** Optional category for organizing skills */ + category?: string; + /** Optional tags for filtering/searching skills */ + tags?: string[]; + /** Path to the skill file */ + path: string; +} + +/** + * Represents the result of skill discovery + */ +export interface SkillDiscoveryResult { + /** List of discovered skills */ + skills: Skill[]; + /** Any errors encountered during discovery */ + errors: SkillDiscoveryError[]; +} + +/** + * Represents an error that occurred during skill discovery + */ +export interface SkillDiscoveryError { + /** Path where the error occurred */ + path: string; + /** Error message */ + message: string; +} + +/** + * Default directory name where skills are stored + */ +export const SKILLS_DIR = ".skills"; + +/** + * Parse frontmatter and content from a markdown file + */ +function parseFrontmatter(content: string): { + frontmatter: Record; + body: string; +} { + const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/; + const match = content.match(frontmatterRegex); + + if (!match || !match[1] || !match[2]) { + return { frontmatter: {}, body: content }; + } + + const frontmatterText = match[1]; + const body = match[2]; + const frontmatter: Record = {}; + + // Parse YAML-like frontmatter (simple key: value pairs and arrays) + const lines = frontmatterText.split("\n"); + let currentKey: string | null = null; + let currentArray: string[] = []; + + for (const line of lines) { + // Check if this is an array item + if (line.trim().startsWith("-") && currentKey) { + const value = line.trim().slice(1).trim(); + currentArray.push(value); + continue; + } + + // If we were building an array, save it + if (currentKey && currentArray.length > 0) { + frontmatter[currentKey] = currentArray; + currentKey = null; + currentArray = []; + } + + const colonIndex = line.indexOf(":"); + if (colonIndex > 0) { + const key = line.slice(0, colonIndex).trim(); + const value = line.slice(colonIndex + 1).trim(); + currentKey = key; + + if (value) { + // Simple key: value pair + frontmatter[key] = value; + currentKey = null; + } else { + // Might be starting an array + currentArray = []; + } + } + } + + // Save any remaining array + if (currentKey && currentArray.length > 0) { + frontmatter[currentKey] = currentArray; + } + + return { frontmatter, body: body.trim() }; +} + +/** + * Discovers skills by recursively searching for SKILL.MD files + * @param skillsPath - The directory to search for skills (default: .skills in current directory) + * @returns A result containing discovered skills and any errors + */ +export async function discoverSkills( + skillsPath: string = join(process.cwd(), SKILLS_DIR), +): Promise { + const errors: SkillDiscoveryError[] = []; + + // Check if skills directory exists + if (!existsSync(skillsPath)) { + return { skills: [], errors: [] }; + } + + const skills: Skill[] = []; + + try { + // Recursively find all SKILL.MD files + await findSkillFiles(skillsPath, skillsPath, skills, errors); + } catch (error) { + errors.push({ + path: skillsPath, + message: `Failed to read skills directory: ${error instanceof Error ? error.message : String(error)}`, + }); + } + + return { skills, errors }; +} + +/** + * Recursively searches for SKILL.MD files in a directory + * @param currentPath - The current directory being searched + * @param rootPath - The root skills directory + * @param skills - Array to collect found skills + * @param errors - Array to collect errors + */ +async function findSkillFiles( + currentPath: string, + rootPath: string, + skills: Skill[], + errors: SkillDiscoveryError[], +): Promise { + try { + const entries = await readdir(currentPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(currentPath, entry.name); + + if (entry.isDirectory()) { + // Recursively search subdirectories + await findSkillFiles(fullPath, rootPath, skills, errors); + } else if (entry.isFile() && entry.name.toUpperCase() === "SKILL.MD") { + // Found a SKILL.MD file + try { + const skill = await parseSkillFile(fullPath, rootPath); + if (skill) { + skills.push(skill); + } + } catch (error) { + errors.push({ + path: fullPath, + message: error instanceof Error ? error.message : String(error), + }); + } + } + } + } catch (error) { + errors.push({ + path: currentPath, + message: `Failed to read directory: ${error instanceof Error ? error.message : String(error)}`, + }); + } +} + +/** + * Parses a skill file and extracts metadata + * @param filePath - Path to the skill file + * @param rootPath - Root skills directory to derive relative path + * @returns A Skill object or null if parsing fails + */ +async function parseSkillFile( + filePath: string, + rootPath: string, +): Promise { + const content = await readFile(filePath, "utf-8"); + + // Parse frontmatter + const { frontmatter, body } = parseFrontmatter(content); + + // Derive ID from directory structure relative to root + // E.g., .skills/data-analysis/SKILL.MD -> "data-analysis" + // E.g., .skills/web/scraper/SKILL.MD -> "web/scraper" + // Normalize rootPath to not have trailing slash + const normalizedRoot = rootPath.endsWith("/") + ? rootPath.slice(0, -1) + : rootPath; + const relativePath = filePath.slice(normalizedRoot.length + 1); // +1 to remove leading slash + const dirPath = relativePath.slice(0, -"/SKILL.MD".length); + const defaultId = dirPath || "root"; + + const id = + (typeof frontmatter.id === "string" ? frontmatter.id : null) || defaultId; + + // Use name from frontmatter or derive from ID + const name = + (typeof frontmatter.name === "string" ? frontmatter.name : null) || + (typeof frontmatter.title === "string" ? frontmatter.title : null) || + (id.split("/").pop() ?? "") + .replace(/-/g, " ") + .replace(/\b\w/g, (l) => l.toUpperCase()); + + // Description is required - either from frontmatter or first paragraph of content + let description = + typeof frontmatter.description === "string" + ? frontmatter.description + : null; + if (!description) { + // Extract first paragraph from content as description + const firstParagraph = body.trim().split("\n\n")[0]; + description = firstParagraph || "No description available"; + } + + // Strip surrounding quotes from description if present + description = description.trim(); + if ( + (description.startsWith('"') && description.endsWith('"')) || + (description.startsWith("'") && description.endsWith("'")) + ) { + description = description.slice(1, -1); + } + + // Extract tags (handle both string and array) + let tags: string[] | undefined; + if (Array.isArray(frontmatter.tags)) { + tags = frontmatter.tags; + } else if (typeof frontmatter.tags === "string") { + tags = [frontmatter.tags]; + } + + return { + id, + name, + description, + category: + typeof frontmatter.category === "string" + ? frontmatter.category + : undefined, + tags, + path: filePath, + }; +} + +/** + * Formats discovered skills as a string for the skills memory block + * @param skills - Array of discovered skills + * @param skillsDirectory - Absolute path to the skills directory + * @returns Formatted string representation of skills + */ +export function formatSkillsForMemory( + skills: Skill[], + skillsDirectory: string, +): string { + let output = `Skills Directory: ${skillsDirectory}\n\n`; + + if (skills.length === 0) { + return `${output}[NO SKILLS AVAILABLE]`; + } + + output += "Available Skills:\n\n"; + + // Group skills by category if categories exist + const categorized = new Map(); + const uncategorized: Skill[] = []; + + for (const skill of skills) { + if (skill.category) { + const existing = categorized.get(skill.category) || []; + existing.push(skill); + categorized.set(skill.category, existing); + } else { + uncategorized.push(skill); + } + } + + // Output categorized skills + for (const [category, categorySkills] of categorized) { + output += `## ${category}\n\n`; + for (const skill of categorySkills) { + output += formatSkill(skill); + } + output += "\n"; + } + + // Output uncategorized skills + if (uncategorized.length > 0) { + if (categorized.size > 0) { + output += "## Other\n\n"; + } + for (const skill of uncategorized) { + output += formatSkill(skill); + } + } + + return output.trim(); +} + +/** + * Formats a single skill for display + */ +function formatSkill(skill: Skill): string { + let output = `### ${skill.name}\n`; + output += `ID: \`${skill.id}\`\n`; + output += `Description: ${skill.description}\n`; + + if (skill.tags && skill.tags.length > 0) { + output += `Tags: ${skill.tags.map((t) => `\`${t}\``).join(", ")}\n`; + } + + output += "\n"; + return output; +} diff --git a/src/cli/components/InputRich.tsx b/src/cli/components/InputRich.tsx index 5c5198f..9e73e81 100644 --- a/src/cli/components/InputRich.tsx +++ b/src/cli/components/InputRich.tsx @@ -1,6 +1,5 @@ // Import useInput from vendored Ink for bracketed paste support import { Box, Text, useInput } from "ink"; -import Link from "ink-link"; import SpinnerLib from "ink-spinner"; import type { ComponentType } from "react"; import { useEffect, useRef, useState } from "react"; diff --git a/src/headless.ts b/src/headless.ts index 1ec55b3..1643c39 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -17,19 +17,37 @@ import { drainStreamWithResume } from "./cli/helpers/stream"; import { settingsManager } from "./settings-manager"; import { checkToolPermission, executeTool } from "./tools/manager"; -export async function handleHeadlessCommand(argv: string[], model?: string) { +export async function handleHeadlessCommand( + argv: string[], + model?: string, + skillsDirectory?: string, +) { const settings = settingsManager.getSettings(); // Parse CLI args + // Include all flags from index.ts to prevent them from being treated as positionals const { values, positionals } = parseArgs({ args: argv, options: { + // Flags used in headless mode continue: { type: "boolean", short: "c" }, new: { type: "boolean" }, agent: { type: "string", short: "a" }, model: { type: "string", short: "m" }, prompt: { type: "boolean", short: "p" }, "output-format": { type: "string" }, + // Additional flags from index.ts that need to be filtered out + help: { type: "boolean", short: "h" }, + version: { type: "boolean", short: "v" }, + run: { type: "boolean" }, + tools: { type: "string" }, + allowedTools: { type: "string" }, + disallowedTools: { type: "string" }, + "permission-mode": { type: "string" }, + yolo: { type: "boolean" }, + skills: { type: "string" }, + link: { type: "boolean" }, + unlink: { type: "boolean" }, }, strict: false, allowPositionals: true, @@ -81,6 +99,7 @@ export async function handleHeadlessCommand(argv: string[], model?: string) { undefined, updateArgs, forceNew, + skillsDirectory, ); } @@ -113,7 +132,14 @@ export async function handleHeadlessCommand(argv: string[], model?: string) { // Priority 5: Create a new agent if (!agent) { const updateArgs = getModelUpdateArgs(model); - agent = await createAgent(undefined, model, undefined, updateArgs); + agent = await createAgent( + undefined, + model, + undefined, + updateArgs, + false, + skillsDirectory, + ); } // Save agent ID to both project and global settings diff --git a/src/index.ts b/src/index.ts index 745eb24..9a70f5b 100755 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,7 @@ OPTIONS -p, --prompt Headless prompt mode --output-format Output format for headless mode (text, json, stream-json) Default: text + --skills Custom path to skills directory (default: .skills in current directory) BEHAVIOR By default, letta auto-resumes the last agent used in the current directory @@ -91,6 +92,7 @@ async function main() { "permission-mode": { type: "string" }, yolo: { type: "boolean" }, "output-format": { type: "string" }, + skills: { type: "string" }, link: { type: "boolean" }, unlink: { type: "boolean" }, }, @@ -134,6 +136,7 @@ async function main() { const forceNew = (values.new as boolean | undefined) ?? false; const specifiedAgentId = (values.agent as string | undefined) ?? null; const specifiedModel = (values.model as string | undefined) ?? undefined; + const skillsDirectory = (values.skills as string | undefined) ?? undefined; const isHeadless = values.prompt || values.run || !process.stdin.isTTY; // Check if API key is configured @@ -257,7 +260,7 @@ async function main() { await upsertToolsToServer(client); const { handleHeadlessCommand } = await import("./headless"); - await handleHeadlessCommand(process.argv, specifiedModel); + await handleHeadlessCommand(process.argv, specifiedModel, skillsDirectory); return; } @@ -273,11 +276,13 @@ async function main() { forceNew, agentIdArg, model, + skillsDirectory, }: { continueSession: boolean; forceNew: boolean; agentIdArg: string | null; model?: string; + skillsDirectory?: string; }) { const [loadingState, setLoadingState] = useState< | "assembling" @@ -357,6 +362,7 @@ async function main() { undefined, updateArgs, forceNew, + skillsDirectory, ); } @@ -394,7 +400,14 @@ async function main() { // Priority 5: Create a new agent if (!agent) { const updateArgs = getModelUpdateArgs(model); - agent = await createAgent(undefined, model, undefined, updateArgs); + agent = await createAgent( + undefined, + model, + undefined, + updateArgs, + false, + skillsDirectory, + ); } // Ensure local project settings are loaded before updating @@ -461,6 +474,7 @@ async function main() { forceNew: forceNew, agentIdArg: specifiedAgentId, model: specifiedModel, + skillsDirectory: skillsDirectory, }), { exitOnCtrlC: false, // We handle CTRL-C manually with double-press guard diff --git a/src/models.json b/src/models.json index 0f77b0f..a67cef2 100644 --- a/src/models.json +++ b/src/models.json @@ -42,15 +42,6 @@ "context_window": 272000 } }, - { - "id": "glm-4.6", - "handle": "openrouter/z-ai/glm-4.6:exacto", - "label": "GLM-4.6", - "description": "The best open weights coding model", - "updateArgs": { - "context_window": 200000 - } - }, { "id": "gpt-5-minimal", "handle": "openai/gpt-5", @@ -95,6 +86,64 @@ "context_window": 272000 } }, + { + "id": "gpt-5-mini-medium", + "handle": "openai/gpt-5-mini-2025-08-07", + "label": "GPT-5-Mini (medium)", + "description": "OpenAI's latest mini model (using their recommended reasoning level)", + "updateArgs": { + "reasoning_effort": "medium", + "verbosity": "medium", + "context_window": 272000 + } + }, + { + "id": "gpt-5-nano-medium", + "handle": "openai/gpt-5-nano-2025-08-07", + "label": "GPT-5-Nano (medium)", + "description": "OpenAI's latest nano model (using their recommended reasoning level)", + "updateArgs": { + "reasoning_effort": "medium", + "verbosity": "medium", + "context_window": 272000 + } + }, + { + "id": "glm-4.6", + "handle": "openrouter/z-ai/glm-4.6:exacto", + "label": "GLM-4.6", + "description": "The best open weights coding model", + "updateArgs": { + "context_window": 200000 + } + }, + { + "id": "minimax-m2", + "handle": "openrouter/minimax/minimax-m2", + "label": "Minimax M2", + "description": "Minimax's latest model", + "updateArgs": { + "context_window": 196000 + } + }, + { + "id": "kimi-k2", + "handle": "openrouter/moonshotai/kimi-k2-0905", + "label": "Kimi K2", + "description": "Kimi's latest model", + "updateArgs": { + "context_window": 262144 + } + }, + { + "id": "deepseek-chat-v3.1", + "handle": "openrouter/deepseek/deepseek-chat-v3.1", + "label": "DeepSeek Chat V3.1", + "description": "DeepSeek V3.1 model", + "updateArgs": { + "context_window": 128000 + } + }, { "id": "gemini-flash", "handle": "google_ai/gemini-2.5-flash", @@ -114,7 +163,21 @@ "handle": "openai/gpt-4.1", "label": "GPT-4.1", "description": "OpenAI's most recent non-reasoner model", - "updateArgs": { "context_window": 180000 } + "updateArgs": { "context_window": 1047576 } + }, + { + "id": "gpt-4.1-mini", + "handle": "openai/gpt-4.1-mini-2025-04-14", + "label": "GPT-4.1-Mini", + "description": "OpenAI's most recent non-reasoner model (mini version)", + "updateArgs": { "context_window": 1047576 } + }, + { + "id": "gpt-4.1-nano", + "handle": "openai/gpt-4.1-nano-2025-04-14", + "label": "GPT-4.1-Nano", + "description": "OpenAI's most recent non-reasoner model (nano version)", + "updateArgs": { "context_window": 1047576 } }, { "id": "o4-mini",