From 237246a8cf55ad4e699a914484c95564f43dec15 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 5 Feb 2026 22:15:46 -0800 Subject: [PATCH] feat(cli): implement send command for one-shot agent messages --- src/cli/send.test.ts | 25 +++++++++++++++ src/cli/send.ts | 76 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 src/cli/send.test.ts diff --git a/src/cli/send.test.ts b/src/cli/send.test.ts new file mode 100644 index 0000000..cc8b6e7 --- /dev/null +++ b/src/cli/send.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect, vi } from 'vitest'; +import { createSendAgent } from './send.js'; +import type { ChatRequest, ChatResponse } from '../models/types.js'; + +describe('send command', () => { + it('createSendAgent creates an agent that can process a message', async () => { + // Mock model client that returns a canned response + const mockModelClient = { + chat: vi.fn().mockImplementation(async (_request: ChatRequest): Promise => ({ + content: 'Hello from Flynn!', + stopReason: 'end_turn', + usage: { inputTokens: 10, outputTokens: 5 }, + })), + }; + + const agent = createSendAgent({ + modelClient: mockModelClient, + systemPrompt: 'You are Flynn.', + }); + + const response = await agent.process('Hi there'); + expect(response).toBe('Hello from Flynn!'); + expect(mockModelClient.chat).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/cli/send.ts b/src/cli/send.ts index 099d958..b3043ed 100644 --- a/src/cli/send.ts +++ b/src/cli/send.ts @@ -1,12 +1,82 @@ import type { Command } from 'commander'; +import type { ModelClient } from '../models/types.js'; +import { NativeAgent } from '../backends/index.js'; +import { ToolRegistry, ToolExecutor, allBuiltinTools } from '../tools/index.js'; +import { HookEngine } from '../hooks/index.js'; +import { loadConfigSafe, getConfigPath } from './shared.js'; +import { existsSync, readFileSync } from 'fs'; +import { resolve } from 'path'; + +/** Create a lightweight agent for one-shot message processing. */ +export function createSendAgent(deps: { + modelClient: ModelClient; + systemPrompt: string; + enableTools?: boolean; +}): NativeAgent { + const config: ConstructorParameters[0] = { + modelClient: deps.modelClient, + systemPrompt: deps.systemPrompt, + }; + + if (deps.enableTools !== false) { + const hookEngine = new HookEngine({ confirm: [], log: [], silent: [] }); + const toolRegistry = new ToolRegistry(); + for (const tool of allBuiltinTools) { + toolRegistry.register(tool); + } + const toolExecutor = new ToolExecutor(toolRegistry, hookEngine); + config.toolRegistry = toolRegistry; + config.toolExecutor = toolExecutor; + } + + return new NativeAgent(config); +} + +function loadSystemPrompt(): string { + const paths = [ + resolve(process.cwd(), 'SOUL.md'), + resolve(import.meta.dirname, '../../SOUL.md'), + ]; + for (const p of paths) { + if (existsSync(p)) return readFileSync(p, 'utf-8'); + } + return 'You are Flynn, a helpful personal AI assistant.'; +} export function registerSendCommand(program: Command): void { program .command('send ') .description('Send a one-shot message and print the response') .option('-c, --config ', 'Config file path') - .action(async (_message: string, _opts: { config?: string }) => { - console.error('Not yet implemented'); - process.exit(1); + .option('--no-tools', 'Disable tool use') + .action(async (message: string, opts: { config?: string; tools?: boolean }) => { + const configPath = opts.config ?? getConfigPath(); + const { config, error } = loadConfigSafe(configPath); + if (!config) { + console.error(error); + process.exit(1); + } + + // Dynamic import to avoid loading model code eagerly + const { AnthropicClient } = await import('../models/index.js'); + const modelClient = new AnthropicClient({ + model: config.models.default.model, + apiKey: config.models.default.api_key, + authToken: config.models.default.auth_token, + }); + + const agent = createSendAgent({ + modelClient, + systemPrompt: loadSystemPrompt(), + enableTools: opts.tools, + }); + + try { + const response = await agent.process(message); + console.log(response); + } catch (err) { + console.error('Error:', err instanceof Error ? err.message : err); + process.exit(1); + } }); }