feat(cli): implement send command for one-shot agent messages
This commit is contained in:
@@ -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<ChatResponse> => ({
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
+73
-3
@@ -1,12 +1,82 @@
|
|||||||
import type { Command } from 'commander';
|
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<typeof NativeAgent>[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 {
|
export function registerSendCommand(program: Command): void {
|
||||||
program
|
program
|
||||||
.command('send <message>')
|
.command('send <message>')
|
||||||
.description('Send a one-shot message and print the response')
|
.description('Send a one-shot message and print the response')
|
||||||
.option('-c, --config <path>', 'Config file path')
|
.option('-c, --config <path>', 'Config file path')
|
||||||
.action(async (_message: string, _opts: { config?: string }) => {
|
.option('--no-tools', 'Disable tool use')
|
||||||
console.error('Not yet implemented');
|
.action(async (message: string, opts: { config?: string; tools?: boolean }) => {
|
||||||
process.exit(1);
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user