@yeaft/webchat-agent 0.1.399 → 0.1.408

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.
@@ -149,12 +149,13 @@ async function _createRoleQueryInner(session, roleName) {
149
149
 
150
150
  // 继承全局 MCP disallowedTools,避免不必要的 tool schema token 消耗
151
151
  const globalDisallowed = ctx.CONFIG?.disallowedTools || [];
152
- // Crew 角色禁用 Agent 和 Skill 工具:
153
- // - Agent: 角色间协作必须通过 ROUTE 块,不能自行启动 sub-agent
154
- // - Skill: skills trigger 词会和角色职能冲突(如 review-code、commit),
155
- // 导致 Claude 调用 Skill 而不是输出 ROUTE 块。从 schema 层面移除比
156
- // canCallTool throw 更可靠(完全不消耗 tool schema token)
157
- const crewDisallowed = ['Agent', 'Skill'];
152
+ // Developer 角色保留 skills(tdd、commit 等对开发有帮助),
153
+ // 其他角色(pm、reviewer、tester)禁用 skills 以避免和 ROUTE 冲突
154
+ const isDeveloper = role.roleType === 'developer';
155
+ // Crew 角色禁用 Agent 工具:角色间协作必须通过 ROUTE 块,不能自行启动 sub-agent
156
+ // developer 角色额外禁用 Skill 工具:skills 的 trigger 词会和角色职能冲突
157
+ // (如 review-code、commit),导致 Claude 调用 Skill 而不是输出 ROUTE 块
158
+ const crewDisallowed = isDeveloper ? ['Agent'] : ['Agent', 'Skill'];
158
159
  const effectiveDisallowed = [...globalDisallowed, ...crewDisallowed];
159
160
 
160
161
  const queryOptions = {
@@ -163,6 +164,9 @@ async function _createRoleQueryInner(session, roleName) {
163
164
  abort: abortController.signal,
164
165
  model: role.model || undefined,
165
166
  appendSystemPrompt: systemPrompt,
167
+ // 非 developer 角色禁用 skills(--disable-slash-commands),
168
+ // 从 CLI 层面阻止 skills 系统 prompt 注入,避免和 ROUTE 冲突
169
+ ...(!isDeveloper && { disableSlashCommands: true }),
166
170
  ...(effectiveDisallowed.length > 0 && { disallowedTools: effectiveDisallowed })
167
171
  };
168
172
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.399",
3
+ "version": "0.1.408",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -52,9 +52,11 @@
52
52
  "ext": "js"
53
53
  },
54
54
  "dependencies": {
55
+ "better-sqlite3": "^11.0.0",
55
56
  "dotenv": "^16.3.1",
56
57
  "tweetnacl": "^1.0.3",
57
58
  "tweetnacl-util": "^0.15.1",
59
+ "uuid": "^11.1.0",
58
60
  "ws": "^8.16.0"
59
61
  },
60
62
  "optionalDependencies": {
package/sdk/query.js CHANGED
@@ -350,7 +350,8 @@ export function query(config) {
350
350
  model,
351
351
  canCallTool,
352
352
  abort,
353
- noSessionPersistence
353
+ noSessionPersistence,
354
+ disableSlashCommands
354
355
  } = {}
355
356
  } = config;
356
357
 
@@ -374,6 +375,7 @@ export function query(config) {
374
375
  if (disallowedTools.length > 0) args.push('--disallowedTools', ...disallowedTools);
375
376
  if (permissionMode) args.push('--permission-mode', permissionMode);
376
377
  if (noSessionPersistence) args.push('--no-session-persistence');
378
+ if (disableSlashCommands) args.push('--disable-slash-commands');
377
379
 
378
380
  // Handle prompt input
379
381
  if (typeof prompt === 'string') {
package/unify/cli.js ADDED
@@ -0,0 +1,537 @@
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
+
12
+ import { createInterface } from 'readline';
13
+ import { join } from 'path';
14
+ import { initYeaftDir } from './init.js';
15
+ import { loadConfig } from './config.js';
16
+ import { DebugTrace, NullTrace, createTrace } from './debug-trace.js';
17
+ import { createLLMAdapter } from './llm/adapter.js';
18
+ import { Engine } from './engine.js';
19
+ import { listModels, resolveModel } from './models.js';
20
+ import { buildSystemPrompt } from './prompts.js';
21
+
22
+ // ─── Argument parsing ──────────────────────────────────────────
23
+
24
+ function parseArgs(argv) {
25
+ const args = {
26
+ mode: 'chat',
27
+ debug: false,
28
+ interactive: false,
29
+ verbose: false,
30
+ model: null,
31
+ language: null,
32
+ trace: null, // 'stats' | 'recent' | 'search' | 'tools' | null
33
+ traceArg: null, // search keyword or tool name
34
+ dryRun: false,
35
+ prompt: null,
36
+ };
37
+
38
+ const rest = argv.slice(2);
39
+ let i = 0;
40
+
41
+ while (i < rest.length) {
42
+ const arg = rest[i];
43
+ switch (arg) {
44
+ case '-m':
45
+ case '--mode':
46
+ args.mode = rest[++i] || 'chat';
47
+ break;
48
+ case '-d':
49
+ case '--debug':
50
+ args.debug = true;
51
+ break;
52
+ case '-i':
53
+ case '--interactive':
54
+ args.interactive = true;
55
+ break;
56
+ case '-v':
57
+ case '--verbose':
58
+ args.verbose = true;
59
+ break;
60
+ case '--model':
61
+ args.model = rest[++i] || null;
62
+ break;
63
+ case '--language':
64
+ args.language = rest[++i] || null;
65
+ break;
66
+ case '--trace':
67
+ args.trace = rest[++i] || 'stats';
68
+ if (['search', 'tools'].includes(args.trace) && i + 1 < rest.length && !rest[i + 1].startsWith('-')) {
69
+ args.traceArg = rest[++i];
70
+ }
71
+ break;
72
+ case '--dry-run':
73
+ args.dryRun = true;
74
+ break;
75
+ default:
76
+ if (!arg.startsWith('-') && !args.prompt) {
77
+ args.prompt = arg;
78
+ }
79
+ break;
80
+ }
81
+ i++;
82
+ }
83
+
84
+ return args;
85
+ }
86
+
87
+ // ─── Trace query handler ───────────────────────────────────────
88
+
89
+ function handleTraceQuery(args, config) {
90
+ const dbPath = join(config.dir, 'debug.db');
91
+ let trace;
92
+ try {
93
+ trace = new DebugTrace(dbPath);
94
+ } catch (e) {
95
+ throw new Error(`Cannot open debug database at ${dbPath}: ${e.message}`);
96
+ }
97
+
98
+ try {
99
+ switch (args.trace) {
100
+ case 'stats': {
101
+ const s = trace.stats();
102
+ console.log('Debug Trace Statistics:');
103
+ console.log(` Turns: ${s.turnCount}`);
104
+ console.log(` Tools: ${s.toolCount}`);
105
+ console.log(` Events: ${s.eventCount}`);
106
+ console.log(` DB Size: ${(s.dbSizeBytes / 1024).toFixed(1)} KB`);
107
+ break;
108
+ }
109
+ case 'recent': {
110
+ const turns = trace.queryRecent(20);
111
+ if (turns.length === 0) {
112
+ console.log('No recent turns.');
113
+ } else {
114
+ for (const t of turns) {
115
+ const time = new Date(t.started_at).toLocaleString();
116
+ const tokens = t.input_tokens != null ? `${t.input_tokens}+${t.output_tokens} tokens` : 'pending';
117
+ const model = t.model || 'unknown';
118
+ console.log(` [${time}] ${model} | ${tokens} | ${t.stop_reason || 'running'}`);
119
+ }
120
+ }
121
+ break;
122
+ }
123
+ case 'search': {
124
+ if (!args.traceArg) {
125
+ throw new Error('Usage: --trace search <keyword>');
126
+ }
127
+ const results = trace.search(args.traceArg);
128
+ console.log(`Found ${results.length} turns matching "${args.traceArg}":`);
129
+ for (const t of results) {
130
+ const time = new Date(t.started_at).toLocaleString();
131
+ const preview = (t.response_text || '').slice(0, 80);
132
+ console.log(` [${time}] ${preview}...`);
133
+ }
134
+ break;
135
+ }
136
+ case 'tools': {
137
+ const tools = trace.queryTools({ name: args.traceArg });
138
+ console.log(`Found ${tools.length} tool calls${args.traceArg ? ` for "${args.traceArg}"` : ''}:`);
139
+ for (const t of tools.slice(0, 20)) {
140
+ const time = new Date(t.created_at).toLocaleString();
141
+ const status = t.is_error ? 'ERROR' : 'OK';
142
+ console.log(` [${time}] ${t.tool_name} | ${t.duration_ms || '?'}ms | ${status}`);
143
+ }
144
+ break;
145
+ }
146
+ default:
147
+ throw new Error(`Unknown trace command: ${args.trace}. Available: stats, recent, search <keyword>, tools [name]`);
148
+ }
149
+ } finally {
150
+ trace.close();
151
+ }
152
+ }
153
+
154
+ // ─── Dry-run handler ───────────────────────────────────────────
155
+
156
+ function handleDryRun(args, config) {
157
+ const systemPrompt = buildSystemPrompt({ language: config.language, mode: args.mode });
158
+ const messages = [];
159
+
160
+ if (args.prompt) {
161
+ messages.push({ role: 'user', content: args.prompt });
162
+ }
163
+
164
+ console.log('=== DRY RUN ===');
165
+ console.log();
166
+ console.log('--- Config ---');
167
+ console.log(` Model: ${config.model}`);
168
+ console.log(` Adapter: ${config.adapter || 'auto'}`);
169
+ console.log(` Mode: ${args.mode}`);
170
+ console.log(` Debug: ${config.debug}`);
171
+ console.log();
172
+ console.log('--- System Prompt ---');
173
+ console.log(systemPrompt);
174
+ console.log();
175
+ console.log('--- Messages ---');
176
+ for (const msg of messages) {
177
+ console.log(` [${msg.role}] ${msg.content}`);
178
+ }
179
+ if (messages.length === 0) {
180
+ console.log(' (no messages)');
181
+ }
182
+ console.log();
183
+ console.log('=== END DRY RUN ===');
184
+ }
185
+
186
+ // ─── REPL ──────────────────────────────────────────────────────
187
+
188
+ async function runREPL(config, args) {
189
+ const trace = createTrace({
190
+ enabled: config.debug,
191
+ dbPath: join(config.dir, 'debug.db'),
192
+ });
193
+
194
+ let adapter;
195
+ let engine;
196
+ let currentMode = args.mode;
197
+ let conversationMessages = []; // persistent conversation for REPL
198
+
199
+ // Lazy adapter creation (don't fail on start if no API key for --trace-only usage)
200
+ async function ensureEngine() {
201
+ if (!engine) {
202
+ adapter = await createLLMAdapter(config);
203
+ engine = new Engine({ adapter, trace, config });
204
+ }
205
+ return engine;
206
+ }
207
+
208
+ console.log(`Yeaft Unify REPL (model: ${config.model}, mode: ${currentMode})`);
209
+ console.log('Type /help for commands, /quit to exit.');
210
+ console.log();
211
+
212
+ const rl = createInterface({
213
+ input: process.stdin,
214
+ output: process.stdout,
215
+ prompt: `yeaft:${currentMode}> `,
216
+ });
217
+
218
+ rl.prompt();
219
+
220
+ rl.on('line', async (line) => {
221
+ const input = line.trim();
222
+ if (!input) {
223
+ rl.prompt();
224
+ return;
225
+ }
226
+
227
+ // Handle / commands
228
+ if (input.startsWith('/')) {
229
+ const [cmd, ...cmdArgs] = input.slice(1).split(/\s+/);
230
+ switch (cmd) {
231
+ case 'help':
232
+ console.log('Commands:');
233
+ console.log(' /mode <chat|work|dream> — Switch mode');
234
+ console.log(' /debug — Toggle debug mode');
235
+ console.log(' /trace <stats|recent> — Query debug trace');
236
+ console.log(' /memory — Show memory status');
237
+ console.log(' /context — Show context info');
238
+ console.log(' /dry-run — Toggle dry-run mode');
239
+ console.log(' /stats — Show session stats');
240
+ console.log(' /model <name> — Switch model');
241
+ console.log(' /models — List available models');
242
+ console.log(' /language <en|zh> — Switch language');
243
+ console.log(' /clear — Clear conversation history');
244
+ console.log(' /quit — Exit');
245
+ break;
246
+
247
+ case 'mode':
248
+ if (cmdArgs[0]) {
249
+ currentMode = cmdArgs[0];
250
+ rl.setPrompt(`yeaft:${currentMode}> `);
251
+ console.log(`Mode switched to: ${currentMode}`);
252
+ } else {
253
+ console.log(`Current mode: ${currentMode}`);
254
+ }
255
+ break;
256
+
257
+ case 'debug':
258
+ config.debug = !config.debug;
259
+ console.log(`Debug mode: ${config.debug ? 'ON' : 'OFF'}`);
260
+ break;
261
+
262
+ case 'trace': {
263
+ const subcmd = cmdArgs[0] || 'stats';
264
+ try {
265
+ handleTraceQuery({ trace: subcmd, traceArg: cmdArgs[1] }, config);
266
+ } catch (e) {
267
+ console.error(`Trace error: ${e.message}`);
268
+ }
269
+ break;
270
+ }
271
+
272
+ case 'memory':
273
+ console.log('Memory status: (not yet implemented — Phase 2)');
274
+ break;
275
+
276
+ case 'context':
277
+ console.log(`Context info:`);
278
+ console.log(` Model: ${config.model}`);
279
+ console.log(` Mode: ${currentMode}`);
280
+ console.log(` Language: ${config.language}`);
281
+ console.log(` Max context: ${config.maxContextTokens} tokens`);
282
+ console.log(` System prompt: ${buildSystemPrompt({ language: config.language, mode: currentMode }).length} chars`);
283
+ break;
284
+
285
+ case 'dry-run':
286
+ handleDryRun({ ...args, mode: currentMode, prompt: cmdArgs.join(' ') || null }, config);
287
+ break;
288
+
289
+ case 'stats': {
290
+ const s = trace.stats();
291
+ console.log(`Session stats:`);
292
+ console.log(` Mode: ${currentMode}`);
293
+ console.log(` Debug: ${config.debug}`);
294
+ console.log(` Turns: ${s.turnCount}`);
295
+ console.log(` Tools: ${s.toolCount}`);
296
+ break;
297
+ }
298
+
299
+ case 'model':
300
+ if (cmdArgs[0]) {
301
+ try {
302
+ config.model = cmdArgs[0];
303
+ // Re-resolve adapter and baseUrl from model registry
304
+ const newModelInfo = resolveModel(config.model);
305
+ if (newModelInfo) {
306
+ config.adapter = newModelInfo.adapter === 'anthropic' ? 'anthropic' : 'openai';
307
+ config.baseUrl = newModelInfo.baseUrl;
308
+ config.maxContextTokens = newModelInfo.contextWindow;
309
+ config.maxOutputTokens = newModelInfo.maxOutputTokens;
310
+ config.modelInfo = newModelInfo;
311
+ }
312
+ engine = null; // Force re-creation with new model + adapter
313
+ console.log(`Model switched to: ${config.model} (adapter: ${config.adapter})`);
314
+ } catch (e) {
315
+ console.error(`Error switching model: ${e.message}`);
316
+ }
317
+ } else {
318
+ console.log(`Current model: ${config.model} (adapter: ${config.adapter})`);
319
+ }
320
+ break;
321
+
322
+ case 'models': {
323
+ const models = listModels();
324
+ console.log('Available models:');
325
+ for (const m of models) {
326
+ const current = m.name === config.model ? ' ← current' : '';
327
+ console.log(` ${m.name} (${m.displayName}) — ${m.adapter}, ${(m.contextWindow / 1000).toFixed(0)}K ctx${current}`);
328
+ }
329
+ break;
330
+ }
331
+
332
+ case 'language':
333
+ case 'lang':
334
+ if (cmdArgs[0]) {
335
+ config.language = cmdArgs[0];
336
+ console.log(`Language switched to: ${config.language}`);
337
+ } else {
338
+ console.log(`Current language: ${config.language}`);
339
+ }
340
+ break;
341
+
342
+ case 'clear':
343
+ conversationMessages = [];
344
+ console.log('Conversation history cleared.');
345
+ break;
346
+
347
+ case 'quit':
348
+ case 'exit':
349
+ case 'q':
350
+ rl.close(); // close handler does trace.close() + process.exit()
351
+ return; // don't call rl.prompt() below
352
+
353
+ default:
354
+ console.log(`Unknown command: /${cmd}. Type /help for commands.`);
355
+ }
356
+ rl.prompt();
357
+ return;
358
+ }
359
+
360
+ // Regular input → engine.query
361
+ try {
362
+ const eng = await ensureEngine();
363
+ let responseText = '';
364
+
365
+ for await (const event of eng.query({
366
+ prompt: input,
367
+ mode: currentMode,
368
+ messages: conversationMessages,
369
+ })) {
370
+ switch (event.type) {
371
+ case 'text_delta':
372
+ responseText += event.text;
373
+ process.stdout.write(event.text);
374
+ break;
375
+ case 'tool_start':
376
+ if (config.debug) {
377
+ process.stderr.write(`\n[tool] ${event.name}(${JSON.stringify(event.input)})\n`);
378
+ }
379
+ break;
380
+ case 'tool_end':
381
+ if (config.debug) {
382
+ const status = event.isError ? 'ERROR' : 'OK';
383
+ process.stderr.write(`[tool] ${event.name} → ${status}\n`);
384
+ }
385
+ break;
386
+ case 'error':
387
+ process.stderr.write(`\nError: ${event.error.message}\n`);
388
+ break;
389
+ case 'turn_start':
390
+ if (config.debug && event.turnNumber > 1) {
391
+ process.stderr.write(`\n--- Turn ${event.turnNumber} ---\n`);
392
+ }
393
+ break;
394
+ }
395
+ }
396
+ console.log(); // newline after response
397
+
398
+ // Save both user and assistant messages for multi-turn context
399
+ conversationMessages.push({ role: 'user', content: input });
400
+ if (responseText) {
401
+ conversationMessages.push({ role: 'assistant', content: responseText });
402
+ }
403
+ } catch (err) {
404
+ console.error(`Error: ${err.message}`);
405
+ }
406
+ rl.prompt();
407
+ });
408
+
409
+ rl.on('close', () => {
410
+ trace.close();
411
+ console.log('\nBye!');
412
+ process.exit(0);
413
+ });
414
+ }
415
+
416
+ // ─── One-shot handler ──────────────────────────────────────────
417
+
418
+ async function runOnce(config, args) {
419
+ const trace = createTrace({
420
+ enabled: config.debug,
421
+ dbPath: join(config.dir, 'debug.db'),
422
+ });
423
+
424
+ try {
425
+ if (args.dryRun) {
426
+ handleDryRun(args, config);
427
+ return;
428
+ }
429
+
430
+ const adapter = await createLLMAdapter(config);
431
+ const engine = new Engine({ adapter, trace, config });
432
+
433
+ for await (const event of engine.query({ prompt: args.prompt, mode: args.mode })) {
434
+ switch (event.type) {
435
+ case 'text_delta':
436
+ process.stdout.write(event.text);
437
+ break;
438
+ case 'tool_start':
439
+ if (args.verbose) {
440
+ process.stderr.write(`\n[tool] ${event.name}(${JSON.stringify(event.input)})\n`);
441
+ }
442
+ break;
443
+ case 'tool_end':
444
+ if (args.verbose) {
445
+ const status = event.isError ? 'ERROR' : 'OK';
446
+ process.stderr.write(`[tool] ${event.name} → ${status}\n`);
447
+ }
448
+ break;
449
+ case 'error':
450
+ process.stderr.write(`\nError: ${event.error.message}\n`);
451
+ break;
452
+ case 'turn_start':
453
+ if (args.verbose && event.turnNumber > 1) {
454
+ process.stderr.write(`\n--- Turn ${event.turnNumber} ---\n`);
455
+ }
456
+ break;
457
+ }
458
+ }
459
+ // Final newline after streaming text
460
+ console.log();
461
+ } finally {
462
+ trace.close();
463
+ }
464
+ }
465
+
466
+ // ─── Main ──────────────────────────────────────────────────────
467
+
468
+ async function main() {
469
+ const args = parseArgs(process.argv);
470
+
471
+ // Load config with CLI overrides
472
+ const config = loadConfig({
473
+ model: args.model,
474
+ language: args.language,
475
+ debug: args.debug || undefined,
476
+ });
477
+
478
+ // Initialize directory structure
479
+ initYeaftDir(config.dir);
480
+
481
+ // Handle --trace queries (no LLM needed)
482
+ if (args.trace) {
483
+ handleTraceQuery(args, config);
484
+ return;
485
+ }
486
+
487
+ // Handle interactive mode
488
+ if (args.interactive) {
489
+ await runREPL(config, args);
490
+ return;
491
+ }
492
+
493
+ // Handle prompt (from args or stdin)
494
+ if (args.prompt) {
495
+ await runOnce(config, args);
496
+ return;
497
+ }
498
+
499
+ // Read from stdin if piped
500
+ if (!process.stdin.isTTY) {
501
+ let input = '';
502
+ for await (const chunk of process.stdin) {
503
+ input += chunk;
504
+ }
505
+ args.prompt = input.trim();
506
+ if (args.prompt) {
507
+ await runOnce(config, args);
508
+ return;
509
+ }
510
+ }
511
+
512
+ // No input and not interactive — show help
513
+ console.log('Yeaft Unify CLI');
514
+ console.log();
515
+ console.log('Usage:');
516
+ console.log(' node cli.js "your prompt" — One-shot query');
517
+ console.log(' node cli.js -i — Interactive REPL');
518
+ console.log(' node cli.js --dry-run "prompt" — Show what would be sent');
519
+ console.log(' node cli.js --trace stats — Debug trace statistics');
520
+ console.log(' node cli.js --trace recent — Recent turns');
521
+ console.log(' node cli.js --trace search "keyword" — Search traces');
522
+ console.log();
523
+ console.log('Options:');
524
+ console.log(' -m, --mode <mode> Mode: chat, work, dream (default: chat)');
525
+ console.log(' -d, --debug Enable debug tracing');
526
+ console.log(' -i, --interactive Start REPL');
527
+ console.log(' -v, --verbose Verbose output');
528
+ console.log(' --model <name> Override model');
529
+ console.log(' --language <code> Language: en, zh (default: en)');
530
+ console.log(' --trace <cmd> Query debug trace');
531
+ console.log(' --dry-run Show prompt without calling LLM');
532
+ }
533
+
534
+ main().catch(err => {
535
+ console.error(err);
536
+ process.exit(1);
537
+ });