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/commands/session.js
CHANGED
|
@@ -450,6 +450,256 @@ export function registerSessionCommand(program) {
|
|
|
450
450
|
}
|
|
451
451
|
});
|
|
452
452
|
|
|
453
|
+
// ---------------------------------------------------------------
|
|
454
|
+
// session-core SessionManager lifecycle (Phase H — CLI parity)
|
|
455
|
+
// Parked sessions persist to ~/.chainlesschain/parked-sessions.json
|
|
456
|
+
// ---------------------------------------------------------------
|
|
457
|
+
session
|
|
458
|
+
.command("lifecycle")
|
|
459
|
+
.description("List session-core handles (running/idle/parked)")
|
|
460
|
+
.option("--status <s>", "Filter by status: running | idle | parked")
|
|
461
|
+
.option("--agent <id>", "Filter by agent id")
|
|
462
|
+
.option("--json", "Output as JSON")
|
|
463
|
+
.action(async (options) => {
|
|
464
|
+
try {
|
|
465
|
+
const { getSessionManager } =
|
|
466
|
+
await import("../lib/session-core-singletons.js");
|
|
467
|
+
const mgr = getSessionManager();
|
|
468
|
+
const live = mgr.list({
|
|
469
|
+
agentId: options.agent,
|
|
470
|
+
status: options.status,
|
|
471
|
+
});
|
|
472
|
+
let parked = [];
|
|
473
|
+
if (mgr._parkedStore) {
|
|
474
|
+
const all = await mgr._parkedStore.list();
|
|
475
|
+
parked = all
|
|
476
|
+
.filter((s) => !options.agent || s.agentId === options.agent)
|
|
477
|
+
.filter((s) => !options.status || s.status === options.status);
|
|
478
|
+
}
|
|
479
|
+
const merged = [
|
|
480
|
+
...live.map((h) => h.toJSON()),
|
|
481
|
+
...parked.filter(
|
|
482
|
+
(p) => !live.some((h) => h.sessionId === p.sessionId),
|
|
483
|
+
),
|
|
484
|
+
];
|
|
485
|
+
if (options.json) {
|
|
486
|
+
console.log(JSON.stringify(merged, null, 2));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (merged.length === 0) {
|
|
490
|
+
logger.info("No session-core handles");
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
for (const h of merged) {
|
|
494
|
+
logger.log(
|
|
495
|
+
`${chalk.gray(h.sessionId.slice(0, 12))} ${chalk.cyan(h.status.padEnd(7))} agent=${h.agentId || "-"}`,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
} catch (err) {
|
|
499
|
+
logger.error(`Failed: ${err.message}`);
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
session
|
|
505
|
+
.command("park")
|
|
506
|
+
.description("Park a session (persist to disk, free process memory)")
|
|
507
|
+
.argument("<id>", "Session ID")
|
|
508
|
+
.action(async (id) => {
|
|
509
|
+
try {
|
|
510
|
+
const { getSessionManager } =
|
|
511
|
+
await import("../lib/session-core-singletons.js");
|
|
512
|
+
const mgr = getSessionManager();
|
|
513
|
+
if (!mgr.has(id)) {
|
|
514
|
+
logger.error(`Session ${id} is not active in this process`);
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
mgr.markIdle(id);
|
|
518
|
+
const ok = await mgr.park(id);
|
|
519
|
+
if (!ok) {
|
|
520
|
+
logger.error(`Failed to park ${id}`);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
logger.success(`Session ${chalk.gray(id.slice(0, 12))} parked`);
|
|
524
|
+
} catch (err) {
|
|
525
|
+
logger.error(`Failed: ${err.message}`);
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
session
|
|
531
|
+
.command("unpark")
|
|
532
|
+
.description("Resume a parked session")
|
|
533
|
+
.argument("<id>", "Session ID")
|
|
534
|
+
.action(async (id) => {
|
|
535
|
+
try {
|
|
536
|
+
const { getSessionManager } =
|
|
537
|
+
await import("../lib/session-core-singletons.js");
|
|
538
|
+
const mgr = getSessionManager();
|
|
539
|
+
const ok = await mgr.resume(id);
|
|
540
|
+
if (!ok) {
|
|
541
|
+
logger.error(`No parked session ${id}`);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
logger.success(`Session ${chalk.gray(id.slice(0, 12))} resumed`);
|
|
545
|
+
} catch (err) {
|
|
546
|
+
logger.error(`Failed: ${err.message}`);
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
session
|
|
552
|
+
.command("end")
|
|
553
|
+
.description(
|
|
554
|
+
"Close a session (optionally consolidate trace into MemoryStore)",
|
|
555
|
+
)
|
|
556
|
+
.argument("<id>", "Session ID")
|
|
557
|
+
.option(
|
|
558
|
+
"--consolidate",
|
|
559
|
+
"Consolidate JSONL trace into MemoryStore before closing",
|
|
560
|
+
)
|
|
561
|
+
.option("--scope <s>", "Memory scope for consolidation", "session")
|
|
562
|
+
.option("--scope-id <id>", "Scope id (defaults to session id)")
|
|
563
|
+
.option("--agent-id <id>", "Agent id for scope=agent")
|
|
564
|
+
.action(async (id, options) => {
|
|
565
|
+
try {
|
|
566
|
+
const { getSessionManager } =
|
|
567
|
+
await import("../lib/session-core-singletons.js");
|
|
568
|
+
if (options.consolidate) {
|
|
569
|
+
try {
|
|
570
|
+
const { consolidateJsonlSession } =
|
|
571
|
+
await import("../lib/session-consolidator.js");
|
|
572
|
+
const res = await consolidateJsonlSession(id, {
|
|
573
|
+
scope: options.scope,
|
|
574
|
+
scopeId: options.scopeId || null,
|
|
575
|
+
agentId: options.agentId || null,
|
|
576
|
+
});
|
|
577
|
+
await new Promise((r) => setImmediate(r));
|
|
578
|
+
logger.info(
|
|
579
|
+
`Consolidated ${res.writtenCount} memory entries from session trace`,
|
|
580
|
+
);
|
|
581
|
+
} catch (e) {
|
|
582
|
+
logger.warn(`Consolidation skipped: ${e.message}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const mgr = getSessionManager();
|
|
586
|
+
const ok = await mgr.close(id);
|
|
587
|
+
if (!ok && mgr._parkedStore) {
|
|
588
|
+
await mgr._parkedStore.remove(id);
|
|
589
|
+
}
|
|
590
|
+
logger.success(`Session ${chalk.gray(id.slice(0, 12))} closed`);
|
|
591
|
+
} catch (err) {
|
|
592
|
+
logger.error(`Failed: ${err.message}`);
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// ---------------------------------------------------------------
|
|
598
|
+
// session tail <id> — Phase I: follow JSONL events as NDJSON
|
|
599
|
+
// ---------------------------------------------------------------
|
|
600
|
+
session
|
|
601
|
+
.command("tail")
|
|
602
|
+
.description("Follow a JSONL session's events (NDJSON on stdout)")
|
|
603
|
+
.argument("<id>", "Session ID")
|
|
604
|
+
.option("--from-start", "Start from the first event (default: EOF)")
|
|
605
|
+
.option("--from-offset <n>", "Start from explicit byte offset")
|
|
606
|
+
.option("-t, --type <types>", "Comma-separated event types to include")
|
|
607
|
+
.option("--since <ms>", "Only events with timestamp >= ms")
|
|
608
|
+
.option("--once", "Drain current tail and exit (no follow)")
|
|
609
|
+
.option("--poll <ms>", "Poll interval", "200")
|
|
610
|
+
.action(async (id, options) => {
|
|
611
|
+
try {
|
|
612
|
+
const { followSession } = await import("../lib/session-tail.js");
|
|
613
|
+
const controller = new AbortController();
|
|
614
|
+
const onSig = () => controller.abort();
|
|
615
|
+
process.once("SIGINT", onSig);
|
|
616
|
+
process.once("SIGTERM", onSig);
|
|
617
|
+
const types = options.type
|
|
618
|
+
? options.type
|
|
619
|
+
.split(",")
|
|
620
|
+
.map((s) => s.trim())
|
|
621
|
+
.filter(Boolean)
|
|
622
|
+
: null;
|
|
623
|
+
const iter = followSession(id, {
|
|
624
|
+
signal: controller.signal,
|
|
625
|
+
pollMs: parseInt(options.poll, 10) || 200,
|
|
626
|
+
fromStart: Boolean(options.fromStart),
|
|
627
|
+
fromOffset:
|
|
628
|
+
options.fromOffset !== undefined
|
|
629
|
+
? parseInt(options.fromOffset, 10)
|
|
630
|
+
: undefined,
|
|
631
|
+
types,
|
|
632
|
+
sinceMs: options.since ? parseInt(options.since, 10) : null,
|
|
633
|
+
once: Boolean(options.once),
|
|
634
|
+
});
|
|
635
|
+
for await (const { event } of iter) {
|
|
636
|
+
process.stdout.write(JSON.stringify(event) + "\n");
|
|
637
|
+
}
|
|
638
|
+
} catch (err) {
|
|
639
|
+
logger.error(`Failed: ${err.message}`);
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// ---------------------------------------------------------------
|
|
645
|
+
// session usage [id] — Phase I: aggregate token usage
|
|
646
|
+
// ---------------------------------------------------------------
|
|
647
|
+
session
|
|
648
|
+
.command("usage")
|
|
649
|
+
.description("Aggregate token usage (per-session or global)")
|
|
650
|
+
.argument("[id]", "Session ID (omit for global rollup)")
|
|
651
|
+
.option("--json", "Output as JSON")
|
|
652
|
+
.option("--limit <n>", "Max sessions for global rollup", "1000")
|
|
653
|
+
.action(async (id, options) => {
|
|
654
|
+
try {
|
|
655
|
+
const { sessionUsage, allSessionsUsage } =
|
|
656
|
+
await import("../lib/session-usage.js");
|
|
657
|
+
const result = id
|
|
658
|
+
? sessionUsage(id)
|
|
659
|
+
: allSessionsUsage({ limit: parseInt(options.limit, 10) || 1000 });
|
|
660
|
+
|
|
661
|
+
if (options.json) {
|
|
662
|
+
console.log(JSON.stringify(result, null, 2));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (id) {
|
|
667
|
+
logger.log(chalk.bold(`Session ${chalk.gray(id.slice(0, 16))}`));
|
|
668
|
+
const t = result.total;
|
|
669
|
+
logger.log(
|
|
670
|
+
` total: ${chalk.cyan(t.totalTokens.toLocaleString())} tokens in=${t.inputTokens} out=${t.outputTokens} calls=${t.calls}`,
|
|
671
|
+
);
|
|
672
|
+
if (result.byModel.length === 0) {
|
|
673
|
+
logger.log(chalk.gray(" (no token_usage events recorded)"));
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
for (const row of result.byModel) {
|
|
677
|
+
logger.log(
|
|
678
|
+
` ${chalk.gray((row.provider || "?").padEnd(10))} ${chalk.white((row.model || "?").padEnd(24))} in=${row.inputTokens} out=${row.outputTokens} calls=${row.calls}`,
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
const t = result.total;
|
|
683
|
+
logger.log(chalk.bold("Global usage"));
|
|
684
|
+
logger.log(
|
|
685
|
+
` total: ${chalk.cyan(t.totalTokens.toLocaleString())} tokens in=${t.inputTokens} out=${t.outputTokens} calls=${t.calls} sessions=${result.sessions.length}`,
|
|
686
|
+
);
|
|
687
|
+
if (result.byModel.length === 0) {
|
|
688
|
+
logger.log(chalk.gray(" (no token_usage events recorded)"));
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
for (const row of result.byModel) {
|
|
692
|
+
logger.log(
|
|
693
|
+
` ${chalk.gray((row.provider || "?").padEnd(10))} ${chalk.white((row.model || "?").padEnd(24))} in=${row.inputTokens} out=${row.outputTokens} calls=${row.calls}`,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
} catch (err) {
|
|
698
|
+
logger.error(`Failed: ${err.message}`);
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
453
703
|
// session workflow — inspect canonical coding workflow state
|
|
454
704
|
// Reads .chainlesschain/sessions/<id>/{intent.md,plan.md,progress.log,mode.json}
|
|
455
705
|
// written by the 4 workflow skills ($deep-interview/$ralplan/$ralph/$team).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chainlesschain stream "<prompt>" — scriptable StreamRouter demo.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Desktop's `agent:stream:start` IPC: feed a provider stream through
|
|
5
|
+
* session-core StreamRouter and emit StreamEvent objects as NDJSON on stdout
|
|
6
|
+
* so downstream scripts can consume them line-by-line.
|
|
7
|
+
*
|
|
8
|
+
* Managed Agents parity Phase H — CLI symmetric entry.
|
|
9
|
+
* Provider adapters live in `../lib/provider-stream.js` so the WS
|
|
10
|
+
* `stream.run` route can reuse them (Phase I).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { logger } from "../lib/logger.js";
|
|
14
|
+
import { loadConfig } from "../lib/config-manager.js";
|
|
15
|
+
import { buildProviderSource } from "../lib/provider-stream.js";
|
|
16
|
+
|
|
17
|
+
export function registerStreamCommand(program) {
|
|
18
|
+
program
|
|
19
|
+
.command("stream")
|
|
20
|
+
.description(
|
|
21
|
+
"Stream a single prompt through session-core StreamRouter (NDJSON on stdout)",
|
|
22
|
+
)
|
|
23
|
+
.argument("<prompt>", "The prompt to stream")
|
|
24
|
+
.option("--model <model>", "Model name")
|
|
25
|
+
.option("--provider <provider>", "LLM provider", "ollama")
|
|
26
|
+
.option("--base-url <url>", "API base URL")
|
|
27
|
+
.option("--api-key <key>", "API key")
|
|
28
|
+
.option("--text", "Emit concatenated final text instead of NDJSON events")
|
|
29
|
+
.action(async (prompt, options) => {
|
|
30
|
+
const config = loadConfig();
|
|
31
|
+
const provider = options.provider || config.llm?.provider || "ollama";
|
|
32
|
+
const baseUrl = options.baseUrl || config.llm?.baseUrl;
|
|
33
|
+
const apiKey = options.apiKey || config.llm?.apiKey;
|
|
34
|
+
const model = options.model || config.llm?.model;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const { createStreamRouter } =
|
|
38
|
+
await import("../lib/session-core-singletons.js");
|
|
39
|
+
const router = createStreamRouter();
|
|
40
|
+
const source = buildProviderSource(provider, {
|
|
41
|
+
model,
|
|
42
|
+
baseUrl,
|
|
43
|
+
apiKey,
|
|
44
|
+
prompt,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (options.text) {
|
|
48
|
+
const out = await router.collect(source);
|
|
49
|
+
if (out.errored) {
|
|
50
|
+
logger.error(`Stream errored: ${out.error?.message || out.error}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
process.stdout.write(out.text);
|
|
54
|
+
if (!out.text.endsWith("\n")) process.stdout.write("\n");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for await (const ev of router.stream(source)) {
|
|
59
|
+
process.stdout.write(`${JSON.stringify(ev)}\n`);
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const ev = {
|
|
63
|
+
type: "error",
|
|
64
|
+
error: err.message,
|
|
65
|
+
ts: Date.now(),
|
|
66
|
+
};
|
|
67
|
+
if (options.text) {
|
|
68
|
+
logger.error(`Failed: ${err.message}`);
|
|
69
|
+
} else {
|
|
70
|
+
process.stdout.write(`${JSON.stringify(ev)}\n`);
|
|
71
|
+
}
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cc video — 视频剪辑 Agent (借鉴 CutClaw)
|
|
3
|
+
*
|
|
4
|
+
* 子命令:
|
|
5
|
+
* edit 一键完整流程
|
|
6
|
+
* deconstruct 解构素材(抽帧+ASR+beat)
|
|
7
|
+
* plan 生成 shot_plan
|
|
8
|
+
* assemble Editor ReAct 选时间戳
|
|
9
|
+
* render ffmpeg 渲染成片
|
|
10
|
+
* assets 管理已解构素材缓存
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { logger } from "../lib/logger.js";
|
|
14
|
+
|
|
15
|
+
export function registerVideoCommand(program) {
|
|
16
|
+
const video = program
|
|
17
|
+
.command("video")
|
|
18
|
+
.description(
|
|
19
|
+
"Video editing agent — long footage + music → montage (CutClaw-inspired)",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// ── cc video edit ─────────────────────────────────────────
|
|
23
|
+
video
|
|
24
|
+
.command("edit")
|
|
25
|
+
.description("Full pipeline: deconstruct → plan → assemble → render")
|
|
26
|
+
.requiredOption("--video <path>", "Input video file")
|
|
27
|
+
.option("--audio <path>", "Background music file")
|
|
28
|
+
.option("--instruction <text>", "Editing instruction", "")
|
|
29
|
+
.option("--output <path>", "Output video path", "./output.mp4")
|
|
30
|
+
.option("--srt <path>", "Existing subtitle file (skip ASR)")
|
|
31
|
+
.option("--fps <n>", "Frame sampling FPS", "2")
|
|
32
|
+
.option("--character <name>", "Main character name")
|
|
33
|
+
.option("--parallel", "Run sections in parallel with conflict resolution")
|
|
34
|
+
.option("--concurrency <n>", "Max parallel sections", "4")
|
|
35
|
+
.option("--review", "Enable quality gate (VLM review before commit)")
|
|
36
|
+
.option("--use-madmom", "Use madmom Python sidecar for beat detection")
|
|
37
|
+
.option("--snap-beats", "Snap shot plan timestamps to nearest beat")
|
|
38
|
+
.option("--ducking", "Enable dialogue ducking in audio mix")
|
|
39
|
+
.option("--stream", "Emit NDJSON progress events to stdout")
|
|
40
|
+
.option("--json", "JSON final output")
|
|
41
|
+
.action(async (options) => {
|
|
42
|
+
const { VideoPipeline } =
|
|
43
|
+
await import("../skills/video-editing/pipeline.js");
|
|
44
|
+
|
|
45
|
+
const pipeline = new VideoPipeline({
|
|
46
|
+
videoPath: options.video,
|
|
47
|
+
audioPath: options.audio,
|
|
48
|
+
instruction: options.instruction,
|
|
49
|
+
outputPath: options.output,
|
|
50
|
+
existingSrt: options.srt,
|
|
51
|
+
fps: parseInt(options.fps, 10),
|
|
52
|
+
mainCharacter: options.character,
|
|
53
|
+
useMadmom: !!options.useMadmom,
|
|
54
|
+
snapBeats: !!options.snapBeats,
|
|
55
|
+
ducking: !!options.ducking,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (options.stream) {
|
|
59
|
+
pipeline.on("event", (ev) => {
|
|
60
|
+
process.stdout.write(`${JSON.stringify(ev)}\n`);
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
pipeline.on("event", (ev) => {
|
|
64
|
+
if (ev.type === "phase.start") logger.info(`▶ ${ev.phase}`);
|
|
65
|
+
if (ev.type === "phase.progress")
|
|
66
|
+
logger.info(` ${ev.pct * 100}% ${ev.message || ""}`);
|
|
67
|
+
if (ev.type === "phase.end") logger.info(`✓ ${ev.phase} done`);
|
|
68
|
+
if (ev.type === "error") logger.error(`✗ ${ev.phase}: ${ev.error}`);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const runOpts = {
|
|
74
|
+
parallel: !!options.parallel,
|
|
75
|
+
maxConcurrency: parseInt(options.concurrency, 10),
|
|
76
|
+
};
|
|
77
|
+
const result = options.review
|
|
78
|
+
? await pipeline.runWithReview(runOpts)
|
|
79
|
+
: await pipeline.run(runOpts);
|
|
80
|
+
if (options.json) {
|
|
81
|
+
console.log(JSON.stringify(result, null, 2));
|
|
82
|
+
} else if (!options.stream) {
|
|
83
|
+
logger.info(`\nOutput: ${result.outputPath}`);
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (options.stream) {
|
|
87
|
+
process.stdout.write(
|
|
88
|
+
`${JSON.stringify({ type: "error", error: err.message, ts: Date.now() })}\n`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
logger.error(`Video edit failed: ${err.message}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── cc video deconstruct ──────────────────────────────────
|
|
97
|
+
video
|
|
98
|
+
.command("deconstruct")
|
|
99
|
+
.description("Extract frames + ASR + beat analysis (results cached)")
|
|
100
|
+
.requiredOption("--video <path>", "Input video file")
|
|
101
|
+
.option("--audio <path>", "Audio file for beat analysis")
|
|
102
|
+
.option("--srt <path>", "Existing subtitle file")
|
|
103
|
+
.option("--fps <n>", "Frame sampling FPS", "2")
|
|
104
|
+
.option("--use-madmom", "Use madmom for beat detection")
|
|
105
|
+
.option("--stream", "NDJSON events")
|
|
106
|
+
.option("--json", "JSON output")
|
|
107
|
+
.action(async (options) => {
|
|
108
|
+
const { VideoPipeline } =
|
|
109
|
+
await import("../skills/video-editing/pipeline.js");
|
|
110
|
+
|
|
111
|
+
const pipeline = new VideoPipeline({
|
|
112
|
+
videoPath: options.video,
|
|
113
|
+
audioPath: options.audio,
|
|
114
|
+
existingSrt: options.srt,
|
|
115
|
+
fps: parseInt(options.fps, 10),
|
|
116
|
+
useMadmom: !!options.useMadmom,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (options.stream) {
|
|
120
|
+
pipeline.on("event", (ev) =>
|
|
121
|
+
process.stdout.write(`${JSON.stringify(ev)}\n`),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const dir = await pipeline.deconstruct();
|
|
127
|
+
if (options.json) {
|
|
128
|
+
console.log(
|
|
129
|
+
JSON.stringify({ assetDir: dir, hash: dir.split(/[/\\]/).pop() }),
|
|
130
|
+
);
|
|
131
|
+
} else if (!options.stream) {
|
|
132
|
+
logger.info(`Assets cached: ${dir}`);
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
logger.error(`Deconstruct failed: ${err.message}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── cc video plan ─────────────────────────────────────────
|
|
141
|
+
video
|
|
142
|
+
.command("plan")
|
|
143
|
+
.description("Generate shot_plan from deconstructed assets + instruction")
|
|
144
|
+
.requiredOption("--asset-dir <path>", "Deconstructed asset directory")
|
|
145
|
+
.option("--instruction <text>", "Editing instruction", "")
|
|
146
|
+
.option("--character <name>", "Main character name")
|
|
147
|
+
.option("--json", "JSON output")
|
|
148
|
+
.action(async (options) => {
|
|
149
|
+
const { VideoPipeline } =
|
|
150
|
+
await import("../skills/video-editing/pipeline.js");
|
|
151
|
+
|
|
152
|
+
const pipeline = new VideoPipeline({
|
|
153
|
+
videoPath: "",
|
|
154
|
+
instruction: options.instruction,
|
|
155
|
+
mainCharacter: options.character,
|
|
156
|
+
cacheDir: options.assetDir,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const plan = await pipeline.plan(options.assetDir);
|
|
161
|
+
if (options.json) {
|
|
162
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
163
|
+
} else {
|
|
164
|
+
const shots = (plan.sections || []).reduce(
|
|
165
|
+
(s, sec) => s + (sec.shots?.length || 0),
|
|
166
|
+
0,
|
|
167
|
+
);
|
|
168
|
+
logger.info(
|
|
169
|
+
`Shot plan: ${plan.sections?.length || 0} sections, ${shots} shots`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
logger.error(`Plan failed: ${err.message}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── cc video assemble ─────────────────────────────────────
|
|
179
|
+
video
|
|
180
|
+
.command("assemble")
|
|
181
|
+
.description("Run Editor ReAct loop to select timestamps from shot_plan")
|
|
182
|
+
.requiredOption("--asset-dir <path>", "Deconstructed asset directory")
|
|
183
|
+
.requiredOption("--plan <path>", "shot_plan.json path")
|
|
184
|
+
.option("--parallel", "Run sections in parallel with conflict resolution")
|
|
185
|
+
.option("--concurrency <n>", "Max parallel sections", "4")
|
|
186
|
+
.option("--review", "Enable quality gate (VLM review before commit)")
|
|
187
|
+
.option("--stream", "NDJSON events")
|
|
188
|
+
.option("--json", "JSON output")
|
|
189
|
+
.action(async (options) => {
|
|
190
|
+
const { promises: fs } = await import("fs");
|
|
191
|
+
const { VideoPipeline } =
|
|
192
|
+
await import("../skills/video-editing/pipeline.js");
|
|
193
|
+
|
|
194
|
+
const shotPlan = JSON.parse(await fs.readFile(options.plan, "utf-8"));
|
|
195
|
+
const pipeline = new VideoPipeline({
|
|
196
|
+
videoPath: "",
|
|
197
|
+
cacheDir: options.assetDir,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (options.stream) {
|
|
201
|
+
pipeline.on("event", (ev) =>
|
|
202
|
+
process.stdout.write(`${JSON.stringify(ev)}\n`),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
let points;
|
|
208
|
+
if (options.parallel) {
|
|
209
|
+
points = await pipeline.assembleParallel(shotPlan, options.assetDir, {
|
|
210
|
+
maxConcurrency: parseInt(options.concurrency, 10),
|
|
211
|
+
});
|
|
212
|
+
} else {
|
|
213
|
+
points = await pipeline.assemble(shotPlan, options.assetDir);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (options.review) {
|
|
217
|
+
const { approved } = await pipeline.review(points, options.assetDir);
|
|
218
|
+
points = approved;
|
|
219
|
+
}
|
|
220
|
+
if (options.json) {
|
|
221
|
+
console.log(JSON.stringify(points, null, 2));
|
|
222
|
+
} else if (!options.stream) {
|
|
223
|
+
logger.info(`Assembled ${points.length} shot points`);
|
|
224
|
+
}
|
|
225
|
+
} catch (err) {
|
|
226
|
+
logger.error(`Assemble failed: ${err.message}`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ── cc video render ───────────────────────────────────────
|
|
232
|
+
video
|
|
233
|
+
.command("render")
|
|
234
|
+
.description("Render shot_point.json into final video via ffmpeg")
|
|
235
|
+
.requiredOption("--video <path>", "Original video file")
|
|
236
|
+
.requiredOption("--points <path>", "shot_point.json path")
|
|
237
|
+
.option("--audio <path>", "Background music to mix")
|
|
238
|
+
.option("--output <path>", "Output path", "./output.mp4")
|
|
239
|
+
.option("--stream", "NDJSON events")
|
|
240
|
+
.option("--json", "JSON output")
|
|
241
|
+
.action(async (options) => {
|
|
242
|
+
const { promises: fs } = await import("fs");
|
|
243
|
+
const { VideoPipeline, getCacheDir } =
|
|
244
|
+
await import("../skills/video-editing/pipeline.js");
|
|
245
|
+
|
|
246
|
+
const shotPoints = JSON.parse(await fs.readFile(options.points, "utf-8"));
|
|
247
|
+
const pipeline = new VideoPipeline({
|
|
248
|
+
videoPath: options.video,
|
|
249
|
+
audioPath: options.audio,
|
|
250
|
+
outputPath: options.output,
|
|
251
|
+
cacheDir: getCacheDir(options.video, options.audio),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (options.stream) {
|
|
255
|
+
pipeline.on("event", (ev) =>
|
|
256
|
+
process.stdout.write(`${JSON.stringify(ev)}\n`),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const outPath = await pipeline.render(shotPoints);
|
|
262
|
+
if (options.json) {
|
|
263
|
+
console.log(JSON.stringify({ outputPath: outPath }));
|
|
264
|
+
} else if (!options.stream) {
|
|
265
|
+
logger.info(`Rendered: ${outPath}`);
|
|
266
|
+
}
|
|
267
|
+
} catch (err) {
|
|
268
|
+
logger.error(`Render failed: ${err.message}`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ── cc video assets ───────────────────────────────────────
|
|
274
|
+
video
|
|
275
|
+
.command("assets")
|
|
276
|
+
.description("Manage deconstructed video asset cache")
|
|
277
|
+
.argument("[action]", "list | show | prune", "list")
|
|
278
|
+
.option("--hash <hash>", "Asset hash to show")
|
|
279
|
+
.option("--older-than <days>", "Prune assets older than N days")
|
|
280
|
+
.option("--json", "JSON output")
|
|
281
|
+
.action(async (action, options) => {
|
|
282
|
+
const { promises: fs } = await import("fs");
|
|
283
|
+
const pathMod = await import("path");
|
|
284
|
+
|
|
285
|
+
const base = process.env.APPDATA
|
|
286
|
+
? pathMod.join(
|
|
287
|
+
process.env.APPDATA,
|
|
288
|
+
"chainlesschain-desktop-vue",
|
|
289
|
+
".chainlesschain",
|
|
290
|
+
"video-editing",
|
|
291
|
+
)
|
|
292
|
+
: pathMod.join(
|
|
293
|
+
process.env.HOME || "~",
|
|
294
|
+
".chainlesschain",
|
|
295
|
+
"video-editing",
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
if (action === "list") {
|
|
299
|
+
try {
|
|
300
|
+
const dirs = await fs.readdir(base);
|
|
301
|
+
const assets = [];
|
|
302
|
+
for (const d of dirs) {
|
|
303
|
+
const metaPath = pathMod.join(base, d, "meta.json");
|
|
304
|
+
try {
|
|
305
|
+
const meta = JSON.parse(await fs.readFile(metaPath, "utf-8"));
|
|
306
|
+
const stat = await fs.stat(metaPath);
|
|
307
|
+
assets.push({
|
|
308
|
+
hash: d,
|
|
309
|
+
...meta,
|
|
310
|
+
modifiedAt: stat.mtime.toISOString(),
|
|
311
|
+
});
|
|
312
|
+
} catch {}
|
|
313
|
+
}
|
|
314
|
+
if (options.json) {
|
|
315
|
+
console.log(JSON.stringify({ assets }, null, 2));
|
|
316
|
+
} else {
|
|
317
|
+
if (assets.length === 0) {
|
|
318
|
+
logger.info("No cached assets.");
|
|
319
|
+
} else {
|
|
320
|
+
for (const a of assets) {
|
|
321
|
+
logger.info(
|
|
322
|
+
`${a.hash} ${a.videoPath || "?"} ${a.modifiedAt}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
logger.info("No cached assets.");
|
|
329
|
+
}
|
|
330
|
+
} else if (action === "show" && options.hash) {
|
|
331
|
+
const dir = pathMod.join(base, options.hash);
|
|
332
|
+
try {
|
|
333
|
+
const files = await fs.readdir(dir);
|
|
334
|
+
if (options.json) {
|
|
335
|
+
console.log(JSON.stringify({ hash: options.hash, files }));
|
|
336
|
+
} else {
|
|
337
|
+
logger.info(`Asset ${options.hash}:`);
|
|
338
|
+
for (const f of files) logger.info(` ${f}`);
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
logger.error(`Asset not found: ${options.hash}`);
|
|
342
|
+
}
|
|
343
|
+
} else if (action === "prune") {
|
|
344
|
+
const days = parseInt(options.olderThan || "30", 10);
|
|
345
|
+
const cutoff = Date.now() - days * 86400000;
|
|
346
|
+
try {
|
|
347
|
+
const dirs = await fs.readdir(base);
|
|
348
|
+
let removed = 0;
|
|
349
|
+
for (const d of dirs) {
|
|
350
|
+
const dirPath = pathMod.join(base, d);
|
|
351
|
+
const stat = await fs.stat(dirPath);
|
|
352
|
+
if (stat.mtimeMs < cutoff) {
|
|
353
|
+
await fs.rm(dirPath, { recursive: true, force: true });
|
|
354
|
+
removed++;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
logger.info(`Pruned ${removed} asset(s) older than ${days} days.`);
|
|
358
|
+
} catch {
|
|
359
|
+
logger.info("Nothing to prune.");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|