chainlesschain 0.47.6 → 0.47.7

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.
Files changed (107) hide show
  1. package/package.json +2 -2
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/Analytics-BFI7jbwM.css +1 -0
  4. package/src/assets/web-panel/assets/Analytics-DQ135mAd.js +3 -0
  5. package/src/assets/web-panel/assets/AppLayout-6SPt_8Y_.js +1 -0
  6. package/src/assets/web-panel/assets/AppLayout-BFJ-Fofn.css +1 -0
  7. package/src/assets/web-panel/assets/{Backup-Ba9UybpT.js → Backup-DbVRG5vE.js} +1 -1
  8. package/src/assets/web-panel/assets/{Chat-BwXskT21.js → Chat-wVhrFK9C.js} +1 -1
  9. package/src/assets/web-panel/assets/{Cowork-UmOe7qvE.js → Cowork-lOC25IW2.js} +1 -1
  10. package/src/assets/web-panel/assets/{Cron-JHS-rc-4.js → Cron-3P0eVLTV.js} +1 -1
  11. package/src/assets/web-panel/assets/{Dashboard-B95cMCO7.js → Dashboard-Br7kCwKJ.js} +1 -1
  12. package/src/assets/web-panel/assets/{Git-CSYO0_zk.js → Git-CrDCcBig.js} +2 -2
  13. package/src/assets/web-panel/assets/{Logs-Hxw_K0km.js → Logs-BfTE8urP.js} +1 -1
  14. package/src/assets/web-panel/assets/{McpTools-DIE75TrB.js → McpTools-CsGIijNe.js} +1 -1
  15. package/src/assets/web-panel/assets/{Memory-C4KVnLlp.js → Memory-BXX_yMKJ.js} +1 -1
  16. package/src/assets/web-panel/assets/{Notes-DuzrHMAk.js → Notes-DU6Vf2cL.js} +1 -1
  17. package/src/assets/web-panel/assets/{Organization-DTq6uF82.js → Organization-Bny6yOPV.js} +4 -4
  18. package/src/assets/web-panel/assets/{P2P-C0hjlhsR.js → P2P-BxFZ1Bit.js} +2 -2
  19. package/src/assets/web-panel/assets/{Permissions-Ec0NH-xC.js → Permissions-B1j3Mtms.js} +3 -3
  20. package/src/assets/web-panel/assets/{Projects-U8D0asCS.js → Projects-D-CGscDu.js} +1 -1
  21. package/src/assets/web-panel/assets/{Providers-BngtTLvJ.js → Providers-r6NaBYMf.js} +1 -1
  22. package/src/assets/web-panel/assets/{RssFeed-B9NbwCKM.js → RssFeed-D7b68C5q.js} +1 -1
  23. package/src/assets/web-panel/assets/{Security-BL5Rkr1T.js → Security-MJfKv0EJ.js} +3 -3
  24. package/src/assets/web-panel/assets/{Services-D4MJzLld.js → Services-Yb_Q1V3d.js} +1 -1
  25. package/src/assets/web-panel/assets/{Skills-CQTOMDwF.js → Skills-DLTHcH5T.js} +1 -1
  26. package/src/assets/web-panel/assets/{Tasks-DepbJMnL.js → Tasks-CqycpPjS.js} +1 -1
  27. package/src/assets/web-panel/assets/{Templates-C24PVZPu.js → Templates-y01u2Zis.js} +1 -1
  28. package/src/assets/web-panel/assets/VideoEditing-BA1N-5kq.css +1 -0
  29. package/src/assets/web-panel/assets/VideoEditing-B_nPKw6B.js +1 -0
  30. package/src/assets/web-panel/assets/{Wallet-PQoSpN_P.js → Wallet-CsRgnjJY.js} +1 -1
  31. package/src/assets/web-panel/assets/{WebAuthn-BcuyQ4Lr.js → WebAuthn-DWoR5ADp.js} +1 -1
  32. package/src/assets/web-panel/assets/{WorkflowEditor-C-SvXbHW.js → WorkflowEditor-DBJhFPMN.js} +1 -1
  33. package/src/assets/web-panel/assets/{antd-DEjZPGMj.js → antd-Dh2t0vGq.js} +84 -84
  34. package/src/assets/web-panel/assets/index-tN-8TosE.js +2 -0
  35. package/src/assets/web-panel/assets/{markdown-CusdXFxb.js → markdown-CBnGGMzE.js} +1 -1
  36. package/src/assets/web-panel/index.html +2 -2
  37. package/src/commands/agent.js +20 -0
  38. package/src/commands/mcp.js +86 -4
  39. package/src/commands/memory.js +85 -4
  40. package/src/commands/sandbox.js +80 -6
  41. package/src/commands/serve.js +10 -0
  42. package/src/commands/session.js +250 -0
  43. package/src/commands/stream.js +75 -0
  44. package/src/commands/video.js +363 -0
  45. package/src/gateways/http/envelope-http-server.js +194 -0
  46. package/src/gateways/ws/message-dispatcher.js +123 -0
  47. package/src/gateways/ws/session-core-protocol.js +427 -0
  48. package/src/gateways/ws/session-protocol.js +42 -1
  49. package/src/gateways/ws/video-protocol.js +230 -0
  50. package/src/gateways/ws/ws-server.js +72 -0
  51. package/src/gateways/ws/ws-session-gateway.js +7 -3
  52. package/src/harness/jsonl-session-store.js +17 -9
  53. package/src/index.js +8 -0
  54. package/src/lib/agent-stream.js +63 -0
  55. package/src/lib/chat-core.js +183 -6
  56. package/src/lib/cowork/ab-comparator-cli.js +44 -23
  57. package/src/lib/cowork/agent-group-runner.js +145 -0
  58. package/src/lib/cowork/debate-review-cli.js +47 -25
  59. package/src/lib/cowork/project-style-analyzer-cli.js +34 -7
  60. package/src/lib/interaction-adapter.js +59 -1
  61. package/src/lib/jsonl-session-store.js +2 -0
  62. package/src/lib/memory-injection.js +90 -0
  63. package/src/lib/provider-stream.js +120 -0
  64. package/src/lib/sandbox-v2.js +198 -3
  65. package/src/lib/session-consolidator.js +125 -0
  66. package/src/lib/session-core-singletons.js +56 -0
  67. package/src/lib/session-tail.js +128 -0
  68. package/src/lib/session-usage.js +166 -0
  69. package/src/lib/shell-approval.js +96 -0
  70. package/src/lib/ws-chat-handler.js +3 -0
  71. package/src/repl/agent-repl.js +271 -6
  72. package/src/repl/chat-repl.js +87 -100
  73. package/src/runtime/agent-core.js +98 -15
  74. package/src/runtime/agent-runtime.js +105 -3
  75. package/src/runtime/policies/agent-policy.js +10 -0
  76. package/src/skills/video-editing/SKILL.md +46 -0
  77. package/src/skills/video-editing/beat-snap.js +127 -0
  78. package/src/skills/video-editing/extractors/audio-extractor.js +212 -0
  79. package/src/skills/video-editing/extractors/subtitle-extractor.js +90 -0
  80. package/src/skills/video-editing/extractors/video-extractor.js +137 -0
  81. package/src/skills/video-editing/parallel-orchestrator.js +212 -0
  82. package/src/skills/video-editing/pipeline.js +480 -0
  83. package/src/skills/video-editing/prompts/aesthetic-analysis.md +21 -0
  84. package/src/skills/video-editing/prompts/audio-segment.md +15 -0
  85. package/src/skills/video-editing/prompts/character-identify.md +19 -0
  86. package/src/skills/video-editing/prompts/dense-caption.md +20 -0
  87. package/src/skills/video-editing/prompts/editor-system.md +29 -0
  88. package/src/skills/video-editing/prompts/hook-dialogue.md +17 -0
  89. package/src/skills/video-editing/prompts/protagonist-detect.md +20 -0
  90. package/src/skills/video-editing/prompts/scene-caption.md +16 -0
  91. package/src/skills/video-editing/prompts/shot-caption.md +25 -0
  92. package/src/skills/video-editing/prompts/shot-plan.md +28 -0
  93. package/src/skills/video-editing/prompts/structure-proposal.md +16 -0
  94. package/src/skills/video-editing/prompts/vlog-scene-caption.md +18 -0
  95. package/src/skills/video-editing/render/audio-mix.js +128 -0
  96. package/src/skills/video-editing/render/ffmpeg-concat.js +45 -0
  97. package/src/skills/video-editing/render/ffmpeg-extract.js +67 -0
  98. package/src/skills/video-editing/reviewer.js +161 -0
  99. package/src/skills/video-editing/tools/commit.js +108 -0
  100. package/src/skills/video-editing/tools/review-clip.js +46 -0
  101. package/src/skills/video-editing/tools/semantic-retrieval.js +56 -0
  102. package/src/skills/video-editing/tools/shot-trimming.js +73 -0
  103. package/src/assets/web-panel/assets/Analytics-B4OM8S8X.css +0 -1
  104. package/src/assets/web-panel/assets/Analytics-DgypYeUB.js +0 -3
  105. package/src/assets/web-panel/assets/AppLayout-Bzf3mSZI.js +0 -1
  106. package/src/assets/web-panel/assets/AppLayout-DQyDwGut.css +0 -1
  107. package/src/assets/web-panel/assets/index-CwvzTTw_.js +0 -2
@@ -34,6 +34,7 @@ import {
34
34
  appendUserMessage,
35
35
  appendAssistantMessage,
36
36
  appendCompactEvent,
37
+ appendTokenUsage,
37
38
  rebuildMessages,
38
39
  sessionExists,
39
40
  } from "../harness/jsonl-session-store.js";
@@ -70,18 +71,24 @@ import {
70
71
  */
71
72
  let _hookDb = null;
72
73
  let _compressor = null;
74
+ let _approvalGate = null;
73
75
 
74
76
  /**
75
77
  * Execute a tool call — delegates to agent-core with REPL's hookDb and cwd.
76
78
  */
77
79
  async function executeTool(name, args) {
78
- return coreExecuteTool(name, args, { hookDb: _hookDb, cwd: process.cwd() });
80
+ return coreExecuteTool(name, args, {
81
+ hookDb: _hookDb,
82
+ cwd: process.cwd(),
83
+ approvalGate: _approvalGate,
84
+ });
79
85
  }
80
86
 
81
87
  /**
82
88
  * Agentic loop — wraps agent-core's async generator with REPL display output.
83
89
  */
84
90
  async function agentLoop(messages, options) {
91
+ const usageEvents = [];
85
92
  for await (const event of coreAgentLoop(messages, options)) {
86
93
  if (event.type === "tool-executing") {
87
94
  process.stdout.write(
@@ -97,6 +104,8 @@ async function agentLoop(messages, options) {
97
104
  } else if (event.result?.success) {
98
105
  process.stdout.write(chalk.green(` Done\n`));
99
106
  }
107
+ } else if (event.type === "token-usage") {
108
+ usageEvents.push(event);
100
109
  } else if (event.type === "iteration-warning") {
101
110
  process.stdout.write(chalk.yellow(`\n ${event.message}\n`));
102
111
  } else if (event.type === "iteration-budget-exhausted") {
@@ -104,10 +113,10 @@ async function agentLoop(messages, options) {
104
113
  chalk.red(`\n [Budget Exhausted] ${event.budget}\n`),
105
114
  );
106
115
  } else if (event.type === "response-complete") {
107
- return event.content;
116
+ return { content: event.content, usageEvents };
108
117
  }
109
118
  }
110
- return "";
119
+ return { content: "", usageEvents };
111
120
  }
112
121
 
113
122
  /**
@@ -155,6 +164,39 @@ export async function startAgentRepl(options = {}) {
155
164
  // Set hook DB reference for tool pipeline
156
165
  _hookDb = db;
157
166
 
167
+ // Wire the persistent ApprovalGate singleton (approval-policies.json) with
168
+ // a readline confirm prompt. agent-core's run_shell branch gates
169
+ // MEDIUM/HIGH-risk commands against the session's policy tier
170
+ // (strict / trusted / autopilot).
171
+ try {
172
+ const { getApprovalGate } =
173
+ await import("../lib/session-core-singletons.js");
174
+ _approvalGate = await getApprovalGate();
175
+ if (typeof _approvalGate.setConfirmer === "function") {
176
+ _approvalGate.setConfirmer(async ({ args, riskLevel }) => {
177
+ const rlConfirm = readline.createInterface({
178
+ input: process.stdin,
179
+ output: process.stdout,
180
+ });
181
+ const q = (p) => new Promise((res) => rlConfirm.question(p, res));
182
+ const cmd = args?.command ? ` ${args.command}` : "";
183
+ const ans = (
184
+ await q(
185
+ chalk.yellow(
186
+ `\n[ApprovalGate] ${riskLevel || "medium"} risk command:${cmd}\n Proceed? (y/N) `,
187
+ ),
188
+ )
189
+ )
190
+ .trim()
191
+ .toLowerCase();
192
+ rlConfirm.close();
193
+ return ans === "y" || ans === "yes";
194
+ });
195
+ }
196
+ } catch (_err) {
197
+ _approvalGate = null;
198
+ }
199
+
158
200
  // Resume existing session or create new one
159
201
  const useJsonl = feature("JSONL_SESSION");
160
202
 
@@ -200,10 +242,177 @@ export async function startAgentRepl(options = {}) {
200
242
  }
201
243
  }
202
244
 
245
+ // Phase H — register this session with session-core SessionManager so
246
+ // `cc session lifecycle / park / unpark / end` can see and control it.
247
+ // Resume a previously parked handle if --session points at one; otherwise
248
+ // create a fresh handle keyed by the JSONL sessionId.
249
+ let _sessionMgr = null;
250
+ let _sessionHandle = null;
251
+ try {
252
+ const { getSessionManager } =
253
+ await import("../lib/session-core-singletons.js");
254
+ _sessionMgr = getSessionManager();
255
+ if (sessionId) {
256
+ if (options.sessionId && !_sessionMgr.has(sessionId)) {
257
+ // Try unparking; no-op if nothing parked with that id
258
+ try {
259
+ await _sessionMgr.resume(sessionId);
260
+ } catch (_e) {
261
+ /* non-critical */
262
+ }
263
+ }
264
+ if (!_sessionMgr.has(sessionId)) {
265
+ _sessionHandle = _sessionMgr.create({
266
+ agentId: options.agentId || "cli-agent",
267
+ sessionId,
268
+ metadata: { provider, model },
269
+ });
270
+ } else {
271
+ _sessionHandle = _sessionMgr.get(sessionId);
272
+ }
273
+ }
274
+ } catch (_err) {
275
+ // Non-critical — SessionManager integration must not block startup
276
+ }
277
+
203
278
  const messages = [
204
279
  { role: "system", content: buildSystemPrompt(process.cwd()) },
205
280
  ];
206
281
 
282
+ // Deep Agents Deploy Phase 1 — load agent bundle if --bundle provided.
283
+ // Injects AGENTS.md as system prompt, seeds USER.md into MemoryStore,
284
+ // and applies bundle manifest metadata (model/provider override, agentId).
285
+ let _bundleResolved = null;
286
+ let _bundleMcpClient = null;
287
+ if (options.bundlePath) {
288
+ try {
289
+ const { loadBundle } =
290
+ await import("@chainlesschain/session-core/agent-bundle-loader");
291
+ const { resolveBundle } =
292
+ await import("@chainlesschain/session-core/agent-bundle-resolver");
293
+ const { getMemoryStore } =
294
+ await import("../lib/session-core-singletons.js");
295
+ const bundle = loadBundle(options.bundlePath);
296
+
297
+ const memoryStore = getMemoryStore();
298
+ _bundleResolved = resolveBundle(bundle, {
299
+ memoryStore,
300
+ seedOptions: {
301
+ userId: options.agentId || null,
302
+ },
303
+ });
304
+
305
+ if (_bundleResolved.systemPrompt) {
306
+ messages.push({
307
+ role: "system",
308
+ content: _bundleResolved.systemPrompt,
309
+ });
310
+ }
311
+
312
+ if (_bundleResolved.manifest) {
313
+ if (_bundleResolved.manifest.model && !options.model) {
314
+ model = _bundleResolved.manifest.model;
315
+ }
316
+ if (_bundleResolved.manifest.provider && !options.provider) {
317
+ provider = _bundleResolved.manifest.provider;
318
+ }
319
+ }
320
+
321
+ // Connect bundle MCP servers (stdio transport, local mode only).
322
+ const mcpServers = _bundleResolved.mcpConfig?.servers;
323
+ if (mcpServers && typeof mcpServers === "object") {
324
+ const serverEntries = Object.entries(mcpServers).filter(
325
+ ([, cfg]) => cfg && cfg.command,
326
+ );
327
+ if (serverEntries.length > 0) {
328
+ try {
329
+ const { MCPClient } = await import("../harness/mcp-client.js");
330
+ _bundleMcpClient = new MCPClient();
331
+ let connected = 0;
332
+ for (const [name, cfg] of serverEntries) {
333
+ try {
334
+ await _bundleMcpClient.connect(name, cfg);
335
+ connected += 1;
336
+ } catch (mcpErr) {
337
+ logger.log(
338
+ chalk.yellow(
339
+ `Bundle MCP: "${name}" connect failed — ${mcpErr.message}`,
340
+ ),
341
+ );
342
+ }
343
+ }
344
+ if (connected === 0) {
345
+ await _bundleMcpClient.disconnectAll().catch(() => undefined);
346
+ _bundleMcpClient = null;
347
+ }
348
+ } catch (mcpInitErr) {
349
+ logger.log(
350
+ chalk.yellow(`Bundle MCP: init failed — ${mcpInitErr.message}`),
351
+ );
352
+ _bundleMcpClient = null;
353
+ }
354
+ }
355
+ }
356
+
357
+ const seedInfo = _bundleResolved.seedResult;
358
+ const seedMsg =
359
+ seedInfo && seedInfo.seeded > 0
360
+ ? `, seeded ${seedInfo.seeded} user memories`
361
+ : "";
362
+ const mcpMsg = _bundleMcpClient
363
+ ? `, ${_bundleMcpClient.servers.size} MCP servers`
364
+ : "";
365
+ const warnMsg =
366
+ _bundleResolved.warnings.length > 0
367
+ ? ` (${_bundleResolved.warnings.length} warnings)`
368
+ : "";
369
+ logger.log(
370
+ chalk.gray(
371
+ `Bundle: loaded ${_bundleResolved.manifest?.id || path.basename(options.bundlePath)}${seedMsg}${mcpMsg}${warnMsg}`,
372
+ ),
373
+ );
374
+ } catch (err) {
375
+ logger.log(chalk.red(`Bundle: failed to load — ${err.message}`));
376
+ }
377
+ }
378
+
379
+ // Apply bundle approval policy to this session (after both gate and sessionId are ready)
380
+ if (_bundleResolved?.approvalPolicy?.default && _approvalGate && sessionId) {
381
+ try {
382
+ _approvalGate.setSessionPolicy(
383
+ sessionId,
384
+ _bundleResolved.approvalPolicy.default,
385
+ );
386
+ } catch (_err) {
387
+ // Non-critical — invalid policy value is silently ignored
388
+ }
389
+ }
390
+
391
+ // Phase G #5 — inject top-K memory recall into system prompt for new sessions
392
+ // Skip on resume (existing context already reflects prior work) and when
393
+ // --no-recall-memory is passed.
394
+ if (!options.sessionId && options.recallMemory !== false) {
395
+ try {
396
+ const { buildMemoryInjection } =
397
+ await import("../lib/memory-injection.js");
398
+ const injection = buildMemoryInjection({
399
+ agentId: options.agentId || null,
400
+ query: options.recallQuery || "",
401
+ limit: Number(options.recallLimit) || undefined,
402
+ });
403
+ if (injection) {
404
+ messages.push({ role: injection.role, content: injection.content });
405
+ logger.log(
406
+ chalk.gray(
407
+ `Context: recalled ${injection.count} memory entries into system prompt`,
408
+ ),
409
+ );
410
+ }
411
+ } catch (_err) {
412
+ // Non-critical — memory recall failure must not block REPL startup
413
+ }
414
+ }
415
+
207
416
  // Load resumed session messages
208
417
  if (options.sessionId && sessionId) {
209
418
  try {
@@ -1235,7 +1444,7 @@ export async function startAgentRepl(options = {}) {
1235
1444
  try {
1236
1445
  process.stdout.write("\n");
1237
1446
  const iterationBudget = new IterationBudget({ owner: sessionId });
1238
- const response = await agentLoop(messages, {
1447
+ const { content: response, usageEvents } = await agentLoop(messages, {
1239
1448
  provider,
1240
1449
  model: activeModel,
1241
1450
  baseUrl,
@@ -1245,8 +1454,24 @@ export async function startAgentRepl(options = {}) {
1245
1454
  sessionId,
1246
1455
  cwd: process.cwd(),
1247
1456
  prepareCall: defaultPrepareCall,
1457
+ approvalGate: _approvalGate,
1458
+ mcpClient: _bundleMcpClient || undefined,
1248
1459
  });
1249
1460
 
1461
+ if (sessionId && usageEvents?.length) {
1462
+ for (const ue of usageEvents) {
1463
+ try {
1464
+ appendTokenUsage(sessionId, {
1465
+ provider: ue.provider,
1466
+ model: ue.model,
1467
+ usage: ue.usage,
1468
+ });
1469
+ } catch (_e) {
1470
+ /* best-effort */
1471
+ }
1472
+ }
1473
+ }
1474
+
1250
1475
  // Fire AssistantResponse hook with rewrite/suppress support
1251
1476
  const responseDirective = await fireAssistantResponse(
1252
1477
  _hookDb,
@@ -1270,8 +1495,18 @@ export async function startAgentRepl(options = {}) {
1270
1495
  }
1271
1496
 
1272
1497
  if (effectiveResponse) {
1273
- process.stdout.write(`\n${effectiveResponse}\n\n`);
1274
- messages.push({ role: "assistant", content: effectiveResponse });
1498
+ // Phase G #2 — route through StreamRouter so REPL / WS / future
1499
+ // streaming providers share one StreamEvent protocol.
1500
+ const { streamAgentResponse } = await import("../lib/agent-stream.js");
1501
+ process.stdout.write("\n");
1502
+ const noStream = options.noStream === true;
1503
+ const streamResult = await streamAgentResponse(effectiveResponse, {
1504
+ noStream,
1505
+ writer: noStream ? null : (chunk) => process.stdout.write(chunk),
1506
+ });
1507
+ if (noStream) process.stdout.write(streamResult.text);
1508
+ process.stdout.write("\n\n");
1509
+ messages.push({ role: "assistant", content: streamResult.text });
1275
1510
  } else if (!responseDirective.suppress) {
1276
1511
  process.stdout.write("\n");
1277
1512
  }
@@ -1398,6 +1633,36 @@ export async function startAgentRepl(options = {}) {
1398
1633
  messageCount: messages.length,
1399
1634
  });
1400
1635
 
1636
+ // Phase H — park the SessionManager handle on clean exit so the session
1637
+ // can be resumed later via `cc session unpark <id>`. `--no-park-on-exit`
1638
+ // opts out; a SIGINT path (process-level) will force close instead.
1639
+ if (_sessionMgr && sessionId) {
1640
+ try {
1641
+ if (options.parkOnExit === false) {
1642
+ await _sessionMgr.close(sessionId);
1643
+ } else {
1644
+ _sessionMgr.markIdle(sessionId);
1645
+ await _sessionMgr.park(sessionId);
1646
+ logger.log(
1647
+ chalk.gray(
1648
+ `Session ${sessionId.slice(0, 12)} parked — resume with: cc session unpark ${sessionId}`,
1649
+ ),
1650
+ );
1651
+ }
1652
+ } catch (_e) {
1653
+ // Non-critical — parking failure must not block shutdown
1654
+ }
1655
+ }
1656
+
1657
+ // Disconnect bundle MCP servers
1658
+ if (_bundleMcpClient) {
1659
+ try {
1660
+ await _bundleMcpClient.disconnectAll();
1661
+ } catch (_e) {
1662
+ // Non-critical
1663
+ }
1664
+ }
1665
+
1401
1666
  // Shutdown runtime
1402
1667
  try {
1403
1668
  await shutdown();
@@ -12,6 +12,16 @@ import readline from "readline";
12
12
  import chalk from "chalk";
13
13
  import { logger } from "../lib/logger.js";
14
14
  import { BUILT_IN_PROVIDERS } from "../lib/llm-providers.js";
15
+ import {
16
+ streamOllama,
17
+ streamOpenAI,
18
+ streamAnthropic,
19
+ } from "../lib/chat-core.js";
20
+ import {
21
+ startSession,
22
+ appendTokenUsage,
23
+ appendEvent,
24
+ } from "../harness/jsonl-session-store.js";
15
25
 
16
26
  const SLASH_COMMANDS = {
17
27
  "/exit": "Exit the chat",
@@ -23,104 +33,6 @@ const SLASH_COMMANDS = {
23
33
  "/help": "Show available commands",
24
34
  };
25
35
 
26
- /**
27
- * Stream a response from Ollama
28
- */
29
- async function streamOllama(messages, model, baseUrl, onToken) {
30
- const response = await fetch(`${baseUrl}/api/chat`, {
31
- method: "POST",
32
- headers: { "Content-Type": "application/json" },
33
- body: JSON.stringify({
34
- model,
35
- messages,
36
- stream: true,
37
- }),
38
- });
39
-
40
- if (!response.ok) {
41
- throw new Error(`Ollama error: ${response.status} ${response.statusText}`);
42
- }
43
-
44
- const reader = response.body.getReader();
45
- const decoder = new TextDecoder();
46
- let fullResponse = "";
47
-
48
- while (true) {
49
- const { done, value } = await reader.read();
50
- if (done) break;
51
-
52
- const text = decoder.decode(value, { stream: true });
53
- const lines = text.split("\n").filter(Boolean);
54
-
55
- for (const line of lines) {
56
- try {
57
- const json = JSON.parse(line);
58
- if (json.message?.content) {
59
- fullResponse += json.message.content;
60
- onToken(json.message.content);
61
- }
62
- } catch {
63
- // Partial JSON, skip
64
- }
65
- }
66
- }
67
-
68
- return fullResponse;
69
- }
70
-
71
- /**
72
- * Stream a response from OpenAI-compatible API
73
- */
74
- async function streamOpenAI(messages, model, baseUrl, apiKey, onToken) {
75
- const response = await fetch(`${baseUrl}/chat/completions`, {
76
- method: "POST",
77
- headers: {
78
- "Content-Type": "application/json",
79
- Authorization: `Bearer ${apiKey}`,
80
- },
81
- body: JSON.stringify({
82
- model,
83
- messages,
84
- stream: true,
85
- }),
86
- });
87
-
88
- if (!response.ok) {
89
- throw new Error(`API error: ${response.status} ${response.statusText}`);
90
- }
91
-
92
- const reader = response.body.getReader();
93
- const decoder = new TextDecoder();
94
- let fullResponse = "";
95
-
96
- while (true) {
97
- const { done, value } = await reader.read();
98
- if (done) break;
99
-
100
- const text = decoder.decode(value, { stream: true });
101
- const lines = text.split("\n").filter(Boolean);
102
-
103
- for (const line of lines) {
104
- if (line.startsWith("data: ")) {
105
- const data = line.slice(6);
106
- if (data === "[DONE]") continue;
107
- try {
108
- const json = JSON.parse(data);
109
- const content = json.choices?.[0]?.delta?.content;
110
- if (content) {
111
- fullResponse += content;
112
- onToken(content);
113
- }
114
- } catch {
115
- // Partial data
116
- }
117
- }
118
- }
119
- }
120
-
121
- return fullResponse;
122
- }
123
-
124
36
  /**
125
37
  * Start the interactive chat REPL
126
38
  * @param {object} options
@@ -133,6 +45,21 @@ export async function startChatRepl(options = {}) {
133
45
 
134
46
  const messages = [];
135
47
 
48
+ // Phase J — attach chat REPL to a JSONL session so token_usage is recorded
49
+ // and `cc session usage` / `usage.*` WS routes show real numbers.
50
+ let sessionId = options.sessionId || null;
51
+ if (!sessionId && options.recordUsage !== false) {
52
+ try {
53
+ sessionId = startSession(null, {
54
+ title: "chat-repl",
55
+ provider,
56
+ model,
57
+ });
58
+ } catch {
59
+ sessionId = null;
60
+ }
61
+ }
62
+
136
63
  const rl = readline.createInterface({
137
64
  input: process.stdin,
138
65
  output: process.stdout,
@@ -142,6 +69,7 @@ export async function startChatRepl(options = {}) {
142
69
 
143
70
  logger.log(chalk.bold("\nChainlessChain AI Chat"));
144
71
  logger.log(chalk.gray(`Model: ${model} Provider: ${provider}`));
72
+ if (sessionId) logger.log(chalk.gray(`Session: ${sessionId}`));
145
73
  logger.log(chalk.gray("Type /help for commands, /exit to quit\n"));
146
74
 
147
75
  rl.prompt();
@@ -234,9 +162,43 @@ export async function startChatRepl(options = {}) {
234
162
  try {
235
163
  let response;
236
164
  const onToken = (token) => process.stdout.write(token);
165
+ let capturedUsage = null;
166
+ const onUsage = (u) => {
167
+ capturedUsage = u;
168
+ };
169
+
170
+ if (sessionId)
171
+ appendEvent(sessionId, "user_message", { content: trimmed });
237
172
 
238
173
  if (provider === "ollama") {
239
- response = await streamOllama(messages, model, baseUrl, onToken);
174
+ response = await streamOllama(
175
+ messages,
176
+ model,
177
+ baseUrl,
178
+ onToken,
179
+ onUsage,
180
+ );
181
+ } else if (provider === "anthropic") {
182
+ const providerDef = BUILT_IN_PROVIDERS.anthropic;
183
+ const url =
184
+ baseUrl !== "http://localhost:11434"
185
+ ? baseUrl
186
+ : providerDef?.baseUrl || "https://api.anthropic.com/v1";
187
+ const key =
188
+ apiKey ||
189
+ (providerDef?.apiKeyEnv ? process.env[providerDef.apiKeyEnv] : null);
190
+ if (!key)
191
+ throw new Error(
192
+ `API key required for anthropic (set ${providerDef?.apiKeyEnv || "ANTHROPIC_API_KEY"})`,
193
+ );
194
+ response = await streamAnthropic(
195
+ messages,
196
+ model,
197
+ url,
198
+ key,
199
+ onToken,
200
+ onUsage,
201
+ );
240
202
  } else {
241
203
  // OpenAI-compatible providers (openai, volcengine, deepseek, dashscope, mistral, gemini, anthropic-proxy)
242
204
  const providerDef = BUILT_IN_PROVIDERS[provider];
@@ -251,11 +213,36 @@ export async function startChatRepl(options = {}) {
251
213
  throw new Error(
252
214
  `API key required for ${provider} (set ${providerDef?.apiKeyEnv || "API key"})`,
253
215
  );
254
- response = await streamOpenAI(messages, model, url, key, onToken);
216
+ response = await streamOpenAI(
217
+ messages,
218
+ model,
219
+ url,
220
+ key,
221
+ onToken,
222
+ onUsage,
223
+ );
255
224
  }
256
225
 
257
226
  process.stdout.write("\n\n");
258
227
  messages.push({ role: "assistant", content: response });
228
+
229
+ if (sessionId) {
230
+ try {
231
+ appendEvent(sessionId, "assistant_message", { content: response });
232
+ if (capturedUsage) {
233
+ appendTokenUsage(sessionId, {
234
+ provider,
235
+ model,
236
+ usage: {
237
+ input_tokens: capturedUsage.inputTokens,
238
+ output_tokens: capturedUsage.outputTokens,
239
+ },
240
+ });
241
+ }
242
+ } catch {
243
+ /* best-effort — never break REPL */
244
+ }
245
+ }
259
246
  } catch (err) {
260
247
  process.stdout.write("\n");
261
248
  logger.error(`Error: ${err.message}`);