@yeaft/webchat-agent 0.1.399 → 0.1.409

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/unify/cli.js ADDED
@@ -0,0 +1,735 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cli.js — Yeaft Unify CLI entry point
4
+ *
5
+ * Features:
6
+ * --dry-run "prompt" — Assemble system prompt + messages, don't call LLM
7
+ * --trace stats|recent|search <keyword> — Query debug.db
8
+ * -i / --interactive — REPL mode with / commands
9
+ * <prompt> — One-shot query (Phase 1: engine.query)
10
+ *
11
+ * Phase 2 additions:
12
+ * /memory — Show memory status or manage entries
13
+ * /history [n] — Show last N messages from conversation
14
+ * /search <keyword> — Search conversation history
15
+ * /compact — Trigger manual consolidation
16
+ * Conversation persistence across REPL sessions
17
+ */
18
+
19
+ import { createInterface } from 'readline';
20
+ import { join } from 'path';
21
+ import { initYeaftDir } from './init.js';
22
+ import { loadConfig } from './config.js';
23
+ import { DebugTrace, NullTrace, createTrace } from './debug-trace.js';
24
+ import { createLLMAdapter } from './llm/adapter.js';
25
+ import { Engine } from './engine.js';
26
+ import { listModels, resolveModel } from './models.js';
27
+ import { buildSystemPrompt } from './prompts.js';
28
+ import { ConversationStore } from './conversation/persist.js';
29
+ import { searchMessages } from './conversation/search.js';
30
+ import { MemoryStore } from './memory/store.js';
31
+ import { shouldConsolidate, consolidate } from './memory/consolidate.js';
32
+
33
+ // ─── Argument parsing ──────────────────────────────────────────
34
+
35
+ function parseArgs(argv) {
36
+ const args = {
37
+ mode: 'chat',
38
+ debug: false,
39
+ interactive: false,
40
+ verbose: false,
41
+ model: null,
42
+ language: null,
43
+ trace: null, // 'stats' | 'recent' | 'search' | 'tools' | null
44
+ traceArg: null, // search keyword or tool name
45
+ dryRun: false,
46
+ prompt: null,
47
+ };
48
+
49
+ const rest = argv.slice(2);
50
+ let i = 0;
51
+
52
+ while (i < rest.length) {
53
+ const arg = rest[i];
54
+ switch (arg) {
55
+ case '-m':
56
+ case '--mode':
57
+ args.mode = rest[++i] || 'chat';
58
+ break;
59
+ case '-d':
60
+ case '--debug':
61
+ args.debug = true;
62
+ break;
63
+ case '-i':
64
+ case '--interactive':
65
+ args.interactive = true;
66
+ break;
67
+ case '-v':
68
+ case '--verbose':
69
+ args.verbose = true;
70
+ break;
71
+ case '--model':
72
+ args.model = rest[++i] || null;
73
+ break;
74
+ case '--language':
75
+ args.language = rest[++i] || null;
76
+ break;
77
+ case '--trace':
78
+ args.trace = rest[++i] || 'stats';
79
+ if (['search', 'tools'].includes(args.trace) && i + 1 < rest.length && !rest[i + 1].startsWith('-')) {
80
+ args.traceArg = rest[++i];
81
+ }
82
+ break;
83
+ case '--dry-run':
84
+ args.dryRun = true;
85
+ break;
86
+ default:
87
+ if (!arg.startsWith('-') && !args.prompt) {
88
+ args.prompt = arg;
89
+ }
90
+ break;
91
+ }
92
+ i++;
93
+ }
94
+
95
+ return args;
96
+ }
97
+
98
+ // ─── Trace query handler ───────────────────────────────────────
99
+
100
+ function handleTraceQuery(args, config) {
101
+ const dbPath = join(config.dir, 'debug.db');
102
+ let trace;
103
+ try {
104
+ trace = new DebugTrace(dbPath);
105
+ } catch (e) {
106
+ throw new Error(`Cannot open debug database at ${dbPath}: ${e.message}`);
107
+ }
108
+
109
+ try {
110
+ switch (args.trace) {
111
+ case 'stats': {
112
+ const s = trace.stats();
113
+ console.log('Debug Trace Statistics:');
114
+ console.log(` Turns: ${s.turnCount}`);
115
+ console.log(` Tools: ${s.toolCount}`);
116
+ console.log(` Events: ${s.eventCount}`);
117
+ console.log(` DB Size: ${(s.dbSizeBytes / 1024).toFixed(1)} KB`);
118
+ break;
119
+ }
120
+ case 'recent': {
121
+ const turns = trace.queryRecent(20);
122
+ if (turns.length === 0) {
123
+ console.log('No recent turns.');
124
+ } else {
125
+ for (const t of turns) {
126
+ const time = new Date(t.started_at).toLocaleString();
127
+ const tokens = t.input_tokens != null ? `${t.input_tokens}+${t.output_tokens} tokens` : 'pending';
128
+ const model = t.model || 'unknown';
129
+ console.log(` [${time}] ${model} | ${tokens} | ${t.stop_reason || 'running'}`);
130
+ }
131
+ }
132
+ break;
133
+ }
134
+ case 'search': {
135
+ if (!args.traceArg) {
136
+ throw new Error('Usage: --trace search <keyword>');
137
+ }
138
+ const results = trace.search(args.traceArg);
139
+ console.log(`Found ${results.length} turns matching "${args.traceArg}":`);
140
+ for (const t of results) {
141
+ const time = new Date(t.started_at).toLocaleString();
142
+ const preview = (t.response_text || '').slice(0, 80);
143
+ console.log(` [${time}] ${preview}...`);
144
+ }
145
+ break;
146
+ }
147
+ case 'tools': {
148
+ const tools = trace.queryTools({ name: args.traceArg });
149
+ console.log(`Found ${tools.length} tool calls${args.traceArg ? ` for "${args.traceArg}"` : ''}:`);
150
+ for (const t of tools.slice(0, 20)) {
151
+ const time = new Date(t.created_at).toLocaleString();
152
+ const status = t.is_error ? 'ERROR' : 'OK';
153
+ console.log(` [${time}] ${t.tool_name} | ${t.duration_ms || '?'}ms | ${status}`);
154
+ }
155
+ break;
156
+ }
157
+ default:
158
+ throw new Error(`Unknown trace command: ${args.trace}. Available: stats, recent, search <keyword>, tools [name]`);
159
+ }
160
+ } finally {
161
+ trace.close();
162
+ }
163
+ }
164
+
165
+ // ─── Dry-run handler ───────────────────────────────────────────
166
+
167
+ function handleDryRun(args, config) {
168
+ const systemPrompt = buildSystemPrompt({ language: config.language, mode: args.mode });
169
+ const messages = [];
170
+
171
+ if (args.prompt) {
172
+ messages.push({ role: 'user', content: args.prompt });
173
+ }
174
+
175
+ console.log('=== DRY RUN ===');
176
+ console.log();
177
+ console.log('--- Config ---');
178
+ console.log(` Model: ${config.model}`);
179
+ console.log(` Adapter: ${config.adapter || 'auto'}`);
180
+ console.log(` Mode: ${args.mode}`);
181
+ console.log(` Debug: ${config.debug}`);
182
+ console.log();
183
+ console.log('--- System Prompt ---');
184
+ console.log(systemPrompt);
185
+ console.log();
186
+ console.log('--- Messages ---');
187
+ for (const msg of messages) {
188
+ console.log(` [${msg.role}] ${msg.content}`);
189
+ }
190
+ if (messages.length === 0) {
191
+ console.log(' (no messages)');
192
+ }
193
+ console.log();
194
+ console.log('=== END DRY RUN ===');
195
+ }
196
+
197
+ // ─── REPL ──────────────────────────────────────────────────────
198
+
199
+ async function runREPL(config, args) {
200
+ const trace = createTrace({
201
+ enabled: config.debug,
202
+ dbPath: join(config.dir, 'debug.db'),
203
+ });
204
+
205
+ // Initialize conversation and memory stores
206
+ const conversationStore = new ConversationStore(config.dir);
207
+ const memoryStore = new MemoryStore(config.dir);
208
+
209
+ let adapter;
210
+ let engine;
211
+ let currentMode = args.mode;
212
+
213
+ // Load persisted conversation as initial messages
214
+ let conversationMessages = conversationStore.loadRecent(50).map(m => ({
215
+ role: m.role,
216
+ content: m.content,
217
+ ...(m.toolCallId && { toolCallId: m.toolCallId }),
218
+ ...(m.toolCalls && { toolCalls: m.toolCalls }),
219
+ }));
220
+
221
+ // Lazy adapter creation (don't fail on start if no API key for --trace-only usage)
222
+ async function ensureEngine() {
223
+ if (!engine) {
224
+ adapter = await createLLMAdapter(config);
225
+ engine = new Engine({ adapter, trace, config, conversationStore, memoryStore });
226
+ }
227
+ return engine;
228
+ }
229
+
230
+ const hotCount = conversationStore.countHot();
231
+ const coldCount = conversationStore.countCold();
232
+ const memStats = memoryStore.stats();
233
+
234
+ console.log(`Yeaft Unify REPL (model: ${config.model}, mode: ${currentMode})`);
235
+ console.log(`Conversation: ${hotCount} hot, ${coldCount} cold | Memory: ${memStats.entryCount} entries`);
236
+ console.log('Type /help for commands, /quit to exit.');
237
+ console.log();
238
+
239
+ const rl = createInterface({
240
+ input: process.stdin,
241
+ output: process.stdout,
242
+ prompt: `yeaft:${currentMode}> `,
243
+ });
244
+
245
+ rl.prompt();
246
+
247
+ rl.on('line', async (line) => {
248
+ const input = line.trim();
249
+ if (!input) {
250
+ rl.prompt();
251
+ return;
252
+ }
253
+
254
+ // Handle / commands
255
+ if (input.startsWith('/')) {
256
+ const [cmd, ...cmdArgs] = input.slice(1).split(/\s+/);
257
+ switch (cmd) {
258
+ case 'help':
259
+ console.log('Commands:');
260
+ console.log(' /mode <chat|work|dream> — Switch mode');
261
+ console.log(' /debug — Toggle debug mode');
262
+ console.log(' /trace <stats|recent> — Query debug trace');
263
+ console.log(' /memory [add|clear|stats] — Memory management');
264
+ console.log(' /history [n] — Show last N messages');
265
+ console.log(' /search <keyword> — Search conversation history');
266
+ console.log(' /compact — Trigger consolidation');
267
+ console.log(' /context — Show context info');
268
+ console.log(' /dry-run — Toggle dry-run mode');
269
+ console.log(' /stats — Show session stats');
270
+ console.log(' /model <name> — Switch model');
271
+ console.log(' /models — List available models');
272
+ console.log(' /language <en|zh> — Switch language');
273
+ console.log(' /clear — Clear conversation history');
274
+ console.log(' /quit — Exit');
275
+ break;
276
+
277
+ case 'mode':
278
+ if (cmdArgs[0]) {
279
+ currentMode = cmdArgs[0];
280
+ rl.setPrompt(`yeaft:${currentMode}> `);
281
+ console.log(`Mode switched to: ${currentMode}`);
282
+ } else {
283
+ console.log(`Current mode: ${currentMode}`);
284
+ }
285
+ break;
286
+
287
+ case 'debug':
288
+ config.debug = !config.debug;
289
+ console.log(`Debug mode: ${config.debug ? 'ON' : 'OFF'}`);
290
+ break;
291
+
292
+ case 'trace': {
293
+ const subcmd = cmdArgs[0] || 'stats';
294
+ try {
295
+ handleTraceQuery({ trace: subcmd, traceArg: cmdArgs[1] }, config);
296
+ } catch (e) {
297
+ console.error(`Trace error: ${e.message}`);
298
+ }
299
+ break;
300
+ }
301
+
302
+ case 'memory': {
303
+ const subcmd = cmdArgs[0];
304
+ if (subcmd === 'add') {
305
+ // /memory add <section> <entry text>
306
+ const section = cmdArgs[1];
307
+ const entryText = cmdArgs.slice(2).join(' ');
308
+ if (!section || !entryText) {
309
+ console.log('Usage: /memory add <section> <entry text>');
310
+ } else {
311
+ memoryStore.addToSection(section, `- ${entryText}`);
312
+ console.log(`Added to MEMORY.md [${section}]: ${entryText}`);
313
+ }
314
+ } else if (subcmd === 'clear') {
315
+ memoryStore.clear();
316
+ console.log('All memory cleared.');
317
+ } else if (subcmd === 'stats') {
318
+ const s = memoryStore.stats();
319
+ console.log('Memory stats:');
320
+ console.log(` Entries: ${s.entryCount}`);
321
+ console.log(` Scopes: ${s.scopes.join(', ') || '(none)'}`);
322
+ console.log(` Kinds: ${Object.entries(s.kinds).map(([k, v]) => `${k}=${v}`).join(', ') || '(none)'}`);
323
+ } else if (subcmd === 'search') {
324
+ const keyword = cmdArgs.slice(1).join(' ');
325
+ if (!keyword) {
326
+ console.log('Usage: /memory search <keyword>');
327
+ } else {
328
+ const results = memoryStore.search(keyword);
329
+ if (results.length === 0) {
330
+ console.log(`No memory entries matching "${keyword}".`);
331
+ } else {
332
+ console.log(`Found ${results.length} entries:`);
333
+ for (const e of results) {
334
+ console.log(` [${e.kind}] ${e.name} (scope: ${e.scope})`);
335
+ console.log(` ${(e.content || '').slice(0, 100)}`);
336
+ }
337
+ }
338
+ }
339
+ } else {
340
+ // Default: show MEMORY.md content
341
+ const profile = memoryStore.readProfile();
342
+ const s = memoryStore.stats();
343
+ if (profile) {
344
+ console.log('--- MEMORY.md ---');
345
+ console.log(profile.slice(0, 1000));
346
+ if (profile.length > 1000) console.log('... (truncated)');
347
+ } else {
348
+ console.log('MEMORY.md is empty.');
349
+ }
350
+ console.log(`\nEntries: ${s.entryCount} | Scopes: ${s.scopes.length}`);
351
+ }
352
+ break;
353
+ }
354
+
355
+ case 'history': {
356
+ const limit = parseInt(cmdArgs[0], 10) || 10;
357
+ const messages = conversationStore.loadRecent(limit);
358
+ if (messages.length === 0) {
359
+ console.log('No messages in history.');
360
+ } else {
361
+ console.log(`Last ${messages.length} messages:`);
362
+ for (const m of messages) {
363
+ const time = m.time ? new Date(m.time).toLocaleString() : '?';
364
+ const preview = (m.content || '').slice(0, 100).replace(/\n/g, ' ');
365
+ console.log(` [${time}] ${m.role}: ${preview}`);
366
+ }
367
+ }
368
+ break;
369
+ }
370
+
371
+ case 'search': {
372
+ const keyword = cmdArgs.join(' ');
373
+ if (!keyword) {
374
+ console.log('Usage: /search <keyword>');
375
+ } else {
376
+ const results = searchMessages(config.dir, keyword, 20);
377
+ if (results.length === 0) {
378
+ console.log(`No messages matching "${keyword}".`);
379
+ } else {
380
+ console.log(`Found ${results.length} messages:`);
381
+ for (const m of results) {
382
+ const time = m.time ? new Date(m.time).toLocaleString() : '?';
383
+ const preview = (m.content || '').slice(0, 100).replace(/\n/g, ' ');
384
+ console.log(` [${time}] ${m.role}: ${preview}`);
385
+ }
386
+ }
387
+ }
388
+ break;
389
+ }
390
+
391
+ case 'compact': {
392
+ try {
393
+ const eng = await ensureEngine();
394
+ const budget = config.messageTokenBudget || 8192;
395
+ const hotTokens = conversationStore.hotTokens();
396
+ console.log(`Hot tokens: ${hotTokens} / budget: ${budget}`);
397
+
398
+ if (hotTokens <= 0) {
399
+ console.log('No messages to compact.');
400
+ break;
401
+ }
402
+
403
+ console.log('Running consolidation...');
404
+ const result = await consolidate({
405
+ conversationStore,
406
+ memoryStore,
407
+ adapter: adapter || await createLLMAdapter(config),
408
+ config,
409
+ budget: Math.min(budget, hotTokens), // force trigger
410
+ });
411
+ console.log(`Consolidation complete:`);
412
+ console.log(` Archived: ${result.archivedCount} messages`);
413
+ console.log(` Extracted: ${result.extractedEntries.length} memory entries`);
414
+ if (result.compactSummary) {
415
+ console.log(` Summary: ${result.compactSummary.slice(0, 200)}...`);
416
+ }
417
+ } catch (e) {
418
+ console.error(`Consolidation error: ${e.message}`);
419
+ }
420
+ break;
421
+ }
422
+
423
+ case 'context':
424
+ console.log(`Context info:`);
425
+ console.log(` Model: ${config.model}`);
426
+ console.log(` Mode: ${currentMode}`);
427
+ console.log(` Language: ${config.language}`);
428
+ console.log(` Max context: ${config.maxContextTokens} tokens`);
429
+ console.log(` System prompt: ${buildSystemPrompt({ language: config.language, mode: currentMode }).length} chars`);
430
+ console.log(` Hot messages: ${conversationStore.countHot()}`);
431
+ console.log(` Hot tokens: ${conversationStore.hotTokens()}`);
432
+ console.log(` Cold messages: ${conversationStore.countCold()}`);
433
+ console.log(` Memory entries: ${memoryStore.stats().entryCount}`);
434
+ break;
435
+
436
+ case 'dry-run':
437
+ handleDryRun({ ...args, mode: currentMode, prompt: cmdArgs.join(' ') || null }, config);
438
+ break;
439
+
440
+ case 'stats': {
441
+ const s = trace.stats();
442
+ console.log(`Session stats:`);
443
+ console.log(` Mode: ${currentMode}`);
444
+ console.log(` Debug: ${config.debug}`);
445
+ console.log(` Turns: ${s.turnCount}`);
446
+ console.log(` Tools: ${s.toolCount}`);
447
+ console.log(` Hot messages: ${conversationStore.countHot()}`);
448
+ console.log(` Cold messages: ${conversationStore.countCold()}`);
449
+ console.log(` Memory entries: ${memoryStore.stats().entryCount}`);
450
+ break;
451
+ }
452
+
453
+ case 'model':
454
+ if (cmdArgs[0]) {
455
+ try {
456
+ config.model = cmdArgs[0];
457
+ // Re-resolve adapter and baseUrl from model registry
458
+ const newModelInfo = resolveModel(config.model);
459
+ if (newModelInfo) {
460
+ config.adapter = newModelInfo.adapter === 'anthropic' ? 'anthropic' : 'openai';
461
+ config.baseUrl = newModelInfo.baseUrl;
462
+ config.maxContextTokens = newModelInfo.contextWindow;
463
+ config.maxOutputTokens = newModelInfo.maxOutputTokens;
464
+ config.modelInfo = newModelInfo;
465
+ }
466
+ engine = null; // Force re-creation with new model + adapter
467
+ console.log(`Model switched to: ${config.model} (adapter: ${config.adapter})`);
468
+ } catch (e) {
469
+ console.error(`Error switching model: ${e.message}`);
470
+ }
471
+ } else {
472
+ console.log(`Current model: ${config.model} (adapter: ${config.adapter})`);
473
+ }
474
+ break;
475
+
476
+ case 'models': {
477
+ const models = listModels();
478
+ console.log('Available models:');
479
+ for (const m of models) {
480
+ const current = m.name === config.model ? ' ← current' : '';
481
+ console.log(` ${m.name} (${m.displayName}) — ${m.adapter}, ${(m.contextWindow / 1000).toFixed(0)}K ctx${current}`);
482
+ }
483
+ break;
484
+ }
485
+
486
+ case 'language':
487
+ case 'lang':
488
+ if (cmdArgs[0]) {
489
+ config.language = cmdArgs[0];
490
+ console.log(`Language switched to: ${config.language}`);
491
+ } else {
492
+ console.log(`Current language: ${config.language}`);
493
+ }
494
+ break;
495
+
496
+ case 'clear':
497
+ conversationStore.clear();
498
+ conversationMessages = [];
499
+ console.log('Conversation history cleared (including persisted messages).');
500
+ break;
501
+
502
+ case 'quit':
503
+ case 'exit':
504
+ case 'q':
505
+ rl.close(); // close handler does trace.close() + process.exit()
506
+ return; // don't call rl.prompt() below
507
+
508
+ default:
509
+ console.log(`Unknown command: /${cmd}. Type /help for commands.`);
510
+ }
511
+ rl.prompt();
512
+ return;
513
+ }
514
+
515
+ // Regular input → engine.query
516
+ try {
517
+ const eng = await ensureEngine();
518
+ let responseText = '';
519
+
520
+ for await (const event of eng.query({
521
+ prompt: input,
522
+ mode: currentMode,
523
+ messages: conversationMessages,
524
+ })) {
525
+ switch (event.type) {
526
+ case 'text_delta':
527
+ responseText += event.text;
528
+ process.stdout.write(event.text);
529
+ break;
530
+ case 'tool_start':
531
+ if (config.debug) {
532
+ process.stderr.write(`\n[tool] ${event.name}(${JSON.stringify(event.input)})\n`);
533
+ }
534
+ break;
535
+ case 'tool_end':
536
+ if (config.debug) {
537
+ const status = event.isError ? 'ERROR' : 'OK';
538
+ process.stderr.write(`[tool] ${event.name} → ${status}\n`);
539
+ }
540
+ break;
541
+ case 'recall':
542
+ if (config.debug) {
543
+ process.stderr.write(`[recall] ${event.entryCount} entries${event.cached ? ' (cached)' : ''}\n`);
544
+ }
545
+ break;
546
+ case 'consolidate':
547
+ if (config.debug) {
548
+ process.stderr.write(`[consolidate] archived=${event.archivedCount}, extracted=${event.extractedCount}\n`);
549
+ }
550
+ break;
551
+ case 'fallback':
552
+ process.stderr.write(`\n[fallback] ${event.from} → ${event.to}: ${event.reason}\n`);
553
+ break;
554
+ case 'error':
555
+ process.stderr.write(`\nError: ${event.error.message}\n`);
556
+ break;
557
+ case 'turn_start':
558
+ if (config.debug && event.turnNumber > 1) {
559
+ process.stderr.write(`\n--- Turn ${event.turnNumber} ---\n`);
560
+ }
561
+ break;
562
+ }
563
+ }
564
+ console.log(); // newline after response
565
+
566
+ // Update in-memory conversation for multi-turn context
567
+ // (engine.js already persists to disk via conversationStore)
568
+ conversationMessages.push({ role: 'user', content: input });
569
+ if (responseText) {
570
+ conversationMessages.push({ role: 'assistant', content: responseText });
571
+ }
572
+ } catch (err) {
573
+ console.error(`Error: ${err.message}`);
574
+ }
575
+ rl.prompt();
576
+ });
577
+
578
+ rl.on('close', () => {
579
+ trace.close();
580
+ console.log('\nBye!');
581
+ process.exit(0);
582
+ });
583
+ }
584
+
585
+ // ─── One-shot handler ──────────────────────────────────────────
586
+
587
+ async function runOnce(config, args) {
588
+ const trace = createTrace({
589
+ enabled: config.debug,
590
+ dbPath: join(config.dir, 'debug.db'),
591
+ });
592
+
593
+ // Initialize stores for persistence
594
+ const conversationStore = new ConversationStore(config.dir);
595
+ const memoryStore = new MemoryStore(config.dir);
596
+
597
+ try {
598
+ if (args.dryRun) {
599
+ handleDryRun(args, config);
600
+ return;
601
+ }
602
+
603
+ const adapter = await createLLMAdapter(config);
604
+ const engine = new Engine({ adapter, trace, config, conversationStore, memoryStore });
605
+
606
+ // Load recent conversation as context
607
+ const priorMessages = conversationStore.loadRecent(20).map(m => ({
608
+ role: m.role,
609
+ content: m.content,
610
+ ...(m.toolCallId && { toolCallId: m.toolCallId }),
611
+ ...(m.toolCalls && { toolCalls: m.toolCalls }),
612
+ }));
613
+
614
+ for await (const event of engine.query({
615
+ prompt: args.prompt,
616
+ mode: args.mode,
617
+ messages: priorMessages,
618
+ })) {
619
+ switch (event.type) {
620
+ case 'text_delta':
621
+ process.stdout.write(event.text);
622
+ break;
623
+ case 'tool_start':
624
+ if (args.verbose) {
625
+ process.stderr.write(`\n[tool] ${event.name}(${JSON.stringify(event.input)})\n`);
626
+ }
627
+ break;
628
+ case 'tool_end':
629
+ if (args.verbose) {
630
+ const status = event.isError ? 'ERROR' : 'OK';
631
+ process.stderr.write(`[tool] ${event.name} → ${status}\n`);
632
+ }
633
+ break;
634
+ case 'recall':
635
+ if (args.verbose || args.debug) {
636
+ process.stderr.write(`[recall] ${event.entryCount} entries${event.cached ? ' (cached)' : ''}\n`);
637
+ }
638
+ break;
639
+ case 'consolidate':
640
+ if (args.verbose || args.debug) {
641
+ process.stderr.write(`[consolidate] archived=${event.archivedCount}, extracted=${event.extractedCount}\n`);
642
+ }
643
+ break;
644
+ case 'fallback':
645
+ process.stderr.write(`\n[fallback] ${event.from} → ${event.to}: ${event.reason}\n`);
646
+ break;
647
+ case 'error':
648
+ process.stderr.write(`\nError: ${event.error.message}\n`);
649
+ break;
650
+ case 'turn_start':
651
+ if (args.verbose && event.turnNumber > 1) {
652
+ process.stderr.write(`\n--- Turn ${event.turnNumber} ---\n`);
653
+ }
654
+ break;
655
+ }
656
+ }
657
+ // Final newline after streaming text
658
+ console.log();
659
+ } finally {
660
+ trace.close();
661
+ }
662
+ }
663
+
664
+ // ─── Main ──────────────────────────────────────────────────────
665
+
666
+ async function main() {
667
+ const args = parseArgs(process.argv);
668
+
669
+ // Load config with CLI overrides
670
+ const config = loadConfig({
671
+ model: args.model,
672
+ language: args.language,
673
+ debug: args.debug || undefined,
674
+ });
675
+
676
+ // Initialize directory structure
677
+ initYeaftDir(config.dir);
678
+
679
+ // Handle --trace queries (no LLM needed)
680
+ if (args.trace) {
681
+ handleTraceQuery(args, config);
682
+ return;
683
+ }
684
+
685
+ // Handle interactive mode
686
+ if (args.interactive) {
687
+ await runREPL(config, args);
688
+ return;
689
+ }
690
+
691
+ // Handle prompt (from args or stdin)
692
+ if (args.prompt) {
693
+ await runOnce(config, args);
694
+ return;
695
+ }
696
+
697
+ // Read from stdin if piped
698
+ if (!process.stdin.isTTY) {
699
+ let input = '';
700
+ for await (const chunk of process.stdin) {
701
+ input += chunk;
702
+ }
703
+ args.prompt = input.trim();
704
+ if (args.prompt) {
705
+ await runOnce(config, args);
706
+ return;
707
+ }
708
+ }
709
+
710
+ // No input and not interactive — show help
711
+ console.log('Yeaft Unify CLI');
712
+ console.log();
713
+ console.log('Usage:');
714
+ console.log(' node cli.js "your prompt" — One-shot query');
715
+ console.log(' node cli.js -i — Interactive REPL');
716
+ console.log(' node cli.js --dry-run "prompt" — Show what would be sent');
717
+ console.log(' node cli.js --trace stats — Debug trace statistics');
718
+ console.log(' node cli.js --trace recent — Recent turns');
719
+ console.log(' node cli.js --trace search "keyword" — Search traces');
720
+ console.log();
721
+ console.log('Options:');
722
+ console.log(' -m, --mode <mode> Mode: chat, work, dream (default: chat)');
723
+ console.log(' -d, --debug Enable debug tracing');
724
+ console.log(' -i, --interactive Start REPL');
725
+ console.log(' -v, --verbose Verbose output');
726
+ console.log(' --model <name> Override model');
727
+ console.log(' --language <code> Language: en, zh (default: en)');
728
+ console.log(' --trace <cmd> Query debug trace');
729
+ console.log(' --dry-run Show prompt without calling LLM');
730
+ }
731
+
732
+ main().catch(err => {
733
+ console.error(err);
734
+ process.exit(1);
735
+ });