chainlesschain 0.47.6 → 0.47.8
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 +294 -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(
|
|
@@ -94,9 +101,34 @@ async function agentLoop(messages, options) {
|
|
|
94
101
|
process.stdout.write(
|
|
95
102
|
chalk.red(` Error: ${event.error || event.result?.error}\n`),
|
|
96
103
|
);
|
|
104
|
+
// Parity with Desktop AIChatPage's `Switch to Trusted` button:
|
|
105
|
+
// when the deny came from ApprovalGate (not shell-policy), surface
|
|
106
|
+
// the exact CLI command the user can run to relax the per-session
|
|
107
|
+
// policy. The structured `approval` outcome is attached by
|
|
108
|
+
// `evaluateShellCommandWithApproval` in agent-core.js.
|
|
109
|
+
const approval = event.result?.approval;
|
|
110
|
+
if (approval?.decision === "deny" && approval?.via !== "shell-policy") {
|
|
111
|
+
const sid = options?.sessionId;
|
|
112
|
+
const policy = approval.policy || "strict";
|
|
113
|
+
if (sid && policy === "strict") {
|
|
114
|
+
process.stdout.write(
|
|
115
|
+
chalk.yellow(
|
|
116
|
+
` Hint: relax policy with cc session policy ${sid} --set trusted\n`,
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
} else if (sid) {
|
|
120
|
+
process.stdout.write(
|
|
121
|
+
chalk.yellow(
|
|
122
|
+
` Hint: per-session policy is "${policy}" — see cc session policy ${sid}\n`,
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
97
127
|
} else if (event.result?.success) {
|
|
98
128
|
process.stdout.write(chalk.green(` Done\n`));
|
|
99
129
|
}
|
|
130
|
+
} else if (event.type === "token-usage") {
|
|
131
|
+
usageEvents.push(event);
|
|
100
132
|
} else if (event.type === "iteration-warning") {
|
|
101
133
|
process.stdout.write(chalk.yellow(`\n ${event.message}\n`));
|
|
102
134
|
} else if (event.type === "iteration-budget-exhausted") {
|
|
@@ -104,10 +136,10 @@ async function agentLoop(messages, options) {
|
|
|
104
136
|
chalk.red(`\n [Budget Exhausted] ${event.budget}\n`),
|
|
105
137
|
);
|
|
106
138
|
} else if (event.type === "response-complete") {
|
|
107
|
-
return event.content;
|
|
139
|
+
return { content: event.content, usageEvents };
|
|
108
140
|
}
|
|
109
141
|
}
|
|
110
|
-
return "";
|
|
142
|
+
return { content: "", usageEvents };
|
|
111
143
|
}
|
|
112
144
|
|
|
113
145
|
/**
|
|
@@ -155,6 +187,39 @@ export async function startAgentRepl(options = {}) {
|
|
|
155
187
|
// Set hook DB reference for tool pipeline
|
|
156
188
|
_hookDb = db;
|
|
157
189
|
|
|
190
|
+
// Wire the persistent ApprovalGate singleton (approval-policies.json) with
|
|
191
|
+
// a readline confirm prompt. agent-core's run_shell branch gates
|
|
192
|
+
// MEDIUM/HIGH-risk commands against the session's policy tier
|
|
193
|
+
// (strict / trusted / autopilot).
|
|
194
|
+
try {
|
|
195
|
+
const { getApprovalGate } =
|
|
196
|
+
await import("../lib/session-core-singletons.js");
|
|
197
|
+
_approvalGate = await getApprovalGate();
|
|
198
|
+
if (typeof _approvalGate.setConfirmer === "function") {
|
|
199
|
+
_approvalGate.setConfirmer(async ({ args, riskLevel }) => {
|
|
200
|
+
const rlConfirm = readline.createInterface({
|
|
201
|
+
input: process.stdin,
|
|
202
|
+
output: process.stdout,
|
|
203
|
+
});
|
|
204
|
+
const q = (p) => new Promise((res) => rlConfirm.question(p, res));
|
|
205
|
+
const cmd = args?.command ? ` ${args.command}` : "";
|
|
206
|
+
const ans = (
|
|
207
|
+
await q(
|
|
208
|
+
chalk.yellow(
|
|
209
|
+
`\n[ApprovalGate] ${riskLevel || "medium"} risk command:${cmd}\n Proceed? (y/N) `,
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
.trim()
|
|
214
|
+
.toLowerCase();
|
|
215
|
+
rlConfirm.close();
|
|
216
|
+
return ans === "y" || ans === "yes";
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} catch (_err) {
|
|
220
|
+
_approvalGate = null;
|
|
221
|
+
}
|
|
222
|
+
|
|
158
223
|
// Resume existing session or create new one
|
|
159
224
|
const useJsonl = feature("JSONL_SESSION");
|
|
160
225
|
|
|
@@ -200,10 +265,177 @@ export async function startAgentRepl(options = {}) {
|
|
|
200
265
|
}
|
|
201
266
|
}
|
|
202
267
|
|
|
268
|
+
// Phase H — register this session with session-core SessionManager so
|
|
269
|
+
// `cc session lifecycle / park / unpark / end` can see and control it.
|
|
270
|
+
// Resume a previously parked handle if --session points at one; otherwise
|
|
271
|
+
// create a fresh handle keyed by the JSONL sessionId.
|
|
272
|
+
let _sessionMgr = null;
|
|
273
|
+
let _sessionHandle = null;
|
|
274
|
+
try {
|
|
275
|
+
const { getSessionManager } =
|
|
276
|
+
await import("../lib/session-core-singletons.js");
|
|
277
|
+
_sessionMgr = getSessionManager();
|
|
278
|
+
if (sessionId) {
|
|
279
|
+
if (options.sessionId && !_sessionMgr.has(sessionId)) {
|
|
280
|
+
// Try unparking; no-op if nothing parked with that id
|
|
281
|
+
try {
|
|
282
|
+
await _sessionMgr.resume(sessionId);
|
|
283
|
+
} catch (_e) {
|
|
284
|
+
/* non-critical */
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (!_sessionMgr.has(sessionId)) {
|
|
288
|
+
_sessionHandle = _sessionMgr.create({
|
|
289
|
+
agentId: options.agentId || "cli-agent",
|
|
290
|
+
sessionId,
|
|
291
|
+
metadata: { provider, model },
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
_sessionHandle = _sessionMgr.get(sessionId);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} catch (_err) {
|
|
298
|
+
// Non-critical — SessionManager integration must not block startup
|
|
299
|
+
}
|
|
300
|
+
|
|
203
301
|
const messages = [
|
|
204
302
|
{ role: "system", content: buildSystemPrompt(process.cwd()) },
|
|
205
303
|
];
|
|
206
304
|
|
|
305
|
+
// Deep Agents Deploy Phase 1 — load agent bundle if --bundle provided.
|
|
306
|
+
// Injects AGENTS.md as system prompt, seeds USER.md into MemoryStore,
|
|
307
|
+
// and applies bundle manifest metadata (model/provider override, agentId).
|
|
308
|
+
let _bundleResolved = null;
|
|
309
|
+
let _bundleMcpClient = null;
|
|
310
|
+
if (options.bundlePath) {
|
|
311
|
+
try {
|
|
312
|
+
const { loadBundle } =
|
|
313
|
+
await import("@chainlesschain/session-core/agent-bundle-loader");
|
|
314
|
+
const { resolveBundle } =
|
|
315
|
+
await import("@chainlesschain/session-core/agent-bundle-resolver");
|
|
316
|
+
const { getMemoryStore } =
|
|
317
|
+
await import("../lib/session-core-singletons.js");
|
|
318
|
+
const bundle = loadBundle(options.bundlePath);
|
|
319
|
+
|
|
320
|
+
const memoryStore = getMemoryStore();
|
|
321
|
+
_bundleResolved = resolveBundle(bundle, {
|
|
322
|
+
memoryStore,
|
|
323
|
+
seedOptions: {
|
|
324
|
+
userId: options.agentId || null,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (_bundleResolved.systemPrompt) {
|
|
329
|
+
messages.push({
|
|
330
|
+
role: "system",
|
|
331
|
+
content: _bundleResolved.systemPrompt,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (_bundleResolved.manifest) {
|
|
336
|
+
if (_bundleResolved.manifest.model && !options.model) {
|
|
337
|
+
model = _bundleResolved.manifest.model;
|
|
338
|
+
}
|
|
339
|
+
if (_bundleResolved.manifest.provider && !options.provider) {
|
|
340
|
+
provider = _bundleResolved.manifest.provider;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Connect bundle MCP servers (stdio transport, local mode only).
|
|
345
|
+
const mcpServers = _bundleResolved.mcpConfig?.servers;
|
|
346
|
+
if (mcpServers && typeof mcpServers === "object") {
|
|
347
|
+
const serverEntries = Object.entries(mcpServers).filter(
|
|
348
|
+
([, cfg]) => cfg && cfg.command,
|
|
349
|
+
);
|
|
350
|
+
if (serverEntries.length > 0) {
|
|
351
|
+
try {
|
|
352
|
+
const { MCPClient } = await import("../harness/mcp-client.js");
|
|
353
|
+
_bundleMcpClient = new MCPClient();
|
|
354
|
+
let connected = 0;
|
|
355
|
+
for (const [name, cfg] of serverEntries) {
|
|
356
|
+
try {
|
|
357
|
+
await _bundleMcpClient.connect(name, cfg);
|
|
358
|
+
connected += 1;
|
|
359
|
+
} catch (mcpErr) {
|
|
360
|
+
logger.log(
|
|
361
|
+
chalk.yellow(
|
|
362
|
+
`Bundle MCP: "${name}" connect failed — ${mcpErr.message}`,
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (connected === 0) {
|
|
368
|
+
await _bundleMcpClient.disconnectAll().catch(() => undefined);
|
|
369
|
+
_bundleMcpClient = null;
|
|
370
|
+
}
|
|
371
|
+
} catch (mcpInitErr) {
|
|
372
|
+
logger.log(
|
|
373
|
+
chalk.yellow(`Bundle MCP: init failed — ${mcpInitErr.message}`),
|
|
374
|
+
);
|
|
375
|
+
_bundleMcpClient = null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const seedInfo = _bundleResolved.seedResult;
|
|
381
|
+
const seedMsg =
|
|
382
|
+
seedInfo && seedInfo.seeded > 0
|
|
383
|
+
? `, seeded ${seedInfo.seeded} user memories`
|
|
384
|
+
: "";
|
|
385
|
+
const mcpMsg = _bundleMcpClient
|
|
386
|
+
? `, ${_bundleMcpClient.servers.size} MCP servers`
|
|
387
|
+
: "";
|
|
388
|
+
const warnMsg =
|
|
389
|
+
_bundleResolved.warnings.length > 0
|
|
390
|
+
? ` (${_bundleResolved.warnings.length} warnings)`
|
|
391
|
+
: "";
|
|
392
|
+
logger.log(
|
|
393
|
+
chalk.gray(
|
|
394
|
+
`Bundle: loaded ${_bundleResolved.manifest?.id || path.basename(options.bundlePath)}${seedMsg}${mcpMsg}${warnMsg}`,
|
|
395
|
+
),
|
|
396
|
+
);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
logger.log(chalk.red(`Bundle: failed to load — ${err.message}`));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Apply bundle approval policy to this session (after both gate and sessionId are ready)
|
|
403
|
+
if (_bundleResolved?.approvalPolicy?.default && _approvalGate && sessionId) {
|
|
404
|
+
try {
|
|
405
|
+
_approvalGate.setSessionPolicy(
|
|
406
|
+
sessionId,
|
|
407
|
+
_bundleResolved.approvalPolicy.default,
|
|
408
|
+
);
|
|
409
|
+
} catch (_err) {
|
|
410
|
+
// Non-critical — invalid policy value is silently ignored
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Phase G #5 — inject top-K memory recall into system prompt for new sessions
|
|
415
|
+
// Skip on resume (existing context already reflects prior work) and when
|
|
416
|
+
// --no-recall-memory is passed.
|
|
417
|
+
if (!options.sessionId && options.recallMemory !== false) {
|
|
418
|
+
try {
|
|
419
|
+
const { buildMemoryInjection } =
|
|
420
|
+
await import("../lib/memory-injection.js");
|
|
421
|
+
const injection = buildMemoryInjection({
|
|
422
|
+
agentId: options.agentId || null,
|
|
423
|
+
query: options.recallQuery || "",
|
|
424
|
+
limit: Number(options.recallLimit) || undefined,
|
|
425
|
+
});
|
|
426
|
+
if (injection) {
|
|
427
|
+
messages.push({ role: injection.role, content: injection.content });
|
|
428
|
+
logger.log(
|
|
429
|
+
chalk.gray(
|
|
430
|
+
`Context: recalled ${injection.count} memory entries into system prompt`,
|
|
431
|
+
),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
} catch (_err) {
|
|
435
|
+
// Non-critical — memory recall failure must not block REPL startup
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
207
439
|
// Load resumed session messages
|
|
208
440
|
if (options.sessionId && sessionId) {
|
|
209
441
|
try {
|
|
@@ -1235,7 +1467,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
1235
1467
|
try {
|
|
1236
1468
|
process.stdout.write("\n");
|
|
1237
1469
|
const iterationBudget = new IterationBudget({ owner: sessionId });
|
|
1238
|
-
const response = await agentLoop(messages, {
|
|
1470
|
+
const { content: response, usageEvents } = await agentLoop(messages, {
|
|
1239
1471
|
provider,
|
|
1240
1472
|
model: activeModel,
|
|
1241
1473
|
baseUrl,
|
|
@@ -1245,8 +1477,24 @@ export async function startAgentRepl(options = {}) {
|
|
|
1245
1477
|
sessionId,
|
|
1246
1478
|
cwd: process.cwd(),
|
|
1247
1479
|
prepareCall: defaultPrepareCall,
|
|
1480
|
+
approvalGate: _approvalGate,
|
|
1481
|
+
mcpClient: _bundleMcpClient || undefined,
|
|
1248
1482
|
});
|
|
1249
1483
|
|
|
1484
|
+
if (sessionId && usageEvents?.length) {
|
|
1485
|
+
for (const ue of usageEvents) {
|
|
1486
|
+
try {
|
|
1487
|
+
appendTokenUsage(sessionId, {
|
|
1488
|
+
provider: ue.provider,
|
|
1489
|
+
model: ue.model,
|
|
1490
|
+
usage: ue.usage,
|
|
1491
|
+
});
|
|
1492
|
+
} catch (_e) {
|
|
1493
|
+
/* best-effort */
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1250
1498
|
// Fire AssistantResponse hook with rewrite/suppress support
|
|
1251
1499
|
const responseDirective = await fireAssistantResponse(
|
|
1252
1500
|
_hookDb,
|
|
@@ -1270,8 +1518,18 @@ export async function startAgentRepl(options = {}) {
|
|
|
1270
1518
|
}
|
|
1271
1519
|
|
|
1272
1520
|
if (effectiveResponse) {
|
|
1273
|
-
|
|
1274
|
-
|
|
1521
|
+
// Phase G #2 — route through StreamRouter so REPL / WS / future
|
|
1522
|
+
// streaming providers share one StreamEvent protocol.
|
|
1523
|
+
const { streamAgentResponse } = await import("../lib/agent-stream.js");
|
|
1524
|
+
process.stdout.write("\n");
|
|
1525
|
+
const noStream = options.noStream === true;
|
|
1526
|
+
const streamResult = await streamAgentResponse(effectiveResponse, {
|
|
1527
|
+
noStream,
|
|
1528
|
+
writer: noStream ? null : (chunk) => process.stdout.write(chunk),
|
|
1529
|
+
});
|
|
1530
|
+
if (noStream) process.stdout.write(streamResult.text);
|
|
1531
|
+
process.stdout.write("\n\n");
|
|
1532
|
+
messages.push({ role: "assistant", content: streamResult.text });
|
|
1275
1533
|
} else if (!responseDirective.suppress) {
|
|
1276
1534
|
process.stdout.write("\n");
|
|
1277
1535
|
}
|
|
@@ -1398,6 +1656,36 @@ export async function startAgentRepl(options = {}) {
|
|
|
1398
1656
|
messageCount: messages.length,
|
|
1399
1657
|
});
|
|
1400
1658
|
|
|
1659
|
+
// Phase H — park the SessionManager handle on clean exit so the session
|
|
1660
|
+
// can be resumed later via `cc session unpark <id>`. `--no-park-on-exit`
|
|
1661
|
+
// opts out; a SIGINT path (process-level) will force close instead.
|
|
1662
|
+
if (_sessionMgr && sessionId) {
|
|
1663
|
+
try {
|
|
1664
|
+
if (options.parkOnExit === false) {
|
|
1665
|
+
await _sessionMgr.close(sessionId);
|
|
1666
|
+
} else {
|
|
1667
|
+
_sessionMgr.markIdle(sessionId);
|
|
1668
|
+
await _sessionMgr.park(sessionId);
|
|
1669
|
+
logger.log(
|
|
1670
|
+
chalk.gray(
|
|
1671
|
+
`Session ${sessionId.slice(0, 12)} parked — resume with: cc session unpark ${sessionId}`,
|
|
1672
|
+
),
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
} catch (_e) {
|
|
1676
|
+
// Non-critical — parking failure must not block shutdown
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// Disconnect bundle MCP servers
|
|
1681
|
+
if (_bundleMcpClient) {
|
|
1682
|
+
try {
|
|
1683
|
+
await _bundleMcpClient.disconnectAll();
|
|
1684
|
+
} catch (_e) {
|
|
1685
|
+
// Non-critical
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1401
1689
|
// Shutdown runtime
|
|
1402
1690
|
try {
|
|
1403
1691
|
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}`);
|