portable-agent-layer 0.94.0 → 0.95.1
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/README.md +2 -0
- package/assets/templates/hooks.antigravity.json +1 -1
- package/assets/templates/hooks.codex.json +11 -1
- package/assets/templates/hooks.copilot.json +11 -4
- package/assets/templates/hooks.cursor.json +8 -2
- package/assets/templates/settings.claude.json +55 -3
- package/package.json +1 -1
- package/src/cli/content.ts +23 -0
- package/src/cli/events.ts +47 -0
- package/src/cli/tree.ts +4 -0
- package/src/hooks/AttentionEvent.ts +22 -0
- package/src/hooks/LoadContext.ts +2 -2
- package/src/hooks/PostToolOrchestrator.ts +20 -0
- package/src/hooks/PreCompactPersist.ts +4 -2
- package/src/hooks/PreserveTranscripts.ts +13 -0
- package/src/hooks/SessionClose.ts +5 -0
- package/src/hooks/UserPromptOrchestrator.ts +9 -2
- package/src/hooks/lib/attention-events.ts +43 -0
- package/src/hooks/lib/auto-update.ts +24 -1
- package/src/hooks/lib/commitment-events.ts +20 -0
- package/src/hooks/lib/discord-events.ts +25 -0
- package/src/hooks/lib/entity-events.ts +16 -0
- package/src/hooks/lib/events.ts +252 -0
- package/src/hooks/lib/export.ts +1 -1
- package/src/hooks/lib/hook-timing.ts +33 -0
- package/src/hooks/lib/inference-events.ts +56 -0
- package/src/hooks/lib/inference.ts +17 -1
- package/src/hooks/lib/interaction.ts +74 -27
- package/src/hooks/lib/opencode-events.ts +59 -0
- package/src/hooks/lib/paths.ts +1 -0
- package/src/hooks/lib/post-tool.ts +66 -0
- package/src/hooks/lib/session-events.ts +211 -0
- package/src/hooks/lib/settings.ts +6 -0
- package/src/hooks/lib/stop.ts +27 -31
- package/src/hooks/lib/tool-events.ts +106 -0
- package/src/hooks/lib/transcript-preserve.ts +133 -0
- package/src/hooks/lib/turn-extras.ts +78 -0
- package/src/hooks/lib/waiting-on-user.ts +75 -0
- package/src/hooks/lib/work-tracking.ts +17 -3
- package/src/targets/opencode/plugin.ts +33 -1
- package/src/tools/agent/interaction.ts +2 -2
- package/src/tools/agent/project.ts +4 -0
- package/src/tools/control-room/snooze.ts +2 -0
- package/src/tools/control-room/writes.ts +2 -0
- package/src/tools/knowledge/lib.ts +5 -0
- package/src/tools/lib/session-usage.ts +7 -3
- package/src/tools/lib/transcript-usage.ts +14 -0
- package/src/tools/lib/usage-buckets.ts +15 -8
- package/src/hooks/LedgerCommit.ts +0 -34
- package/src/hooks/LedgerUnapplied.ts +0 -47
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A marker per session for the moment the agent started waiting on the user,
|
|
3
|
+
* taken by whatever the user does next so it can say how long they took.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { ensureDir, paths } from "./paths";
|
|
9
|
+
|
|
10
|
+
const MAX_WAIT_SEC = 30 * 60;
|
|
11
|
+
|
|
12
|
+
type WaitKind = "permission" | "notified";
|
|
13
|
+
|
|
14
|
+
interface Waiting {
|
|
15
|
+
ts: string;
|
|
16
|
+
tool?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function waitingFile(kind: WaitKind, session: string): string {
|
|
20
|
+
const safe = session.replace(/[^\w-]/g, "_");
|
|
21
|
+
return resolve(
|
|
22
|
+
ensureDir(resolve(paths.state(), "waiting-on-user")),
|
|
23
|
+
`${kind}-${safe}.json`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function markWaiting(kind: WaitKind, session: string, waiting: Waiting): void {
|
|
28
|
+
writeFileSync(waitingFile(kind, session), JSON.stringify(waiting));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readWaiting(file: string): Waiting | null {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(readFileSync(file, "utf-8"));
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function secondsSince(ts: string, now: Date): number {
|
|
40
|
+
return Math.round((now.getTime() - Date.parse(ts)) / 100) / 10;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function takeWaiting(
|
|
44
|
+
kind: WaitKind,
|
|
45
|
+
session: string,
|
|
46
|
+
now: Date,
|
|
47
|
+
belongs: (waiting: Waiting) => boolean = () => true
|
|
48
|
+
): number | null {
|
|
49
|
+
const file = waitingFile(kind, session);
|
|
50
|
+
if (!existsSync(file)) return null;
|
|
51
|
+
const waiting = readWaiting(file);
|
|
52
|
+
if (waiting && !belongs(waiting)) return null;
|
|
53
|
+
rmSync(file, { force: true });
|
|
54
|
+
if (!waiting) return null;
|
|
55
|
+
const waited = secondsSince(waiting.ts, now);
|
|
56
|
+
return waited >= 0 && waited <= MAX_WAIT_SEC ? waited : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Seconds the user took to answer a prompt for this tool, once; null when none was open. */
|
|
60
|
+
export function permissionWaitSec(
|
|
61
|
+
session: string | null,
|
|
62
|
+
tool: string,
|
|
63
|
+
now: Date = new Date()
|
|
64
|
+
): number | null {
|
|
65
|
+
if (!session) return null;
|
|
66
|
+
return takeWaiting("permission", session, now, (waiting) => waiting.tool === tool);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Seconds from the last notification to the user's next prompt, once. */
|
|
70
|
+
export function backAfterNotificationSec(
|
|
71
|
+
session: string,
|
|
72
|
+
now: Date = new Date()
|
|
73
|
+
): number | null {
|
|
74
|
+
return takeWaiting("notified", session, now);
|
|
75
|
+
}
|
|
@@ -41,6 +41,17 @@ function readSessions(): SessionRecord[] {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function sessionArchiveFile(record: SessionRecord): string {
|
|
45
|
+
const month = Number.isNaN(Date.parse(record.ts)) ? "undated" : record.ts.slice(0, 7);
|
|
46
|
+
return resolve(ensureDir(resolve(paths.state(), "sessions-archive")), `${month}.jsonl`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function archiveSessions(records: SessionRecord[]): void {
|
|
50
|
+
for (const record of records) {
|
|
51
|
+
appendFileSync(sessionArchiveFile(record), `${JSON.stringify(record)}\n`, "utf-8");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
export function writeSession(record: SessionRecord): void {
|
|
45
56
|
const sessions = readSessions();
|
|
46
57
|
// Replace existing record with same sessionId, or append
|
|
@@ -50,9 +61,12 @@ export function writeSession(record: SessionRecord): void {
|
|
|
50
61
|
} else {
|
|
51
62
|
sessions.push(record);
|
|
52
63
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
archiveSessions(sessions.slice(0, -MAX_SESSIONS));
|
|
65
|
+
writeFileSync(
|
|
66
|
+
sessionsPath(),
|
|
67
|
+
JSON.stringify(sessions.slice(-MAX_SESSIONS), null, 2),
|
|
68
|
+
"utf-8"
|
|
69
|
+
);
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
/** Detect session completion status from last assistant message */
|
|
@@ -61,6 +61,16 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
61
61
|
|
|
62
62
|
// Load shared handlers
|
|
63
63
|
const { runStopHandlers } = await lib<typeof import("../../hooks/lib/stop")>("stop.ts");
|
|
64
|
+
const { logToolCall } =
|
|
65
|
+
await lib<typeof import("../../hooks/lib/tool-events")>("tool-events.ts");
|
|
66
|
+
const { logAttention } =
|
|
67
|
+
await lib<typeof import("../../hooks/lib/attention-events")>("attention-events.ts");
|
|
68
|
+
const {
|
|
69
|
+
logOpencodeCompaction,
|
|
70
|
+
logOpencodePermissionReply,
|
|
71
|
+
logOpencodeSessionCreated,
|
|
72
|
+
rememberAskedTool,
|
|
73
|
+
} = await lib<typeof import("../../hooks/lib/opencode-events")>("opencode-events.ts");
|
|
64
74
|
const { captureSessionName } = await lib<
|
|
65
75
|
typeof import("../../hooks/handlers/session-name")
|
|
66
76
|
>("../handlers/session-name.ts");
|
|
@@ -137,6 +147,13 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
137
147
|
autoUpdateOnClose();
|
|
138
148
|
}
|
|
139
149
|
|
|
150
|
+
if (event.type === "session.created")
|
|
151
|
+
logOpencodeSessionCreated(event.properties.info);
|
|
152
|
+
if (event.type === "session.compacted")
|
|
153
|
+
logOpencodeCompaction(event.properties.sessionID);
|
|
154
|
+
if (event.type === "permission.replied")
|
|
155
|
+
logOpencodePermissionReply(event.properties);
|
|
156
|
+
|
|
140
157
|
if (event.type === "session.created" || event.type === "session.updated") {
|
|
141
158
|
const { regenerateIfNeeded } =
|
|
142
159
|
await lib<typeof import("../../hooks/lib/claude-md")>("claude-md.ts");
|
|
@@ -237,10 +254,25 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
237
254
|
if (call) snapshotCall(call);
|
|
238
255
|
},
|
|
239
256
|
|
|
257
|
+
"permission.ask": async (input: { id: string; type: string; sessionID: string }) => {
|
|
258
|
+
rememberAskedTool(input);
|
|
259
|
+
logAttention({
|
|
260
|
+
hook_event_name: "PermissionRequest",
|
|
261
|
+
session_id: input.sessionID,
|
|
262
|
+
tool_name: input.type,
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
|
|
240
266
|
"tool.execute.after": async (
|
|
241
267
|
input: { tool: string; sessionID: string; callID: string; args: unknown },
|
|
242
|
-
|
|
268
|
+
output: { title: string; output: string; metadata: unknown }
|
|
243
269
|
) => {
|
|
270
|
+
logToolCall({
|
|
271
|
+
tool_name: input.tool,
|
|
272
|
+
tool_input: input.args,
|
|
273
|
+
session_id: input.sessionID,
|
|
274
|
+
tool_response: output?.output,
|
|
275
|
+
});
|
|
244
276
|
const call = ledgeredOpencodeCall(input.tool, input.callID, input.args);
|
|
245
277
|
if (!call) return;
|
|
246
278
|
|
|
@@ -12,8 +12,8 @@ import { group, leaf, runCommand, UsageError } from "../lib/command";
|
|
|
12
12
|
import { claimCheckLines, reportLines, summarize } from "../lib/interaction-report";
|
|
13
13
|
import { scriptArgs } from "../lib/script-args";
|
|
14
14
|
|
|
15
|
-
const DETAILS = `Reads the measured turns in memory/
|
|
16
|
-
text) and prints turns, channels, reactions, the hints sent, per agent the
|
|
15
|
+
const DETAILS = `Reads the measured turns from the event log in memory/events/ and the older
|
|
16
|
+
memory/signals/interaction/ files (features only, no text) and prints turns, channels, reactions, the hints sent, per agent the
|
|
17
17
|
turns, replies filed and hints sent, and for each label (short, long, fast,
|
|
18
18
|
skimming, friction) how the replies written while it was active compare with
|
|
19
19
|
the replies written while none was. Last, the replies that claimed a result
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { resolve } from "node:path";
|
|
13
13
|
import { writeBinding } from "../../hooks/lib/bindings";
|
|
14
|
+
import { logCommitment } from "../../hooks/lib/commitment-events";
|
|
14
15
|
import { paths, toPath } from "../../hooks/lib/paths";
|
|
15
16
|
import {
|
|
16
17
|
defaultSlug,
|
|
@@ -454,6 +455,7 @@ function cmdCompleteIsc(name: string, id: number): undefined {
|
|
|
454
455
|
return;
|
|
455
456
|
}
|
|
456
457
|
applyIscMove(p, move);
|
|
458
|
+
logCommitment(name, id, "kept", "cli");
|
|
457
459
|
ok({ checked: true, id, archived: true });
|
|
458
460
|
}
|
|
459
461
|
|
|
@@ -479,6 +481,7 @@ function cmdReopenIsc(name: string, id: number): undefined {
|
|
|
479
481
|
return;
|
|
480
482
|
}
|
|
481
483
|
applyIscMove(p, move);
|
|
484
|
+
logCommitment(name, id, "reopened", "cli");
|
|
482
485
|
ok({ checked: false, id });
|
|
483
486
|
}
|
|
484
487
|
|
|
@@ -546,6 +549,7 @@ function cmdRetireIsc(name: string, id: number, by: number | null): undefined {
|
|
|
546
549
|
);
|
|
547
550
|
p.updated = now();
|
|
548
551
|
writeProject(p);
|
|
552
|
+
logCommitment(name, id, "dropped", "cli");
|
|
549
553
|
ok({ retired: true, id, supersededBy: by, archived: true });
|
|
550
554
|
}
|
|
551
555
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
|
+
import { logCommitment } from "../../hooks/lib/commitment-events";
|
|
11
12
|
import { ensureDir, paths } from "../../hooks/lib/paths";
|
|
12
13
|
|
|
13
14
|
const DAY_MS = 86_400_000;
|
|
@@ -61,5 +62,6 @@ export function setSnooze(
|
|
|
61
62
|
if (days <= 0) delete live[key];
|
|
62
63
|
else live[key] = new Date(now.getTime() + days * DAY_MS).toISOString().slice(0, 10);
|
|
63
64
|
writeFileSync(snoozeFile(), `${JSON.stringify(live, null, 2)}\n`, "utf-8");
|
|
65
|
+
if (days > 0) logCommitment(project, id, "snoozed", "control-room", days);
|
|
64
66
|
return live[key] ?? null;
|
|
65
67
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* write surface over ~/.pal.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { logCommitment } from "../../hooks/lib/commitment-events";
|
|
9
10
|
import { readProject, writeProject } from "../../hooks/lib/projects";
|
|
10
11
|
import {
|
|
11
12
|
raw as rawSettings,
|
|
@@ -38,6 +39,7 @@ export function setIscStatus(project: string, id: number, close: boolean): Write
|
|
|
38
39
|
p.changelog = move.changelog;
|
|
39
40
|
p.updated = new Date().toISOString();
|
|
40
41
|
writeProject(p);
|
|
42
|
+
logCommitment(project, id, close ? "kept" : "reopened", "control-room");
|
|
41
43
|
return { ok: true, changed: true };
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
writeFileSync,
|
|
32
32
|
} from "node:fs";
|
|
33
33
|
import { resolve } from "node:path";
|
|
34
|
+
import { logEntityFirstSeen } from "../../hooks/lib/entity-events";
|
|
34
35
|
import { ensureDir, paths } from "../../hooks/lib/paths";
|
|
35
36
|
|
|
36
37
|
// --- Constants --------------------------------------------------------------
|
|
@@ -412,9 +413,13 @@ export function exists(domain: Domain, slug: string, rootDir?: string): boolean
|
|
|
412
413
|
|
|
413
414
|
export function save(entity: Entity, rootDir?: string): void {
|
|
414
415
|
const p = entityPath(entity.domain, entity.slug, rootDir);
|
|
416
|
+
const firstSeenInTheStore = !rootDir && !existsSync(p);
|
|
415
417
|
const tmp = `${p}.tmp`;
|
|
416
418
|
writeFileSync(tmp, serialize(entity), "utf-8");
|
|
417
419
|
renameSync(tmp, p);
|
|
420
|
+
if (firstSeenInTheStore) {
|
|
421
|
+
logEntityFirstSeen(entity.domain, entity.frontmatter.type, entity.frontmatter.title);
|
|
422
|
+
}
|
|
418
423
|
}
|
|
419
424
|
|
|
420
425
|
export function load(domain: Domain, slug: string, rootDir?: string): Entity | null {
|
|
@@ -10,7 +10,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
import { costOfUsage } from "../../hooks/lib/models";
|
|
13
|
-
import { cacheWritesOf, type TranscriptUsage } from "./transcript-usage";
|
|
13
|
+
import { cacheWritesOf, finalPerMessage, type TranscriptUsage } from "./transcript-usage";
|
|
14
14
|
|
|
15
15
|
export interface SessionUsage {
|
|
16
16
|
input: number;
|
|
@@ -94,7 +94,7 @@ interface TranscriptLine {
|
|
|
94
94
|
type?: string;
|
|
95
95
|
timestamp?: string;
|
|
96
96
|
sessionId?: string;
|
|
97
|
-
message?: { model?: string; usage?: TranscriptUsage };
|
|
97
|
+
message?: { id?: string; model?: string; usage?: TranscriptUsage };
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
function emptyUsage(): SessionUsage {
|
|
@@ -113,6 +113,7 @@ function emptyUsage(): SessionUsage {
|
|
|
113
113
|
|
|
114
114
|
export function accumulateUsage(transcript: string, sessionId: string): SessionUsage {
|
|
115
115
|
const total = emptyUsage();
|
|
116
|
+
const assistantTurns: TranscriptLine[] = [];
|
|
116
117
|
let firstTs = "";
|
|
117
118
|
let lastTs = "";
|
|
118
119
|
|
|
@@ -133,7 +134,10 @@ export function accumulateUsage(transcript: string, sessionId: string): SessionU
|
|
|
133
134
|
lastTs = entry.timestamp;
|
|
134
135
|
}
|
|
135
136
|
|
|
136
|
-
if (entry.type
|
|
137
|
+
if (entry.type === "assistant") assistantTurns.push(entry);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const entry of finalPerMessage(assistantTurns, (e) => e.message?.id)) {
|
|
137
141
|
const usage = entry.message?.usage;
|
|
138
142
|
const model = entry.message?.model;
|
|
139
143
|
if (!usage || !model) continue;
|
|
@@ -18,6 +18,20 @@ export interface TranscriptUsage {
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Claude Code writes one transcript line per content block of a message, each
|
|
23
|
+
* repeating the message's usage — and an early line can carry the usage while
|
|
24
|
+
* the message was still streaming. A message is billed once, at its last line.
|
|
25
|
+
*/
|
|
26
|
+
export function finalPerMessage<T>(
|
|
27
|
+
calls: Iterable<T>,
|
|
28
|
+
messageIdOf: (call: T) => string | undefined
|
|
29
|
+
): T[] {
|
|
30
|
+
const byMessage = new Map<unknown, T>();
|
|
31
|
+
for (const call of calls) byMessage.set(messageIdOf(call) ?? call, call);
|
|
32
|
+
return [...byMessage.values()];
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
export interface CacheWrites {
|
|
22
36
|
cacheWrite5m: number;
|
|
23
37
|
cacheWrite1h: number;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
9
|
import { costOfUsage } from "../../hooks/lib/models";
|
|
10
|
-
import { cacheWritesOf, type TranscriptUsage } from "./transcript-usage";
|
|
10
|
+
import { cacheWritesOf, finalPerMessage, type TranscriptUsage } from "./transcript-usage";
|
|
11
11
|
|
|
12
12
|
export interface Bucket {
|
|
13
13
|
input: number;
|
|
@@ -180,7 +180,7 @@ export function usageTally(projectFilter?: string, now: Date = new Date()): Usag
|
|
|
180
180
|
interface TranscriptLine {
|
|
181
181
|
type?: string;
|
|
182
182
|
timestamp?: string;
|
|
183
|
-
message?: { model?: string; usage?: TranscriptUsage };
|
|
183
|
+
message?: { id?: string; model?: string; usage?: TranscriptUsage };
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
/**
|
|
@@ -209,6 +209,7 @@ function transcriptsIn(projPath: string): string[] {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
interface PricedCall {
|
|
212
|
+
messageId?: string;
|
|
212
213
|
ts: string;
|
|
213
214
|
model: string;
|
|
214
215
|
tokens: Tokens;
|
|
@@ -230,6 +231,7 @@ function pricedCallOf(line: string): PricedCall | null {
|
|
|
230
231
|
if (!usage || !model || !ts) return null;
|
|
231
232
|
|
|
232
233
|
return {
|
|
234
|
+
messageId: entry.message?.id,
|
|
233
235
|
ts,
|
|
234
236
|
model,
|
|
235
237
|
tokens: {
|
|
@@ -248,6 +250,7 @@ export function readClaudeCode(
|
|
|
248
250
|
): AgentUsage {
|
|
249
251
|
const tally = usageTally(projectFilter, now);
|
|
250
252
|
if (!existsSync(claudeDir)) return tally.usage;
|
|
253
|
+
const calls: (PricedCall & { project: string })[] = [];
|
|
251
254
|
|
|
252
255
|
const projectDirs = readdirSync(claudeDir, { withFileTypes: true })
|
|
253
256
|
.filter((entry) => entry.isDirectory())
|
|
@@ -269,16 +272,20 @@ export function readClaudeCode(
|
|
|
269
272
|
// Cheap reject before the parse: most lines are not model turns.
|
|
270
273
|
if (!line.includes('"usage"')) continue;
|
|
271
274
|
const call = pricedCallOf(line);
|
|
272
|
-
if (
|
|
273
|
-
tally.record({
|
|
274
|
-
...call,
|
|
275
|
-
project: projName,
|
|
276
|
-
cost: costOfUsage(call.model, call.tokens),
|
|
277
|
-
});
|
|
275
|
+
if (call) calls.push({ ...call, project: projName });
|
|
278
276
|
}
|
|
279
277
|
}
|
|
280
278
|
}
|
|
281
279
|
|
|
280
|
+
for (const call of finalPerMessage(calls, (c) => c.messageId)) {
|
|
281
|
+
tally.record({
|
|
282
|
+
ts: call.ts,
|
|
283
|
+
model: call.model,
|
|
284
|
+
tokens: call.tokens,
|
|
285
|
+
project: call.project,
|
|
286
|
+
cost: costOfUsage(call.model, call.tokens),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
282
289
|
return tally.usage;
|
|
283
290
|
}
|
|
284
291
|
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook: PostToolUse — pairs the parked before-state with the result and writes
|
|
3
|
-
* the ledger entry.
|
|
4
|
-
*
|
|
5
|
-
* Only a call that actually landed reaches this event, so every entry written
|
|
6
|
-
* here is an applied one. A denied or failed call fires the snapshot half and
|
|
7
|
-
* never this one, leaving its snapshot unclaimed — which is why claiming also
|
|
8
|
-
* reaps the ones that were abandoned.
|
|
9
|
-
*
|
|
10
|
-
* Silent and fail-open, for the same reason as its other half.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { duplicatesCursorHooks } from "./lib/cursor-shadow";
|
|
14
|
-
import { enterHookWorkspace } from "./lib/hook-turn";
|
|
15
|
-
import { commitApplied, landedCalls } from "./lib/ledger-hook";
|
|
16
|
-
import { logDebug } from "./lib/log";
|
|
17
|
-
import { readStdinJSON } from "./lib/stdin";
|
|
18
|
-
|
|
19
|
-
if (duplicatesCursorHooks()) process.exit(0);
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const input = await readStdinJSON<Record<string, unknown>>();
|
|
23
|
-
if (!input) process.exit(0);
|
|
24
|
-
|
|
25
|
-
enterHookWorkspace(input);
|
|
26
|
-
for (const call of landedCalls(input)) {
|
|
27
|
-
const entry = commitApplied(call);
|
|
28
|
-
if (entry) {
|
|
29
|
-
logDebug("LedgerCommit", `recorded ${entry.id} ${entry.tool} ${entry.target}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
} catch {
|
|
33
|
-
process.exit(0);
|
|
34
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook: PostToolUseFailure and PermissionDenied — records an edit that was
|
|
3
|
-
* attempted and did not land.
|
|
4
|
-
*
|
|
5
|
-
* A log that keeps only what succeeded cannot answer what was attempted, and
|
|
6
|
-
* that is usually the question being asked of it. So the two endings that are
|
|
7
|
-
* not success are recorded too, and kept apart: `failed` is the tool's own
|
|
8
|
-
* attempt breaking, `denied` is something refusing to let it run.
|
|
9
|
-
*
|
|
10
|
-
* One hook serves both events because they differ only in which key carries the
|
|
11
|
-
* reason — a difference `unappliedVerdictOf` owns, so registering this on a
|
|
12
|
-
* third such event later is a line in that table rather than a new file.
|
|
13
|
-
*
|
|
14
|
-
* Not everything that fails to land reaches here. A manual denial at the
|
|
15
|
-
* permission dialog, a `deny` rule, and a PreToolUse hook's own block all fire
|
|
16
|
-
* PreToolUse and nothing after it; a schema rejection fires no hook at all.
|
|
17
|
-
* Those attempts leave a snapshot no half ever claims, which is what the reaper
|
|
18
|
-
* in ledger.ts is for.
|
|
19
|
-
*
|
|
20
|
-
* Silent and fail-open, for the same reason as the other halves.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { duplicatesCursorHooks } from "./lib/cursor-shadow";
|
|
24
|
-
import { reapStalePending } from "./lib/ledger";
|
|
25
|
-
import { commitUnapplied, ledgeredCalls, unappliedVerdictOf } from "./lib/ledger-hook";
|
|
26
|
-
import { logDebug } from "./lib/log";
|
|
27
|
-
import { readStdinJSON } from "./lib/stdin";
|
|
28
|
-
|
|
29
|
-
if (duplicatesCursorHooks()) process.exit(0);
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const input = await readStdinJSON<Record<string, unknown>>();
|
|
33
|
-
if (!input) process.exit(0);
|
|
34
|
-
|
|
35
|
-
const calls = ledgeredCalls(input);
|
|
36
|
-
const verdict = calls.length > 0 && unappliedVerdictOf(input);
|
|
37
|
-
if (!verdict) process.exit(0);
|
|
38
|
-
|
|
39
|
-
for (const call of calls) {
|
|
40
|
-
const entry = commitUnapplied(call, verdict);
|
|
41
|
-
logDebug("LedgerUnapplied", `recorded ${entry.id} ${entry.outcome} ${entry.target}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
reapStalePending();
|
|
45
|
-
} catch {
|
|
46
|
-
process.exit(0);
|
|
47
|
-
}
|