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
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { promises as fsp, existsSync, mkdirSync } from "node:fs";
|
|
11
12
|
import { getHomeDir } from "./paths.js";
|
|
12
13
|
import {
|
|
13
14
|
MemoryStore,
|
|
14
15
|
BetaFlags,
|
|
15
16
|
ApprovalGate,
|
|
16
17
|
APPROVAL_POLICY,
|
|
18
|
+
SessionManager,
|
|
19
|
+
StreamRouter,
|
|
17
20
|
createMemoryFileAdapter,
|
|
18
21
|
createBetaFlagsFileAdapter,
|
|
19
22
|
createApprovalGateFileAdapter,
|
|
@@ -23,6 +26,7 @@ import {
|
|
|
23
26
|
let _memoryStore = null;
|
|
24
27
|
let _betaFlags = null;
|
|
25
28
|
let _approvalGate = null;
|
|
29
|
+
let _sessionManager = null;
|
|
26
30
|
|
|
27
31
|
export function getMemoryStorePath() {
|
|
28
32
|
return join(getHomeDir(), "memory-store.json");
|
|
@@ -63,8 +67,60 @@ export async function getApprovalGate() {
|
|
|
63
67
|
return _approvalGate;
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
export function getParkedSessionsPath() {
|
|
71
|
+
return join(getHomeDir(), "parked-sessions.json");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createParkedSessionsStore(filePath) {
|
|
75
|
+
async function readAll() {
|
|
76
|
+
try {
|
|
77
|
+
const raw = await fsp.readFile(filePath, "utf8");
|
|
78
|
+
return JSON.parse(raw || "{}");
|
|
79
|
+
} catch {
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function writeAll(map) {
|
|
84
|
+
const dir = join(filePath, "..");
|
|
85
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
86
|
+
await fsp.writeFile(filePath, JSON.stringify(map, null, 2), "utf8");
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
async save(json) {
|
|
90
|
+
const map = await readAll();
|
|
91
|
+
map[json.sessionId] = json;
|
|
92
|
+
await writeAll(map);
|
|
93
|
+
},
|
|
94
|
+
async load(sessionId) {
|
|
95
|
+
const map = await readAll();
|
|
96
|
+
return map[sessionId] || null;
|
|
97
|
+
},
|
|
98
|
+
async remove(sessionId) {
|
|
99
|
+
const map = await readAll();
|
|
100
|
+
delete map[sessionId];
|
|
101
|
+
await writeAll(map);
|
|
102
|
+
},
|
|
103
|
+
async list() {
|
|
104
|
+
return Object.values(await readAll());
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getSessionManager() {
|
|
110
|
+
if (_sessionManager) return _sessionManager;
|
|
111
|
+
const store = createParkedSessionsStore(getParkedSessionsPath());
|
|
112
|
+
_sessionManager = new SessionManager({ store });
|
|
113
|
+
_sessionManager._parkedStore = store;
|
|
114
|
+
return _sessionManager;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function createStreamRouter() {
|
|
118
|
+
return new StreamRouter();
|
|
119
|
+
}
|
|
120
|
+
|
|
66
121
|
export function resetSessionCoreSingletonsForTests() {
|
|
67
122
|
_memoryStore = null;
|
|
68
123
|
_betaFlags = null;
|
|
69
124
|
_approvalGate = null;
|
|
125
|
+
_sessionManager = null;
|
|
70
126
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session tail — Phase I of Managed Agents parity plan.
|
|
3
|
+
*
|
|
4
|
+
* Follows a JSONL session file and yields new events as an async iterable.
|
|
5
|
+
* Uses offset-polling so it works cross-platform (fs.watch on Windows is
|
|
6
|
+
* unreliable for appended-to files).
|
|
7
|
+
*
|
|
8
|
+
* Pure generator + helper; the CLI command just wires output.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { promises as fsp } from "node:fs";
|
|
12
|
+
import { existsSync, statSync } from "node:fs";
|
|
13
|
+
import { sessionPath } from "../harness/jsonl-session-store.js";
|
|
14
|
+
|
|
15
|
+
function parseLine(line) {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
if (!trimmed) return null;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(trimmed);
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse a contiguous chunk of buffered text into events. Returns {events, rest}
|
|
27
|
+
* where `rest` is the unterminated trailing partial line to keep for the next
|
|
28
|
+
* read.
|
|
29
|
+
*/
|
|
30
|
+
export function parseChunk(buffer) {
|
|
31
|
+
const events = [];
|
|
32
|
+
let rest = buffer;
|
|
33
|
+
let nl = rest.indexOf("\n");
|
|
34
|
+
while (nl !== -1) {
|
|
35
|
+
const line = rest.slice(0, nl);
|
|
36
|
+
rest = rest.slice(nl + 1);
|
|
37
|
+
const evt = parseLine(line);
|
|
38
|
+
if (evt) events.push(evt);
|
|
39
|
+
nl = rest.indexOf("\n");
|
|
40
|
+
}
|
|
41
|
+
return { events, rest };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function matchesFilter(event, { types, sinceMs }) {
|
|
45
|
+
if (types && types.length > 0 && !types.includes(event.type)) return false;
|
|
46
|
+
if (sinceMs && event.timestamp && event.timestamp < sinceMs) return false;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the initial offset for a session:
|
|
52
|
+
* - fromStart: 0
|
|
53
|
+
* - fromEnd (default): current EOF, so only new events are yielded
|
|
54
|
+
*/
|
|
55
|
+
export function initialOffset(sessionId, { fromStart = false } = {}) {
|
|
56
|
+
const p = sessionPath(sessionId);
|
|
57
|
+
if (!existsSync(p)) return 0;
|
|
58
|
+
if (fromStart) return 0;
|
|
59
|
+
return statSync(p).size;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Follow a session file. Yields {event, offset} objects as new JSONL lines
|
|
64
|
+
* are appended. Caller passes an AbortSignal (or the generator runs forever).
|
|
65
|
+
*
|
|
66
|
+
* Options:
|
|
67
|
+
* - signal AbortSignal — stops the loop
|
|
68
|
+
* - pollMs polling interval (default 200)
|
|
69
|
+
* - fromStart start from byte 0 (default false — tail from EOF)
|
|
70
|
+
* - fromOffset explicit starting byte offset (overrides fromStart)
|
|
71
|
+
* - types string[] of event.type to include (null = all)
|
|
72
|
+
* - sinceMs only yield events with timestamp >= sinceMs
|
|
73
|
+
* - once if true, stop once file is drained (no polling)
|
|
74
|
+
*/
|
|
75
|
+
export async function* followSession(sessionId, options = {}) {
|
|
76
|
+
const {
|
|
77
|
+
signal,
|
|
78
|
+
pollMs = 200,
|
|
79
|
+
fromStart = false,
|
|
80
|
+
fromOffset,
|
|
81
|
+
types = null,
|
|
82
|
+
sinceMs = null,
|
|
83
|
+
once = false,
|
|
84
|
+
} = options;
|
|
85
|
+
|
|
86
|
+
const filePath = sessionPath(sessionId);
|
|
87
|
+
let offset =
|
|
88
|
+
typeof fromOffset === "number"
|
|
89
|
+
? fromOffset
|
|
90
|
+
: initialOffset(sessionId, { fromStart });
|
|
91
|
+
let buffer = "";
|
|
92
|
+
|
|
93
|
+
// eslint-disable-next-line no-constant-condition
|
|
94
|
+
while (true) {
|
|
95
|
+
if (signal?.aborted) return;
|
|
96
|
+
|
|
97
|
+
if (existsSync(filePath)) {
|
|
98
|
+
const stat = await fsp.stat(filePath);
|
|
99
|
+
if (stat.size < offset) {
|
|
100
|
+
// File was truncated / rotated — restart from beginning
|
|
101
|
+
offset = 0;
|
|
102
|
+
buffer = "";
|
|
103
|
+
}
|
|
104
|
+
if (stat.size > offset) {
|
|
105
|
+
const fd = await fsp.open(filePath, "r");
|
|
106
|
+
try {
|
|
107
|
+
const length = stat.size - offset;
|
|
108
|
+
const buf = Buffer.alloc(length);
|
|
109
|
+
await fd.read(buf, 0, length, offset);
|
|
110
|
+
offset = stat.size;
|
|
111
|
+
buffer += buf.toString("utf-8");
|
|
112
|
+
} finally {
|
|
113
|
+
await fd.close();
|
|
114
|
+
}
|
|
115
|
+
const { events, rest } = parseChunk(buffer);
|
|
116
|
+
buffer = rest;
|
|
117
|
+
for (const evt of events) {
|
|
118
|
+
if (matchesFilter(evt, { types, sinceMs })) {
|
|
119
|
+
yield { event: evt, offset };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (once) return;
|
|
126
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session usage aggregation — Phase I of Managed Agents parity plan.
|
|
3
|
+
*
|
|
4
|
+
* Scans JSONL session events for token usage (emitted either as explicit
|
|
5
|
+
* `token_usage` events or embedded under `event.data.usage` on
|
|
6
|
+
* `assistant_message` / `llm_call` events) and produces roll-ups by model.
|
|
7
|
+
*
|
|
8
|
+
* Purely functional aggregation + file-reading helpers. No state.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
readEvents,
|
|
13
|
+
listJsonlSessions,
|
|
14
|
+
} from "../harness/jsonl-session-store.js";
|
|
15
|
+
|
|
16
|
+
const USAGE_EVENT_TYPES = new Set([
|
|
17
|
+
"token_usage",
|
|
18
|
+
"assistant_message",
|
|
19
|
+
"llm_call",
|
|
20
|
+
"llm_response",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function toNumber(v) {
|
|
24
|
+
const n = Number(v);
|
|
25
|
+
return Number.isFinite(n) ? n : 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extract a normalized usage record from a single event, or null if none.
|
|
30
|
+
* Accepts both snake_case (OpenAI/Anthropic) and camelCase variants.
|
|
31
|
+
*/
|
|
32
|
+
export function extractUsage(event) {
|
|
33
|
+
if (!event || typeof event !== "object") return null;
|
|
34
|
+
if (!USAGE_EVENT_TYPES.has(event.type)) return null;
|
|
35
|
+
|
|
36
|
+
const d = event.data || {};
|
|
37
|
+
const raw =
|
|
38
|
+
event.type === "token_usage"
|
|
39
|
+
? d.usage || d
|
|
40
|
+
: d.usage || d.tokenUsage || null;
|
|
41
|
+
if (!raw || typeof raw !== "object") return null;
|
|
42
|
+
|
|
43
|
+
const inputTokens = toNumber(
|
|
44
|
+
raw.input_tokens ?? raw.prompt_tokens ?? raw.inputTokens ?? 0,
|
|
45
|
+
);
|
|
46
|
+
const outputTokens = toNumber(
|
|
47
|
+
raw.output_tokens ?? raw.completion_tokens ?? raw.outputTokens ?? 0,
|
|
48
|
+
);
|
|
49
|
+
const totalTokens = toNumber(
|
|
50
|
+
raw.total_tokens ?? raw.totalTokens ?? inputTokens + outputTokens,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (inputTokens === 0 && outputTokens === 0 && totalTokens === 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
provider: d.provider || raw.provider || null,
|
|
59
|
+
model: d.model || raw.model || null,
|
|
60
|
+
inputTokens,
|
|
61
|
+
outputTokens,
|
|
62
|
+
totalTokens,
|
|
63
|
+
timestamp: event.timestamp || null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Aggregate a list of events into { total, byModel[] }.
|
|
69
|
+
*/
|
|
70
|
+
export function aggregateUsage(events) {
|
|
71
|
+
const byKey = new Map();
|
|
72
|
+
const total = {
|
|
73
|
+
inputTokens: 0,
|
|
74
|
+
outputTokens: 0,
|
|
75
|
+
totalTokens: 0,
|
|
76
|
+
calls: 0,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
for (const evt of events || []) {
|
|
80
|
+
const u = extractUsage(evt);
|
|
81
|
+
if (!u) continue;
|
|
82
|
+
|
|
83
|
+
total.inputTokens += u.inputTokens;
|
|
84
|
+
total.outputTokens += u.outputTokens;
|
|
85
|
+
total.totalTokens += u.totalTokens;
|
|
86
|
+
total.calls += 1;
|
|
87
|
+
|
|
88
|
+
const key = `${u.provider || "?"}/${u.model || "?"}`;
|
|
89
|
+
const entry = byKey.get(key) || {
|
|
90
|
+
provider: u.provider,
|
|
91
|
+
model: u.model,
|
|
92
|
+
inputTokens: 0,
|
|
93
|
+
outputTokens: 0,
|
|
94
|
+
totalTokens: 0,
|
|
95
|
+
calls: 0,
|
|
96
|
+
};
|
|
97
|
+
entry.inputTokens += u.inputTokens;
|
|
98
|
+
entry.outputTokens += u.outputTokens;
|
|
99
|
+
entry.totalTokens += u.totalTokens;
|
|
100
|
+
entry.calls += 1;
|
|
101
|
+
byKey.set(key, entry);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
total,
|
|
106
|
+
byModel: Array.from(byKey.values()).sort(
|
|
107
|
+
(a, b) => b.totalTokens - a.totalTokens,
|
|
108
|
+
),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Roll up usage for a single JSONL session.
|
|
114
|
+
*/
|
|
115
|
+
export function sessionUsage(sessionId) {
|
|
116
|
+
const events = readEvents(sessionId);
|
|
117
|
+
const agg = aggregateUsage(events);
|
|
118
|
+
return { sessionId, ...agg };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Roll up usage across every JSONL session on disk.
|
|
123
|
+
*/
|
|
124
|
+
export function allSessionsUsage({ limit = 1000 } = {}) {
|
|
125
|
+
const sessions = listJsonlSessions({ limit });
|
|
126
|
+
const perSession = sessions.map((s) => sessionUsage(s.id));
|
|
127
|
+
|
|
128
|
+
const total = {
|
|
129
|
+
inputTokens: 0,
|
|
130
|
+
outputTokens: 0,
|
|
131
|
+
totalTokens: 0,
|
|
132
|
+
calls: 0,
|
|
133
|
+
};
|
|
134
|
+
const byKey = new Map();
|
|
135
|
+
|
|
136
|
+
for (const s of perSession) {
|
|
137
|
+
total.inputTokens += s.total.inputTokens;
|
|
138
|
+
total.outputTokens += s.total.outputTokens;
|
|
139
|
+
total.totalTokens += s.total.totalTokens;
|
|
140
|
+
total.calls += s.total.calls;
|
|
141
|
+
for (const row of s.byModel) {
|
|
142
|
+
const key = `${row.provider || "?"}/${row.model || "?"}`;
|
|
143
|
+
const entry = byKey.get(key) || {
|
|
144
|
+
provider: row.provider,
|
|
145
|
+
model: row.model,
|
|
146
|
+
inputTokens: 0,
|
|
147
|
+
outputTokens: 0,
|
|
148
|
+
totalTokens: 0,
|
|
149
|
+
calls: 0,
|
|
150
|
+
};
|
|
151
|
+
entry.inputTokens += row.inputTokens;
|
|
152
|
+
entry.outputTokens += row.outputTokens;
|
|
153
|
+
entry.totalTokens += row.totalTokens;
|
|
154
|
+
entry.calls += row.calls;
|
|
155
|
+
byKey.set(key, entry);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
sessions: perSession,
|
|
161
|
+
total,
|
|
162
|
+
byModel: Array.from(byKey.values()).sort(
|
|
163
|
+
(a, b) => b.totalTokens - a.totalTokens,
|
|
164
|
+
),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shell-approval — compose the existing shell policy ruleset with session-core
|
|
3
|
+
* ApprovalGate.
|
|
4
|
+
*
|
|
5
|
+
* Managed Agents parity Phase G item #3: `evaluateShellCommandPolicy` returns a
|
|
6
|
+
* hard allow/deny/warn on rule patterns, but we also want per-session policy
|
|
7
|
+
* tiers (strict / trusted / autopilot) and a confirm() hook before medium/high
|
|
8
|
+
* risk commands actually run.
|
|
9
|
+
*
|
|
10
|
+
* Mapping:
|
|
11
|
+
* shell decision DENY → risk HIGH (still rejected outright even before
|
|
12
|
+
* policy — hard-denied rules are always
|
|
13
|
+
* unsafe)
|
|
14
|
+
* shell decision WARN → risk MEDIUM
|
|
15
|
+
* shell decision ALLOW → risk LOW
|
|
16
|
+
* shell decision REROUTE → risk HIGH (rerouted, never actually executed)
|
|
17
|
+
*
|
|
18
|
+
* Returns a uniform shape `{ allowed, decision, via, reason, shellPolicy,
|
|
19
|
+
* riskLevel, policy }` so callers don't have to juggle two decision types.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const sharedShellPolicy = require("../runtime/coding-agent-shell-policy.cjs");
|
|
23
|
+
const {
|
|
24
|
+
APPROVAL_RISK: RISK,
|
|
25
|
+
APPROVAL_DECISION: DECISION,
|
|
26
|
+
} = require("@chainlesschain/session-core");
|
|
27
|
+
|
|
28
|
+
const SHELL_TO_RISK = {
|
|
29
|
+
allow: RISK.LOW,
|
|
30
|
+
warn: RISK.MEDIUM,
|
|
31
|
+
deny: RISK.HIGH,
|
|
32
|
+
reroute: RISK.HIGH,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
async function evaluateShellCommandWithApproval({
|
|
36
|
+
command,
|
|
37
|
+
sessionId = null,
|
|
38
|
+
approvalGate = null,
|
|
39
|
+
shellPolicyOptions = {},
|
|
40
|
+
} = {}) {
|
|
41
|
+
const shellPolicy = sharedShellPolicy.evaluateShellCommandPolicy(
|
|
42
|
+
command,
|
|
43
|
+
shellPolicyOptions,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const riskLevel = SHELL_TO_RISK[shellPolicy.decision] || RISK.MEDIUM;
|
|
47
|
+
|
|
48
|
+
// Hard-blocked rules bypass the gate entirely — the gate tier cannot
|
|
49
|
+
// up-authorize them.
|
|
50
|
+
if (!shellPolicy.allowed) {
|
|
51
|
+
return {
|
|
52
|
+
allowed: false,
|
|
53
|
+
decision: DECISION.DENY,
|
|
54
|
+
via: "shell-policy",
|
|
55
|
+
reason: shellPolicy.reason,
|
|
56
|
+
shellPolicy,
|
|
57
|
+
riskLevel,
|
|
58
|
+
policy: null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// No ApprovalGate wired? Preserve legacy behavior (shell policy decides).
|
|
63
|
+
if (!approvalGate || typeof approvalGate.decide !== "function") {
|
|
64
|
+
return {
|
|
65
|
+
allowed: true,
|
|
66
|
+
decision: DECISION.ALLOW,
|
|
67
|
+
via: "shell-policy",
|
|
68
|
+
reason: shellPolicy.reason,
|
|
69
|
+
shellPolicy,
|
|
70
|
+
riskLevel,
|
|
71
|
+
policy: null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const gateResult = await approvalGate.decide({
|
|
76
|
+
sessionId,
|
|
77
|
+
riskLevel,
|
|
78
|
+
tool: "run_shell",
|
|
79
|
+
args: { command },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
allowed: gateResult.decision === DECISION.ALLOW,
|
|
84
|
+
decision: gateResult.decision,
|
|
85
|
+
via: gateResult.via,
|
|
86
|
+
reason: shellPolicy.reason,
|
|
87
|
+
shellPolicy,
|
|
88
|
+
riskLevel,
|
|
89
|
+
policy: gateResult.policy,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
evaluateShellCommandWithApproval,
|
|
95
|
+
SHELL_TO_RISK,
|
|
96
|
+
};
|
|
@@ -46,6 +46,9 @@ export class WSChatHandler {
|
|
|
46
46
|
model: session.model,
|
|
47
47
|
baseUrl: session.baseUrl || "http://localhost:11434",
|
|
48
48
|
apiKey: session.apiKey,
|
|
49
|
+
// Phase J — pipe WS session id so chat-core records token_usage
|
|
50
|
+
// into the JSONL session store; visible via `cc session usage`.
|
|
51
|
+
sessionId: session.sessionId || session.id,
|
|
49
52
|
};
|
|
50
53
|
|
|
51
54
|
const fullContent = await chatWithStreaming(
|