@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/crew/role-query.js +10 -6
- package/package.json +3 -1
- package/sdk/query.js +3 -1
- package/unify/cli.js +735 -0
- package/unify/config.js +269 -0
- package/unify/conversation/persist.js +436 -0
- package/unify/conversation/search.js +65 -0
- package/unify/debug-trace.js +398 -0
- package/unify/engine.js +511 -0
- package/unify/index.js +27 -0
- package/unify/init.js +147 -0
- package/unify/llm/adapter.js +186 -0
- package/unify/llm/anthropic.js +322 -0
- package/unify/llm/chat-completions.js +315 -0
- package/unify/memory/consolidate.js +187 -0
- package/unify/memory/extract.js +97 -0
- package/unify/memory/recall.js +243 -0
- package/unify/memory/store.js +507 -0
- package/unify/models.js +167 -0
- package/unify/prompts.js +109 -0
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
|
+
});
|