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
|
@@ -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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
203
|
-
|
|
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
|
+
}
|