diff --git a/.gitignore b/.gitignore index 18c6f86..8eeff71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,14 @@ +# Letta Code local settings +.letta/settings.local.json +.letta + +.idea node_modules bun.lockb bin/ letta.js letta.js.map .DS_Store -.letta # Logs logs @@ -145,7 +149,3 @@ dist # Vite logs files vite.config.js.timestamp-* vite.config.ts.timestamp-* - -# Letta Code local settings -.letta/settings.local.json -.idea diff --git a/src/headless.ts b/src/headless.ts index f8b5e86..b71aa63 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -18,6 +18,7 @@ export async function handleHeadlessCommand(argv: string[]) { args: argv, options: { continue: { type: "boolean", short: "c" }, + new: { type: "boolean" }, agent: { type: "string", short: "a" }, "output-format": { type: "string" }, }, @@ -51,7 +52,9 @@ export async function handleHeadlessCommand(argv: string[]) { let agent: Letta.AgentState | null = null; const specifiedAgentId = values.agent as string | undefined; const shouldContinue = values.continue as boolean | undefined; + const forceNew = values.new as boolean | undefined; + // Priority 1: Try to use --agent specified ID if (specifiedAgentId) { try { agent = await client.agents.retrieve(specifiedAgentId); @@ -60,6 +63,27 @@ export async function handleHeadlessCommand(argv: string[]) { } } + // Priority 2: Check if --new flag was passed (skip all resume logic) + if (!agent && forceNew) { + agent = await createAgent(); + } + + // Priority 3: Try to resume from project settings (.letta/settings.local.json) + if (!agent) { + const { loadProjectSettings } = await import("./settings"); + const projectSettings = await loadProjectSettings(); + if (projectSettings?.lastAgent) { + try { + agent = await client.agents.retrieve(projectSettings.lastAgent); + } catch (_error) { + console.error( + `Project agent ${projectSettings.lastAgent} not found, creating new one...`, + ); + } + } + } + + // Priority 4: Try to reuse global lastAgent if --continue flag is passed if (!agent && shouldContinue && settings.lastAgent) { try { agent = await client.agents.retrieve(settings.lastAgent); @@ -70,11 +94,16 @@ export async function handleHeadlessCommand(argv: string[]) { } } + // Priority 5: Create a new agent if (!agent) { agent = await createAgent(); - await updateSettings({ lastAgent: agent.id }); } + // Save agent ID to both project and global settings + const { updateProjectSettings } = await import("./settings"); + await updateProjectSettings({ lastAgent: agent.id }); + await updateSettings({ lastAgent: agent.id }); + // Validate output format const outputFormat = (values["output-format"] as string | undefined) || "text"; diff --git a/src/index.ts b/src/index.ts index 6e51819..2e4840f 100755 --- a/src/index.ts +++ b/src/index.ts @@ -14,26 +14,32 @@ Letta Code is a general purpose CLI for interacting with Letta agents USAGE # interactive TUI - letta Start a new agent session - letta --continue Resume the last agent session + letta Auto-resume project agent (from .letta/settings.local.json) + letta --new Force create a new agent + letta --continue Resume global last agent (deprecated, use project-based) letta --agent Open a specific agent by ID # headless - letta --prompt One-off prompt in headless mode (no TTY UI) + letta -p "..." One-off prompt in headless mode (no TTY UI) OPTIONS -h, --help Show this help and exit -v, --version Print version and exit - -c, --continue Resume previous session (uses settings.lastAgent) + --new Force create new agent (skip auto-resume) + -c, --continue Resume previous session (uses global lastAgent, deprecated) -a, --agent Use a specific agent ID -p, --prompt Headless prompt mode --output-format Output format for headless mode (text, json, stream-json) Default: text +BEHAVIOR + By default, letta auto-resumes the last agent used in the current directory + (stored in .letta/settings.local.json). Use --new to force a new agent. + EXAMPLES # when installed as an executable - letta --help - letta --continue + letta # Auto-resume project agent or create new + letta --new # Force new agent letta --agent agent_123 # headless with JSON output (includes stats) @@ -57,6 +63,7 @@ async function main() { help: { type: "boolean", short: "h" }, version: { type: "boolean", short: "v" }, continue: { type: "boolean", short: "c" }, + new: { type: "boolean" }, agent: { type: "string", short: "a" }, prompt: { type: "boolean", short: "p" }, run: { type: "boolean" }, @@ -100,6 +107,7 @@ async function main() { } const shouldContinue = (values.continue as boolean | undefined) ?? false; + const forceNew = (values.new as boolean | undefined) ?? false; const specifiedAgentId = (values.agent as string | undefined) ?? null; const isHeadless = values.prompt || values.run || !process.stdin.isTTY; @@ -179,9 +187,11 @@ async function main() { function LoadingApp({ continueSession, + forceNew, agentIdArg, }: { continueSession: boolean; + forceNew: boolean; agentIdArg: string | null; }) { const [loadingState, setLoadingState] = useState< @@ -190,6 +200,7 @@ async function main() { const [agentId, setAgentId] = useState(null); const [agentState, setAgentState] = useState(null); const [resumeData, setResumeData] = useState(null); + const [isResumingSession, setIsResumingSession] = useState(false); useEffect(() => { async function init() { @@ -202,7 +213,8 @@ async function main() { setLoadingState("initializing"); const { createAgent } = await import("./agent/create"); - const { updateSettings } = await import("./settings"); + const { updateSettings, loadProjectSettings, updateProjectSettings } = + await import("./settings"); let agent: Letta.AgentState | null = null; @@ -218,7 +230,28 @@ async function main() { } } - // Priority 2: Try to reuse lastAgent if --continue flag is passed + // Priority 2: Check if --new flag was passed (skip all resume logic) + if (!agent && forceNew) { + // Create new agent, don't check any lastAgent fields + agent = await createAgent(); + } + + // Priority 3: Try to resume from project settings (.letta/settings.local.json) + if (!agent) { + const projectSettings = await loadProjectSettings(); + if (projectSettings?.lastAgent) { + try { + agent = await client.agents.retrieve(projectSettings.lastAgent); + // console.log(`Resuming project agent ${projectSettings.lastAgent}...`); + } catch (error) { + console.error( + `Project agent ${projectSettings.lastAgent} not found (error: ${JSON.stringify(error)}), creating new one...`, + ); + } + } + } + + // Priority 4: Try to reuse global lastAgent if --continue flag is passed if (!agent && continueSession && settings.lastAgent) { try { agent = await client.agents.retrieve(settings.lastAgent); @@ -230,15 +263,26 @@ async function main() { } } - // Priority 3: Create a new agent + // Priority 5: Create a new agent if (!agent) { agent = await createAgent(); - // Save the new agent ID to settings - await updateSettings({ lastAgent: agent.id }); } - // Get resume data (pending approval + message history) if continuing session or using specific agent - if (continueSession || agentIdArg) { + // Save agent ID to both project and global settings + await updateProjectSettings({ lastAgent: agent.id }); + await updateSettings({ lastAgent: agent.id }); + + // Check if we're resuming an existing agent + const projectSettings = await loadProjectSettings(); + const isResumingProject = + !forceNew && + projectSettings?.lastAgent && + agent.id === projectSettings.lastAgent; + const resuming = continueSession || !!agentIdArg || isResumingProject; + setIsResumingSession(resuming); + + // Get resume data (pending approval + message history) if resuming + if (resuming) { setLoadingState("checking"); const data = await getResumeData(client, agent.id); setResumeData(data); @@ -250,9 +294,7 @@ async function main() { } init(); - }, [continueSession, agentIdArg]); - - const isResumingSession = continueSession || !!agentIdArg; + }, [continueSession, forceNew, agentIdArg]); if (!agentId) { return React.createElement(App, { @@ -279,6 +321,7 @@ async function main() { render( React.createElement(LoadingApp, { continueSession: shouldContinue, + forceNew: forceNew, agentIdArg: specifiedAgentId, }), { diff --git a/src/settings.ts b/src/settings.ts index 9a742eb..4568c04 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,10 +1,10 @@ // src/settings.ts -// Manages user settings stored in ~/.letta/settings.json +// Manages user settings stored in ~/.letta/settings.json and project settings in ./.letta/settings.local.json import { homedir } from "node:os"; import { join } from "node:path"; import type { PermissionRules } from "./permissions/types"; -import { exists, readFile, writeFile } from "./utils/fs.js"; +import { exists, mkdir, readFile, writeFile } from "./utils/fs.js"; export type UIMode = "simple" | "rich"; @@ -17,6 +17,11 @@ export interface Settings { env?: Record; } +export interface ProjectSettings { + lastAgent: string | null; + permissions?: PermissionRules; +} + const DEFAULT_SETTINGS: Settings = { uiMode: "simple", lastAgent: null, @@ -90,3 +95,66 @@ export async function getSetting( const settings = await loadSettings(); return settings[key]; } + +/** + * Get project settings path (./.letta/settings.local.json) + */ +function getProjectSettingsPath(): string { + return join(process.cwd(), ".letta", "settings.local.json"); +} + +/** + * Load project settings from ./.letta/settings.local.json + * Returns null if file doesn't exist + */ +export async function loadProjectSettings(): Promise { + const settingsPath = getProjectSettingsPath(); + + try { + if (!exists(settingsPath)) { + return null; + } + + const content = await readFile(settingsPath); + const settings = JSON.parse(content) as ProjectSettings; + return settings; + } catch (error) { + console.error("Error loading project settings:", error); + return null; + } +} + +/** + * Save project settings to ./.letta/settings.local.json + * Creates .letta directory if it doesn't exist + */ +export async function saveProjectSettings( + settings: ProjectSettings, +): Promise { + const settingsPath = getProjectSettingsPath(); + const dirPath = join(process.cwd(), ".letta"); + + try { + // Create .letta directory if it doesn't exist + if (!exists(dirPath)) { + await mkdir(dirPath, { recursive: true }); + } + + await writeFile(settingsPath, JSON.stringify(settings, null, 2)); + } catch (error) { + console.error("Error saving project settings:", error); + throw error; + } +} + +/** + * Update project settings fields + */ +export async function updateProjectSettings( + updates: Partial, +): Promise { + const currentSettings = (await loadProjectSettings()) || { lastAgent: null }; + const newSettings = { ...currentSettings, ...updates }; + await saveProjectSettings(newSettings); + return newSettings; +} diff --git a/src/utils/fs.ts b/src/utils/fs.ts index 53868f9..73a0fd6 100644 --- a/src/utils/fs.ts +++ b/src/utils/fs.ts @@ -35,3 +35,13 @@ export async function writeFile(path: string, content: string): Promise { export function exists(path: string): boolean { return existsSync(path); } + +/** + * Create a directory, including parent directories + */ +export async function mkdir( + path: string, + options?: { recursive?: boolean }, +): Promise { + mkdirSync(path, options); +}