@fancyrobot/fred-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@fancyrobot/fred-cli",
3
+ "version": "0.1.0",
4
+ "description": "Fred AI framework CLI",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "bin": {
9
+ "fred": "./src/index.ts"
10
+ },
11
+ "scripts": {
12
+ "build": "bun build src/index.ts --outdir dist --target bun --format esm"
13
+ },
14
+ "dependencies": {
15
+ "@fancyrobot/fred": "^0.1.0",
16
+ "@fancyrobot/fred-dev": "^0.1.0"
17
+ },
18
+ "peerDependencies": {
19
+ "effect": "^3.19.0"
20
+ },
21
+ "devDependencies": {
22
+ "effect": "^3.19.15",
23
+ "@types/bun": "latest"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "keywords": ["fred", "cli", "ai", "agents"],
29
+ "license": "MIT"
30
+ }
package/src/dev.ts ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Dev command handler
5
+ * Starts the development chat interface with hot reload
6
+ */
7
+
8
+ import { resolve } from 'path';
9
+ import { existsSync } from 'fs';
10
+ import { pathToFileURL } from 'url';
11
+ import { Fred } from '@fancyrobot/fred';
12
+ import { startDevChat } from '@fancyrobot/fred-dev';
13
+
14
+ /**
15
+ * Try to load and call project's setup() function if it exists
16
+ */
17
+ async function loadProjectSetup(fred: Fred): Promise<void> {
18
+ // Try to find project's index.ts or src/index.ts
19
+ const possiblePaths = [
20
+ resolve(process.cwd(), 'src', 'index.ts'),
21
+ resolve(process.cwd(), 'index.ts'),
22
+ resolve(process.cwd(), 'src', 'index.js'),
23
+ resolve(process.cwd(), 'index.js'),
24
+ ];
25
+
26
+ for (const indexPath of possiblePaths) {
27
+ if (existsSync(indexPath)) {
28
+ try {
29
+ // Dynamically import the project's index file
30
+ // Bun natively supports TypeScript imports, so we can import .ts files directly
31
+ const projectModule = await import(pathToFileURL(indexPath).href);
32
+
33
+ // Check if setup function is exported
34
+ if (typeof projectModule.setup === 'function') {
35
+ // Call the setup function with the Fred instance
36
+ await projectModule.setup(fred);
37
+ return;
38
+ }
39
+ } catch (error) {
40
+ // If import fails (e.g., syntax error, missing dependencies), continue to next path
41
+ // This is expected if the file has errors or doesn't export setup
42
+ continue;
43
+ }
44
+ }
45
+ }
46
+
47
+ // If no setup function found, that's okay - dev-chat will use auto-agent creation
48
+ }
49
+
50
+ /**
51
+ * Handle dev command
52
+ * Uses BunRuntime.runMain internally via startDevChat for proper signal handling.
53
+ * This function never returns - it runs until interrupted.
54
+ */
55
+ export function handleDevCommand(): void {
56
+ const setupHook = async (fred: Fred) => {
57
+ await loadProjectSetup(fred);
58
+ };
59
+
60
+ // startDevChat uses BunRuntime.runMain internally
61
+ // It will handle signals and cleanup, and never returns
62
+ startDevChat(setupHook);
63
+ }
package/src/index.ts ADDED
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Fred CLI
5
+ * Main entry point for CLI commands
6
+ */
7
+
8
+ import { handleTestCommand } from './test';
9
+ import { handleDevCommand } from './dev';
10
+
11
+ /**
12
+ * Options that require a value
13
+ */
14
+ const OPTIONS_REQUIRING_VALUE = new Set([
15
+ 'record',
16
+ 'config',
17
+ 'traces-dir',
18
+ 'tracesDir',
19
+ ]);
20
+
21
+ /**
22
+ * Parse command line arguments
23
+ */
24
+ function parseArgs(args: string[]): { command: string; args: string[]; options: Record<string, any> } {
25
+ const command = args[0] || 'help';
26
+ const remainingArgs: string[] = [];
27
+ const options: Record<string, any> = {};
28
+
29
+ for (let i = 1; i < args.length; i++) {
30
+ const arg = args[i];
31
+
32
+ if (arg.startsWith('--')) {
33
+ const key = arg.substring(2);
34
+ const nextArg = args[i + 1];
35
+ const requiresValue = OPTIONS_REQUIRING_VALUE.has(key);
36
+
37
+ // Check if option requires a value
38
+ if (requiresValue) {
39
+ // Validate that a value is provided
40
+ if (nextArg === undefined || nextArg.startsWith('--')) {
41
+ throw new Error(`Option --${key} requires a value. Example: --${key} <value>`);
42
+ }
43
+ options[key] = nextArg;
44
+ i++; // Skip next arg as it's the value
45
+ } else {
46
+ // Handle boolean flags (options that don't require values)
47
+ if (nextArg === undefined || nextArg.startsWith('--')) {
48
+ options[key] = true;
49
+ } else {
50
+ // If a value is provided for a boolean flag, treat it as the value
51
+ // (some flags might accept optional values)
52
+ options[key] = nextArg;
53
+ i++; // Skip next arg as it's the value
54
+ }
55
+ }
56
+ } else {
57
+ remainingArgs.push(arg);
58
+ }
59
+ }
60
+
61
+ return { command, args: remainingArgs, options };
62
+ }
63
+
64
+ /**
65
+ * Show help message
66
+ */
67
+ function showHelp(): void {
68
+ console.log(`
69
+ Fred CLI
70
+
71
+ Usage:
72
+ fred <command> [options]
73
+
74
+ Commands:
75
+ dev Start development chat interface with hot reload
76
+ - If your project exports setup(fred) from src/index.(ts|js) or index.(ts|js), it will be executed before chat starts
77
+ test Run golden trace tests
78
+ test --record <message> Record a new golden trace
79
+ test --update Update existing golden traces
80
+ test <pattern> Run tests matching pattern
81
+
82
+ Options:
83
+ --config <file> Path to Fred config file
84
+ --traces-dir <dir> Directory for golden traces (default: tests/golden-traces)
85
+
86
+ Examples:
87
+ fred dev
88
+ fred test
89
+ fred test --record "Hello, world!"
90
+ fred test --update
91
+ fred test --config fred.config.yaml
92
+ `);
93
+ }
94
+
95
+ /**
96
+ * Main CLI entry point
97
+ */
98
+ async function main(): Promise<void> {
99
+ const args = process.argv.slice(2);
100
+
101
+ if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
102
+ showHelp();
103
+ process.exit(0);
104
+ }
105
+
106
+ const { command, args: commandArgs, options } = parseArgs(args);
107
+
108
+ try {
109
+ let exitCode = 0;
110
+
111
+ switch (command) {
112
+ case 'dev':
113
+ // handleDevCommand uses BunRuntime.runMain internally and never returns
114
+ // It handles signals and cleanup, and exits the process
115
+ handleDevCommand();
116
+ // This line is never reached
117
+ return;
118
+
119
+ case 'test':
120
+ exitCode = await handleTestCommand(commandArgs, {
121
+ pattern: commandArgs[0],
122
+ update: options.update === true,
123
+ record: options.record,
124
+ tracesDir: options['traces-dir'] || options.tracesDir,
125
+ configFile: options.config,
126
+ });
127
+ break;
128
+
129
+ default:
130
+ console.error(`Unknown command: ${command}`);
131
+ showHelp();
132
+ exitCode = 1;
133
+ }
134
+
135
+ process.exit(exitCode);
136
+ } catch (error) {
137
+ console.error('Error:', error instanceof Error ? error.message : String(error));
138
+ process.exit(1);
139
+ }
140
+ }
141
+
142
+ // Run if executed directly
143
+ if (import.meta.main) {
144
+ main();
145
+ }
package/src/test.ts ADDED
@@ -0,0 +1,262 @@
1
+ import { readdir, readFile, writeFile, mkdir, unlink } from 'fs/promises';
2
+ import { join, resolve, dirname } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import { Fred } from '@fancyrobot/fred';
5
+ import { NoOpTracer, GoldenTraceRecorder, loadGoldenTrace, runTestCase, formatTestResults, TestCase } from '@fancyrobot/fred';
6
+ import { createHash } from 'crypto';
7
+
8
+ /**
9
+ * Test command options
10
+ */
11
+ export interface TestCommandOptions {
12
+ pattern?: string;
13
+ update?: boolean;
14
+ record?: string;
15
+ tracesDir?: string;
16
+ configFile?: string;
17
+ }
18
+
19
+ /**
20
+ * Find all golden trace files
21
+ */
22
+ async function findGoldenTraces(tracesDir: string): Promise<string[]> {
23
+ if (!existsSync(tracesDir)) {
24
+ return [];
25
+ }
26
+
27
+ const files = await readdir(tracesDir);
28
+ return files
29
+ .filter(file => file.endsWith('.json') && file.startsWith('trace-v'))
30
+ .map(file => join(tracesDir, file));
31
+ }
32
+
33
+ /**
34
+ * Find test case files (JSON files with test definitions)
35
+ */
36
+ async function findTestCases(tracesDir: string): Promise<TestCase[]> {
37
+ const testCaseFile = join(tracesDir, 'test-cases.json');
38
+
39
+ if (!existsSync(testCaseFile)) {
40
+ return [];
41
+ }
42
+
43
+ const content = await readFile(testCaseFile, 'utf-8');
44
+ const testCases = JSON.parse(content);
45
+
46
+ if (!Array.isArray(testCases)) {
47
+ throw new Error('test-cases.json must contain an array of test cases');
48
+ }
49
+
50
+ return testCases;
51
+ }
52
+
53
+ /**
54
+ * Record a new golden trace
55
+ */
56
+ export async function recordTrace(
57
+ message: string,
58
+ fred: Fred,
59
+ tracesDir: string,
60
+ options?: { conversationId?: string }
61
+ ): Promise<string> {
62
+ // Create recorder with a base tracer
63
+ const baseTracer = new NoOpTracer();
64
+ const recorder = new GoldenTraceRecorder(baseTracer);
65
+
66
+ // Create tracer with callback to automatically capture spans
67
+ const tracer = new NoOpTracer((span) => {
68
+ recorder.addSpan(span);
69
+ });
70
+
71
+ // Enable tracing with the callback-enabled tracer
72
+ fred.enableTracing(tracer);
73
+
74
+ // Record message
75
+ recorder.recordMessage(message);
76
+
77
+ // Process message (spans will be automatically captured via callback)
78
+ const response = await fred.processMessage(message, {
79
+ conversationId: options?.conversationId,
80
+ });
81
+
82
+ if (!response) {
83
+ throw new Error('No response from agent');
84
+ }
85
+
86
+ // Record response
87
+ recorder.recordResponse(response);
88
+
89
+ // Save trace (spans are already captured via callback)
90
+ const filepath = await recorder.saveToFile(tracesDir);
91
+ console.log(`Recorded golden trace: ${filepath}`);
92
+
93
+ return filepath;
94
+ }
95
+
96
+ /**
97
+ * Run golden trace tests
98
+ */
99
+ export async function runTests(
100
+ tracesDir: string,
101
+ pattern?: string
102
+ ): Promise<boolean> {
103
+ // Find test cases
104
+ const testCases = await findTestCases(tracesDir);
105
+
106
+ if (testCases.length === 0) {
107
+ console.log('No test cases found. Create a test-cases.json file in the traces directory.');
108
+ return true;
109
+ }
110
+
111
+ // Filter by pattern if provided
112
+ const filteredCases = pattern
113
+ ? testCases.filter(tc => tc.name.includes(pattern))
114
+ : testCases;
115
+
116
+ if (filteredCases.length === 0) {
117
+ console.log(`No test cases match pattern: ${pattern}`);
118
+ return true;
119
+ }
120
+
121
+ // Run tests
122
+ const results = [];
123
+ for (const testCase of filteredCases) {
124
+ const result = await runTestCase(testCase, tracesDir);
125
+ results.push(result);
126
+ }
127
+
128
+ // Display results
129
+ console.log(formatTestResults(results));
130
+
131
+ // Return success if all passed
132
+ return results.every(r => r.passed);
133
+ }
134
+
135
+ /**
136
+ * Update golden traces
137
+ */
138
+ export async function updateTraces(
139
+ tracesDir: string,
140
+ fred: Fred,
141
+ pattern?: string
142
+ ): Promise<void> {
143
+ const traceFiles = await findGoldenTraces(tracesDir);
144
+
145
+ if (traceFiles.length === 0) {
146
+ console.log('No golden traces found to update.');
147
+ return;
148
+ }
149
+
150
+ // Filter by pattern if provided
151
+ const filteredFiles = pattern
152
+ ? traceFiles.filter(file => file.includes(pattern))
153
+ : traceFiles;
154
+
155
+ let successCount = 0;
156
+ for (const traceFile of filteredFiles) {
157
+ try {
158
+ const trace = await loadGoldenTrace(traceFile);
159
+ const message = trace.trace.message;
160
+
161
+ console.log(`Updating trace for: "${message.substring(0, 50)}..."`);
162
+
163
+ // Re-record trace
164
+ await recordTrace(message, fred, tracesDir, {
165
+ conversationId: trace.metadata.config?.conversationId,
166
+ });
167
+
168
+ // Remove old trace file now that the new one is created
169
+ await unlink(traceFile);
170
+ successCount++;
171
+ } catch (error) {
172
+ console.error(`Failed to update trace file ${traceFile}:`, error instanceof Error ? error.message : String(error));
173
+ }
174
+ }
175
+
176
+ console.log(`Updated ${successCount} trace(s)`);
177
+ }
178
+
179
+ /**
180
+ * Main test command handler
181
+ */
182
+ export async function handleTestCommand(
183
+ args: string[],
184
+ options: TestCommandOptions
185
+ ): Promise<number> {
186
+ const tracesDir = options.tracesDir || resolve(process.cwd(), 'tests', 'golden-traces');
187
+
188
+ // Ensure traces directory exists
189
+ if (!existsSync(tracesDir)) {
190
+ await mkdir(tracesDir, { recursive: true });
191
+ }
192
+
193
+ try {
194
+ // Handle record command
195
+ if (options.record) {
196
+ // Load Fred instance
197
+ let fred!: Fred;
198
+ if (options.configFile) {
199
+ fred = new Fred();
200
+ await fred.initializeFromConfig(options.configFile);
201
+ } else {
202
+ // Try to find default config
203
+ const defaultConfigs = ['fred.config.yaml', 'fred.config.yml', 'fred.config.json'];
204
+ let configFound = false;
205
+
206
+ for (const configFile of defaultConfigs) {
207
+ if (existsSync(configFile)) {
208
+ fred = new Fred();
209
+ await fred.initializeFromConfig(configFile);
210
+ configFound = true;
211
+ break;
212
+ }
213
+ }
214
+
215
+ if (!configFound) {
216
+ console.error('No config file found. Use --config to specify one.');
217
+ return 1;
218
+ }
219
+ }
220
+
221
+ await recordTrace(options.record, fred, tracesDir);
222
+ return 0;
223
+ }
224
+
225
+ // Handle update command
226
+ if (options.update) {
227
+ // Load Fred instance
228
+ let fred!: Fred;
229
+ if (options.configFile) {
230
+ fred = new Fred();
231
+ await fred.initializeFromConfig(options.configFile);
232
+ } else {
233
+ const defaultConfigs = ['fred.config.yaml', 'fred.config.yml', 'fred.config.json'];
234
+ let configFound = false;
235
+
236
+ for (const configFile of defaultConfigs) {
237
+ if (existsSync(configFile)) {
238
+ fred = new Fred();
239
+ await fred.initializeFromConfig(configFile);
240
+ configFound = true;
241
+ break;
242
+ }
243
+ }
244
+
245
+ if (!configFound) {
246
+ console.error('No config file found. Use --config to specify one.');
247
+ return 1;
248
+ }
249
+ }
250
+
251
+ await updateTraces(tracesDir, fred, options.pattern);
252
+ return 0;
253
+ }
254
+
255
+ // Handle run command (default)
256
+ const success = await runTests(tracesDir, options.pattern);
257
+ return success ? 0 : 1;
258
+ } catch (error) {
259
+ console.error('Error running tests:', error instanceof Error ? error.message : String(error));
260
+ return 1;
261
+ }
262
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }