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.
- package/package.json +2 -2
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/Analytics-BFI7jbwM.css +1 -0
- package/src/assets/web-panel/assets/Analytics-DQ135mAd.js +3 -0
- package/src/assets/web-panel/assets/AppLayout-6SPt_8Y_.js +1 -0
- package/src/assets/web-panel/assets/AppLayout-BFJ-Fofn.css +1 -0
- package/src/assets/web-panel/assets/{Backup-Ba9UybpT.js → Backup-DbVRG5vE.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BwXskT21.js → Chat-wVhrFK9C.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-UmOe7qvE.js → Cowork-lOC25IW2.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-JHS-rc-4.js → Cron-3P0eVLTV.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-B95cMCO7.js → Dashboard-Br7kCwKJ.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CSYO0_zk.js → Git-CrDCcBig.js} +2 -2
- package/src/assets/web-panel/assets/{Logs-Hxw_K0km.js → Logs-BfTE8urP.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DIE75TrB.js → McpTools-CsGIijNe.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-C4KVnLlp.js → Memory-BXX_yMKJ.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DuzrHMAk.js → Notes-DU6Vf2cL.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DTq6uF82.js → Organization-Bny6yOPV.js} +4 -4
- package/src/assets/web-panel/assets/{P2P-C0hjlhsR.js → P2P-BxFZ1Bit.js} +2 -2
- package/src/assets/web-panel/assets/{Permissions-Ec0NH-xC.js → Permissions-B1j3Mtms.js} +3 -3
- package/src/assets/web-panel/assets/{Projects-U8D0asCS.js → Projects-D-CGscDu.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BngtTLvJ.js → Providers-r6NaBYMf.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B9NbwCKM.js → RssFeed-D7b68C5q.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BL5Rkr1T.js → Security-MJfKv0EJ.js} +3 -3
- package/src/assets/web-panel/assets/{Services-D4MJzLld.js → Services-Yb_Q1V3d.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CQTOMDwF.js → Skills-DLTHcH5T.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-DepbJMnL.js → Tasks-CqycpPjS.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-C24PVZPu.js → Templates-y01u2Zis.js} +1 -1
- package/src/assets/web-panel/assets/VideoEditing-BA1N-5kq.css +1 -0
- package/src/assets/web-panel/assets/VideoEditing-B_nPKw6B.js +1 -0
- package/src/assets/web-panel/assets/{Wallet-PQoSpN_P.js → Wallet-CsRgnjJY.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-BcuyQ4Lr.js → WebAuthn-DWoR5ADp.js} +1 -1
- package/src/assets/web-panel/assets/{WorkflowEditor-C-SvXbHW.js → WorkflowEditor-DBJhFPMN.js} +1 -1
- package/src/assets/web-panel/assets/{antd-DEjZPGMj.js → antd-Dh2t0vGq.js} +84 -84
- package/src/assets/web-panel/assets/index-tN-8TosE.js +2 -0
- package/src/assets/web-panel/assets/{markdown-CusdXFxb.js → markdown-CBnGGMzE.js} +1 -1
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/agent.js +20 -0
- package/src/commands/mcp.js +86 -4
- package/src/commands/memory.js +85 -4
- package/src/commands/sandbox.js +80 -6
- package/src/commands/serve.js +10 -0
- package/src/commands/session.js +250 -0
- package/src/commands/stream.js +75 -0
- package/src/commands/video.js +363 -0
- package/src/gateways/http/envelope-http-server.js +194 -0
- package/src/gateways/ws/message-dispatcher.js +123 -0
- package/src/gateways/ws/session-core-protocol.js +427 -0
- package/src/gateways/ws/session-protocol.js +42 -1
- package/src/gateways/ws/video-protocol.js +230 -0
- package/src/gateways/ws/ws-server.js +72 -0
- package/src/gateways/ws/ws-session-gateway.js +7 -3
- package/src/harness/jsonl-session-store.js +17 -9
- package/src/index.js +8 -0
- package/src/lib/agent-stream.js +63 -0
- package/src/lib/chat-core.js +183 -6
- package/src/lib/cowork/ab-comparator-cli.js +44 -23
- package/src/lib/cowork/agent-group-runner.js +145 -0
- package/src/lib/cowork/debate-review-cli.js +47 -25
- package/src/lib/cowork/project-style-analyzer-cli.js +34 -7
- package/src/lib/interaction-adapter.js +59 -1
- package/src/lib/jsonl-session-store.js +2 -0
- package/src/lib/memory-injection.js +90 -0
- package/src/lib/provider-stream.js +120 -0
- package/src/lib/sandbox-v2.js +198 -3
- package/src/lib/session-consolidator.js +125 -0
- package/src/lib/session-core-singletons.js +56 -0
- package/src/lib/session-tail.js +128 -0
- package/src/lib/session-usage.js +166 -0
- package/src/lib/shell-approval.js +96 -0
- package/src/lib/ws-chat-handler.js +3 -0
- package/src/repl/agent-repl.js +271 -6
- package/src/repl/chat-repl.js +87 -100
- package/src/runtime/agent-core.js +98 -15
- package/src/runtime/agent-runtime.js +105 -3
- package/src/runtime/policies/agent-policy.js +10 -0
- package/src/skills/video-editing/SKILL.md +46 -0
- package/src/skills/video-editing/beat-snap.js +127 -0
- package/src/skills/video-editing/extractors/audio-extractor.js +212 -0
- package/src/skills/video-editing/extractors/subtitle-extractor.js +90 -0
- package/src/skills/video-editing/extractors/video-extractor.js +137 -0
- package/src/skills/video-editing/parallel-orchestrator.js +212 -0
- package/src/skills/video-editing/pipeline.js +480 -0
- package/src/skills/video-editing/prompts/aesthetic-analysis.md +21 -0
- package/src/skills/video-editing/prompts/audio-segment.md +15 -0
- package/src/skills/video-editing/prompts/character-identify.md +19 -0
- package/src/skills/video-editing/prompts/dense-caption.md +20 -0
- package/src/skills/video-editing/prompts/editor-system.md +29 -0
- package/src/skills/video-editing/prompts/hook-dialogue.md +17 -0
- package/src/skills/video-editing/prompts/protagonist-detect.md +20 -0
- package/src/skills/video-editing/prompts/scene-caption.md +16 -0
- package/src/skills/video-editing/prompts/shot-caption.md +25 -0
- package/src/skills/video-editing/prompts/shot-plan.md +28 -0
- package/src/skills/video-editing/prompts/structure-proposal.md +16 -0
- package/src/skills/video-editing/prompts/vlog-scene-caption.md +18 -0
- package/src/skills/video-editing/render/audio-mix.js +128 -0
- package/src/skills/video-editing/render/ffmpeg-concat.js +45 -0
- package/src/skills/video-editing/render/ffmpeg-extract.js +67 -0
- package/src/skills/video-editing/reviewer.js +161 -0
- package/src/skills/video-editing/tools/commit.js +108 -0
- package/src/skills/video-editing/tools/review-clip.js +46 -0
- package/src/skills/video-editing/tools/semantic-retrieval.js +56 -0
- package/src/skills/video-editing/tools/shot-trimming.js +73 -0
- package/src/assets/web-panel/assets/Analytics-B4OM8S8X.css +0 -1
- package/src/assets/web-panel/assets/Analytics-DgypYeUB.js +0 -3
- package/src/assets/web-panel/assets/AppLayout-Bzf3mSZI.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-DQyDwGut.css +0 -1
- package/src/assets/web-panel/assets/index-CwvzTTw_.js +0 -2
package/src/repl/agent-repl.js
CHANGED
|
@@ -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, {
|
|
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
|
-
|
|
1274
|
-
|
|
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();
|
package/src/repl/chat-repl.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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}`);
|