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.
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 +294 -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
@@ -502,6 +502,9 @@ export async function executeTool(name, args, context = {}) {
502
502
  externalToolDescriptors: context.externalToolDescriptors || null,
503
503
  externalToolExecutors: context.externalToolExecutors || null,
504
504
  mcpClient: context.mcpClient || null,
505
+ shellPolicyOverrides: context.shellPolicyOverrides || null,
506
+ approvalGate: context.approvalGate || null,
507
+ shellConfirm: context.shellConfirm || null,
505
508
  });
506
509
  } catch (err) {
507
510
  if (hookDb) {
@@ -569,6 +572,8 @@ async function executeToolInner(
569
572
  externalToolExecutors,
570
573
  mcpClient,
571
574
  shellPolicyOverrides,
575
+ approvalGate,
576
+ shellConfirm,
572
577
  },
573
578
  ) {
574
579
  const localToolDescriptor =
@@ -710,19 +715,49 @@ async function executeToolInner(
710
715
  const shellPolicyOpts = shellPolicyOverrides
711
716
  ? { overrideRuleIds: shellPolicyOverrides }
712
717
  : {};
713
- const shellPolicy = evaluateShellCommandPolicy(
714
- args.command,
715
- shellPolicyOpts,
716
- );
717
718
  const override = getRuntimeToolDescriptorByCommand(args.command);
718
- if (!shellPolicy.allowed) {
719
- return attachDescriptor(
720
- {
721
- error: `[Shell Policy] ${shellPolicy.reason}`,
722
- shellCommandPolicy: shellPolicy,
723
- },
724
- override || runtimeDescriptor,
725
- );
719
+ let shellPolicy;
720
+ let approvalOutcome = null;
721
+ if (approvalGate) {
722
+ const { evaluateShellCommandWithApproval } =
723
+ await import("../lib/shell-approval.js");
724
+ const gated = await evaluateShellCommandWithApproval({
725
+ command: args.command,
726
+ sessionId,
727
+ approvalGate,
728
+ shellPolicyOptions: shellPolicyOpts,
729
+ });
730
+ shellPolicy = gated.shellPolicy;
731
+ approvalOutcome = {
732
+ decision: gated.decision,
733
+ via: gated.via,
734
+ riskLevel: gated.riskLevel,
735
+ policy: gated.policy,
736
+ };
737
+ if (!gated.allowed) {
738
+ return attachDescriptor(
739
+ {
740
+ error:
741
+ gated.via === "shell-policy"
742
+ ? `[Shell Policy] ${gated.reason}`
743
+ : `[ApprovalGate] command denied (${gated.via})`,
744
+ shellCommandPolicy: shellPolicy,
745
+ approval: approvalOutcome,
746
+ },
747
+ override || runtimeDescriptor,
748
+ );
749
+ }
750
+ } else {
751
+ shellPolicy = evaluateShellCommandPolicy(args.command, shellPolicyOpts);
752
+ if (!shellPolicy.allowed) {
753
+ return attachDescriptor(
754
+ {
755
+ error: `[Shell Policy] ${shellPolicy.reason}`,
756
+ shellCommandPolicy: shellPolicy,
757
+ },
758
+ override || runtimeDescriptor,
759
+ );
760
+ }
726
761
  }
727
762
 
728
763
  try {
@@ -736,6 +771,7 @@ async function executeToolInner(
736
771
  {
737
772
  stdout: output.substring(0, 30000),
738
773
  shellCommandPolicy: shellPolicy,
774
+ approval: approvalOutcome,
739
775
  },
740
776
  override || runtimeDescriptor,
741
777
  );
@@ -746,6 +782,7 @@ async function executeToolInner(
746
782
  stderr: (err.stderr || "").substring(0, 2000),
747
783
  exitCode: err.status,
748
784
  shellCommandPolicy: shellPolicy,
785
+ approval: approvalOutcome,
749
786
  },
750
787
  override || runtimeDescriptor,
751
788
  );
@@ -1580,7 +1617,14 @@ export async function chatWithTools(rawMessages, options) {
1580
1617
  if (!response.ok) {
1581
1618
  throw new Error(`Ollama error: ${response.status}`);
1582
1619
  }
1583
- return await response.json();
1620
+ const data = await response.json();
1621
+ if (data.prompt_eval_count || data.eval_count) {
1622
+ data.usage = {
1623
+ input_tokens: data.prompt_eval_count || 0,
1624
+ output_tokens: data.eval_count || 0,
1625
+ };
1626
+ }
1627
+ return data;
1584
1628
  }
1585
1629
 
1586
1630
  if (provider === "anthropic") {
@@ -1627,7 +1671,14 @@ export async function chatWithTools(rawMessages, options) {
1627
1671
  }
1628
1672
 
1629
1673
  const data = await response.json();
1630
- return _normalizeAnthropicResponse(data);
1674
+ const normalized = _normalizeAnthropicResponse(data);
1675
+ if (data.usage) {
1676
+ normalized.usage = {
1677
+ input_tokens: data.usage.input_tokens || 0,
1678
+ output_tokens: data.usage.output_tokens || 0,
1679
+ };
1680
+ }
1681
+ return normalized;
1631
1682
  }
1632
1683
 
1633
1684
  // OpenAI-compatible providers
@@ -1696,7 +1747,14 @@ export async function chatWithTools(rawMessages, options) {
1696
1747
  throw new Error("Invalid API response: no choices returned");
1697
1748
  }
1698
1749
  const choice = data.choices[0];
1699
- return { message: choice.message };
1750
+ const out = { message: choice.message };
1751
+ if (data.usage) {
1752
+ out.usage = {
1753
+ input_tokens: data.usage.prompt_tokens || 0,
1754
+ output_tokens: data.usage.completion_tokens || 0,
1755
+ };
1756
+ }
1757
+ return out;
1700
1758
  }
1701
1759
 
1702
1760
  function _normalizeAnthropicResponse(data) {
@@ -1758,6 +1816,8 @@ export async function* agentLoop(messages, options) {
1758
1816
  parentMessages: messages, // pass parent messages for sub-agent auto-condensation
1759
1817
  interaction: options.interaction || null,
1760
1818
  shellPolicyOverrides: options.shellPolicyOverrides || null,
1819
+ approvalGate: options.approvalGate || null,
1820
+ shellConfirm: options.shellConfirm || null,
1761
1821
  };
1762
1822
 
1763
1823
  throwIfAborted(signal);
@@ -1817,6 +1877,17 @@ export async function* agentLoop(messages, options) {
1817
1877
  // real `chatWithTools`.
1818
1878
  const llmCall = options.chatFn || chatWithTools;
1819
1879
 
1880
+ // Phase 5 run bookends — a stable runId lets envelope subscribers correlate
1881
+ // every tool_call / tool_result / message / ended event back to one run.
1882
+ const runId =
1883
+ options.runId ||
1884
+ `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1885
+ yield {
1886
+ type: "run-started",
1887
+ runId,
1888
+ sessionId: options.sessionId || null,
1889
+ };
1890
+
1820
1891
  while (budget.hasRemaining()) {
1821
1892
  budget.consume();
1822
1893
  throwIfAborted(signal);
@@ -1877,8 +1948,18 @@ export async function* agentLoop(messages, options) {
1877
1948
  throwIfAborted(signal);
1878
1949
  const msg = result?.message;
1879
1950
 
1951
+ if (result?.usage) {
1952
+ yield {
1953
+ type: "token-usage",
1954
+ provider: options.provider || "ollama",
1955
+ model: options.model || "unknown",
1956
+ usage: result.usage,
1957
+ };
1958
+ }
1959
+
1880
1960
  if (!msg) {
1881
1961
  yield { type: "response-complete", content: "(No response from LLM)" };
1962
+ yield { type: "run-ended", runId, reason: "no-response" };
1882
1963
  return;
1883
1964
  }
1884
1965
 
@@ -1886,6 +1967,7 @@ export async function* agentLoop(messages, options) {
1886
1967
 
1887
1968
  if (!toolCalls || toolCalls.length === 0) {
1888
1969
  yield { type: "response-complete", content: msg.content || "" };
1970
+ yield { type: "run-ended", runId, reason: "complete" };
1889
1971
  return;
1890
1972
  }
1891
1973
 
@@ -1948,6 +2030,7 @@ export async function* agentLoop(messages, options) {
1948
2030
  type: "response-complete",
1949
2031
  content: `(Iteration budget exhausted — ${budget.toSummary()})`,
1950
2032
  };
2033
+ yield { type: "run-ended", runId, reason: "budget-exhausted" };
1951
2034
  }
1952
2035
 
1953
2036
  // ─── Format helpers ───────────────────────────────────────────────────────
@@ -199,8 +199,15 @@ export class AgentRuntime {
199
199
 
200
200
  async startServer() {
201
201
  const { logger: runtimeLogger } = this.deps;
202
- const { port, maxConnections, timeout, token, allowRemote, project } =
203
- this.policy;
202
+ const {
203
+ port,
204
+ maxConnections,
205
+ timeout,
206
+ token,
207
+ allowRemote,
208
+ project,
209
+ httpPort,
210
+ } = this.policy;
204
211
  let { host } = this.policy;
205
212
 
206
213
  if (Number.isNaN(port) || port < 1 || port > 65535) {
@@ -233,15 +240,80 @@ export class AgentRuntime {
233
240
  logger: runtimeLogger,
234
241
  });
235
242
 
243
+ // Deep Agents Deploy: load agent bundle if --bundle provided.
244
+ // Bundle's AGENTS.md becomes defaultSystemPromptExtension for all new sessions.
245
+ // Bundle's MCP servers are connected to the shared mcpClient.
246
+ let bundleResolved = null;
247
+ let bundleMcpClient = mcpClient;
248
+ if (this.policy.bundlePath) {
249
+ try {
250
+ const { loadBundle } =
251
+ await import("@chainlesschain/session-core/agent-bundle-loader");
252
+ const { resolveBundle } =
253
+ await import("@chainlesschain/session-core/agent-bundle-resolver");
254
+ const bundle = loadBundle(this.policy.bundlePath);
255
+ bundleResolved = resolveBundle(bundle);
256
+
257
+ // Connect bundle MCP servers
258
+ const bundleServers = bundleResolved.mcpConfig?.servers;
259
+ if (bundleServers && typeof bundleServers === "object") {
260
+ const entries = Object.entries(bundleServers).filter(
261
+ ([, cfg]) => cfg && cfg.command,
262
+ );
263
+ if (entries.length > 0) {
264
+ if (!bundleMcpClient) {
265
+ bundleMcpClient = this.deps.createMcpClient();
266
+ }
267
+ for (const [name, cfg] of entries) {
268
+ try {
269
+ await bundleMcpClient.connect(name, cfg);
270
+ } catch (mcpErr) {
271
+ runtimeLogger.log(
272
+ chalk.yellow(
273
+ ` Bundle MCP: "${name}" connect failed — ${mcpErr.message}`,
274
+ ),
275
+ );
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ const bid = bundleResolved.manifest?.id || "unknown";
282
+ runtimeLogger.log(chalk.gray(` Bundle: loaded ${bid}`));
283
+ } catch (bundleErr) {
284
+ runtimeLogger.log(
285
+ chalk.red(` Bundle: failed to load — ${bundleErr.message}`),
286
+ );
287
+ }
288
+ }
289
+
236
290
  const sessionManager = this.deps.createSessionManager({
237
291
  db,
238
292
  defaultProjectRoot: project,
239
293
  config: appConfig,
240
- mcpClient,
294
+ mcpClient: bundleMcpClient,
241
295
  allowedMcpServerNames: DEFAULT_ALLOWED_MCP_SERVER_NAMES,
242
296
  mcpServerRegistry: this.deps.mcpServerRegistry,
297
+ defaultSystemPromptExtension: bundleResolved?.systemPrompt || null,
243
298
  });
244
299
 
300
+ let envelopeBus = null;
301
+ let httpServer = null;
302
+ if (httpPort) {
303
+ if (Number.isNaN(httpPort) || httpPort < 1 || httpPort > 65535) {
304
+ throw new Error("Invalid --http-port. Must be between 1 and 65535.");
305
+ }
306
+ const { createEnvelopeBus, createEnvelopeHttpServer } =
307
+ await import("../gateways/http/envelope-http-server.js");
308
+ envelopeBus = createEnvelopeBus();
309
+ httpServer = createEnvelopeHttpServer({
310
+ bus: envelopeBus,
311
+ port: httpPort,
312
+ host,
313
+ token,
314
+ });
315
+ }
316
+
245
317
  const server = this.deps.createServer({
246
318
  port,
247
319
  host,
@@ -249,6 +321,7 @@ export class AgentRuntime {
249
321
  maxConnections,
250
322
  timeout,
251
323
  sessionManager,
324
+ envelopeBus,
252
325
  });
253
326
 
254
327
  server.on("connection", ({ clientId, ip }) => {
@@ -287,10 +360,24 @@ export class AgentRuntime {
287
360
  runtimeLogger.log(
288
361
  "\n" + chalk.yellow("Shutting down WebSocket server..."),
289
362
  );
363
+ if (
364
+ bundleMcpClient &&
365
+ bundleMcpClient !== mcpClient &&
366
+ typeof bundleMcpClient.disconnectAll === "function"
367
+ ) {
368
+ await bundleMcpClient.disconnectAll().catch(() => undefined);
369
+ }
290
370
  if (mcpClient && typeof mcpClient.disconnectAll === "function") {
291
371
  await mcpClient.disconnectAll().catch(() => undefined);
292
372
  }
293
373
  await server.stop();
374
+ if (httpServer) {
375
+ try {
376
+ await httpServer.stop();
377
+ } catch (_e) {
378
+ // non-critical
379
+ }
380
+ }
294
381
  process.exit(0);
295
382
  };
296
383
 
@@ -299,6 +386,11 @@ export class AgentRuntime {
299
386
 
300
387
  await server.start();
301
388
 
389
+ let hostedHttp = null;
390
+ if (httpServer) {
391
+ hostedHttp = await httpServer.start();
392
+ }
393
+
302
394
  this.emit(RUNTIME_EVENTS.RUNTIME_START, {
303
395
  kind: this.kind,
304
396
  policy: { ...this.policy, host },
@@ -313,11 +405,21 @@ export class AgentRuntime {
313
405
  runtimeLogger.log(chalk.bold(" ChainlessChain WebSocket Server"));
314
406
  runtimeLogger.log("");
315
407
  runtimeLogger.log(` Address: ${chalk.cyan(`ws://${host}:${port}`)}`);
408
+ if (hostedHttp) {
409
+ runtimeLogger.log(
410
+ ` HTTP SSE: ${chalk.cyan(`http://${hostedHttp.host}:${hostedHttp.port}/v1/sessions/:id/events`)}`,
411
+ );
412
+ }
316
413
  runtimeLogger.log(
317
414
  ` Auth: ${token ? chalk.green("enabled") : chalk.yellow("disabled")}`,
318
415
  );
319
416
  runtimeLogger.log(` Sessions: ${chalk.green("enabled")}`);
320
417
  runtimeLogger.log(` Project: ${project}`);
418
+ if (bundleResolved) {
419
+ runtimeLogger.log(
420
+ ` Bundle: ${chalk.green(bundleResolved.manifest?.id || "loaded")}`,
421
+ );
422
+ }
321
423
  runtimeLogger.log(` Max conn: ${maxConnections}`);
322
424
  runtimeLogger.log(` Timeout: ${timeout}ms`);
323
425
  runtimeLogger.log("");
@@ -18,6 +18,14 @@ export function resolveAgentPolicy({
18
18
  ? overrides.apiKey
19
19
  : llm.apiKey || defaults.apiKey || null,
20
20
  sessionId: overrides.sessionId || null,
21
+ agentId: overrides.agentId || null,
22
+ recallMemory:
23
+ overrides.recallMemory === false ? false : overrides.recallMemory,
24
+ recallLimit: overrides.recallLimit,
25
+ recallQuery: overrides.recallQuery,
26
+ noStream: overrides.noStream === true,
27
+ parkOnExit: overrides.parkOnExit === false ? false : true,
28
+ bundlePath: overrides.bundlePath || null,
21
29
  };
22
30
  }
23
31
 
@@ -30,6 +38,8 @@ export function resolveServerPolicy(overrides = {}) {
30
38
  timeout: overrides.timeout,
31
39
  allowRemote: Boolean(overrides.allowRemote),
32
40
  project: overrides.project || process.cwd(),
41
+ httpPort: overrides.httpPort || null,
42
+ bundlePath: overrides.bundlePath || null,
33
43
  };
34
44
  }
35
45
 
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: video-editing
3
+ description: 长视频素材 + 音乐 → 节奏化蒙太奇剪辑(借鉴 GVCLab/CutClaw)
4
+ version: 0.1.0
5
+ category: media
6
+ source: bundled
7
+ tools:
8
+ - video_semantic_retrieval
9
+ - video_shot_trimming
10
+ - video_review_clip
11
+ - video_commit_clip
12
+ model-hints:
13
+ vision: claude-opus-4-6
14
+ reasoning: claude-opus-4-6
15
+ asr: openai/whisper-1
16
+ ---
17
+
18
+ ## 指南
19
+
20
+ 将长视频素材按音乐节奏剪成短蒙太奇。完整流程分四阶段,由 `pipeline.js` 自动编排:
21
+
22
+ 1. **解构(deconstruct)**: 抽帧 → VLM caption;音频 ASR + 段落分析。结果缓存到 `~/.chainlesschain/video-editing/<sha256>/`
23
+ 2. **规划(plan)**: Screenwriter 根据用户指令 + 音乐段落产出 `shot_plan.json`(音乐段 → 镜头骨架)
24
+ 3. **组装(assemble)**: Editor ReAct 循环(4 个工具)选时间戳 → `shot_point.json`
25
+ 4. **渲染(render)**: ffmpeg 抽片段 + concat + 混音
26
+
27
+ ## 工具
28
+
29
+ - `video_semantic_retrieval(scene_range)` — 在 scene 索引中拉候选镜头
30
+ - `video_shot_trimming(time_range)` — 帧级断点 + 可用性评估
31
+ - `video_review_clip(start, end)` — 与已选片段做时间区间冲突检测
32
+ - `video_commit_clip(clips[])` — 提交,最多 3 段拼接成一个镜头
33
+
34
+ ## 示例
35
+
36
+ ```bash
37
+ cc video edit \
38
+ --video resource/raw.mp4 \
39
+ --audio resource/bgm.mp3 \
40
+ --instruction "节奏感强的角色蒙太奇"
41
+ ```
42
+
43
+ ## 设计参考
44
+
45
+ - 设计文档: `docs/design/modules/93_CutClaw借鉴_视频剪辑Agent.md`
46
+ - 上游灵感: https://github.com/GVCLab/CutClaw
@@ -0,0 +1,127 @@
1
+ /**
2
+ * beat-snap.js — Snap shot_plan timestamps to nearest beat positions
3
+ *
4
+ * CutClaw 对齐逻辑:每个 shot 的 start/end 对齐到最近的 beat,
5
+ * 保证剪辑点落在节拍上,产生音画同步感。
6
+ */
7
+
8
+ export function snapToBeats(shotPlan, beats, options = {}) {
9
+ const tolerance = options.tolerance ?? 0.5;
10
+ if (!beats || beats.length === 0) return shotPlan;
11
+
12
+ const snapped = {
13
+ ...shotPlan,
14
+ sections: (shotPlan.sections || []).map((section) => ({
15
+ ...section,
16
+ music_segment: section.music_segment
17
+ ? snapSegment(section.music_segment, beats, tolerance)
18
+ : section.music_segment,
19
+ shots: (section.shots || []).map((shot) =>
20
+ snapShot(shot, beats, tolerance),
21
+ ),
22
+ })),
23
+ };
24
+
25
+ return snapped;
26
+ }
27
+
28
+ function snapSegment(seg, beats, tolerance) {
29
+ return {
30
+ ...seg,
31
+ start: findNearestBeat(seg.start, beats, tolerance),
32
+ end: findNearestBeat(seg.end, beats, tolerance),
33
+ };
34
+ }
35
+
36
+ function snapShot(shot, beats, tolerance) {
37
+ if (shot.target_duration == null) return shot;
38
+ const snappedDuration = snapDurationToBeats(
39
+ shot.target_duration,
40
+ beats,
41
+ tolerance,
42
+ );
43
+ return { ...shot, target_duration: snappedDuration };
44
+ }
45
+
46
+ export function findNearestBeat(time, beats, tolerance = 0.5) {
47
+ if (!beats || beats.length === 0) return time;
48
+
49
+ let best = beats[0];
50
+ let bestDist = Math.abs(time - beats[0]);
51
+
52
+ for (let i = 1; i < beats.length; i++) {
53
+ const dist = Math.abs(time - beats[i]);
54
+ if (dist < bestDist) {
55
+ best = beats[i];
56
+ bestDist = dist;
57
+ }
58
+ if (beats[i] > time + tolerance) break;
59
+ }
60
+
61
+ return bestDist <= tolerance ? best : time;
62
+ }
63
+
64
+ export function snapDurationToBeats(duration, beats, tolerance = 0.5) {
65
+ if (!beats || beats.length < 2) return duration;
66
+
67
+ const avgInterval = (beats[beats.length - 1] - beats[0]) / (beats.length - 1);
68
+ if (avgInterval <= 0) return duration;
69
+
70
+ const beatCount = Math.round(duration / avgInterval);
71
+ const snapped = beatCount * avgInterval;
72
+
73
+ return Math.abs(snapped - duration) <= tolerance
74
+ ? parseFloat(snapped.toFixed(3))
75
+ : duration;
76
+ }
77
+
78
+ export function buildBeatGrid(beats, downbeats) {
79
+ if (!beats || beats.length === 0) return { bars: [], beatsPerBar: 4 };
80
+
81
+ const dbSet = new Set((downbeats || []).map((d) => d.toString()));
82
+ let beatsPerBar = 4;
83
+ if (downbeats && downbeats.length >= 2) {
84
+ const dbIntervals = [];
85
+ for (let i = 1; i < downbeats.length; i++) {
86
+ const count = beats.filter(
87
+ (b) => b >= downbeats[i - 1] && b < downbeats[i],
88
+ ).length;
89
+ if (count > 0) dbIntervals.push(count);
90
+ }
91
+ if (dbIntervals.length > 0) {
92
+ beatsPerBar = median(dbIntervals);
93
+ }
94
+ }
95
+
96
+ const bars = [];
97
+ let barStart = 0;
98
+ let barBeats = [];
99
+
100
+ for (const b of beats) {
101
+ if (dbSet.has(b.toString()) && barBeats.length > 0) {
102
+ bars.push({ start: barStart, end: b, beats: barBeats });
103
+ barStart = b;
104
+ barBeats = [b];
105
+ } else {
106
+ if (barBeats.length === 0) barStart = b;
107
+ barBeats.push(b);
108
+ }
109
+ }
110
+ if (barBeats.length > 0) {
111
+ bars.push({
112
+ start: barStart,
113
+ end: barBeats[barBeats.length - 1],
114
+ beats: barBeats,
115
+ });
116
+ }
117
+
118
+ return { bars, beatsPerBar };
119
+ }
120
+
121
+ function median(arr) {
122
+ const sorted = [...arr].sort((a, b) => a - b);
123
+ const mid = Math.floor(sorted.length / 2);
124
+ return sorted.length % 2 === 0
125
+ ? Math.round((sorted[mid - 1] + sorted[mid]) / 2)
126
+ : sorted[mid];
127
+ }