codiedev 0.10.0-beta.4 → 0.10.0-beta.6
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 +11 -5
- package/assets/tui-brand-plugin.tsx +14 -2
- package/dist/backup/cache.js +1 -5
- package/dist/backup/match.js +0 -6
- package/dist/backup/run.js +3 -35
- package/dist/captureExclusions.js +0 -12
- package/dist/captureLog.js +0 -28
- package/dist/captureReadiness.js +0 -58
- package/dist/captureSignals.js +68 -3
- package/dist/cli.js +75 -64
- package/dist/codexHookTrust.js +125 -6
- package/dist/codexLimitScan.js +2 -109
- package/dist/codie.js +50 -114
- package/dist/codieBrandInstall.js +0 -18
- package/dist/codieLauncher.js +20 -92
- package/dist/codieRuntime.js +19 -23
- package/dist/codieRuntimeInstall.js +108 -23
- package/dist/codieUsage.js +38 -41
- package/dist/commands/agentTemplates.generated.js +136 -0
- package/dist/commands/agents.js +1066 -0
- package/dist/commands/agentsSdk.generated.js +7 -0
- package/dist/commands/ask.js +0 -3
- package/dist/commands/capture.js +0 -2
- package/dist/commands/createTicket.js +0 -10
- package/dist/commands/doctor.js +53 -120
- package/dist/commands/evals.js +187 -0
- package/dist/commands/inbox.js +0 -2
- package/dist/commands/library.js +0 -3
- package/dist/commands/pack.js +573 -0
- package/dist/commands/ping.js +0 -2
- package/dist/commands/pull.js +6 -10
- package/dist/commands/push.js +2 -12
- package/dist/commands/search.js +0 -4
- package/dist/commands/send.js +0 -5
- package/dist/commands/share.js +0 -4
- package/dist/commands/shared.js +0 -35
- package/dist/commands/traces.js +344 -0
- package/dist/commands/tracesLabel.js +348 -0
- package/dist/configDir.js +0 -10
- package/dist/connect.js +19 -116
- package/dist/connectFlow.js +0 -9
- package/dist/copilotHooks.js +391 -0
- package/dist/copilotInstallation.js +64 -0
- package/dist/cursorHooks.js +38 -30
- package/dist/cursorLifecycle.js +109 -0
- package/dist/detection.js +0 -30
- package/dist/harnessDirs.js +77 -0
- package/dist/hook.js +182 -469
- package/dist/inboxFormat.js +0 -24
- package/dist/launchContext.js +150 -0
- package/dist/launcher.js +140 -0
- package/dist/limitEvent.js +41 -35
- package/dist/limitEventReporter.js +1 -1
- package/dist/managedPi.js +240 -0
- package/dist/mcp.js +3 -384
- package/dist/nativeSteering.js +24 -0
- package/dist/packs/codie.js +182 -0
- package/dist/packs/harnesses.js +411 -0
- package/dist/packs/hookGate.js +256 -0
- package/dist/packs/install.js +286 -0
- package/dist/packs/manifest.js +126 -0
- package/dist/packs/paths.js +81 -0
- package/dist/packs/shim.js +214 -0
- package/dist/packs/store.js +155 -0
- package/dist/packs/sync.js +170 -0
- package/dist/piHooks.js +216 -0
- package/dist/piRuntime.js +120 -0
- package/dist/repoResolver.js +0 -19
- package/dist/secret.js +0 -3
- package/dist/selfUpdate.js +376 -0
- package/dist/skillMarker.js +23 -0
- package/dist/subagents.js +135 -0
- package/dist/teamSkillContext.js +10 -0
- package/dist/transcriptModels.js +6 -8
- package/dist/upgradeNudge.js +0 -30
- package/dist/utils.js +494 -824
- package/dist/version.js +0 -10
- package/dist/versionCheck.js +0 -33
- package/package.json +8 -9
- package/dist/backup/cache.d.ts +0 -14
- package/dist/backup/match.d.ts +0 -2
- package/dist/backup/run.d.ts +0 -25
- package/dist/captureExclusions.d.ts +0 -30
- package/dist/captureLog.d.ts +0 -20
- package/dist/captureReadiness.d.ts +0 -65
- package/dist/captureSignals.d.ts +0 -19
- package/dist/cli.d.ts +0 -16
- package/dist/codexHookTrust.d.ts +0 -34
- package/dist/codexLimitScan.d.ts +0 -64
- package/dist/codie.d.ts +0 -2
- package/dist/codieBrandInstall.d.ts +0 -11
- package/dist/codieLauncher.d.ts +0 -86
- package/dist/codieRuntime.d.ts +0 -29
- package/dist/codieRuntimeInstall.d.ts +0 -28
- package/dist/codieStartup.d.ts +0 -26
- package/dist/codieStartup.js +0 -30
- package/dist/codieUsage.d.ts +0 -57
- package/dist/commands/ask.d.ts +0 -1
- package/dist/commands/capture.d.ts +0 -14
- package/dist/commands/createTicket.d.ts +0 -27
- package/dist/commands/delete.d.ts +0 -1
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/inbox.d.ts +0 -3
- package/dist/commands/library.d.ts +0 -2
- package/dist/commands/note.d.ts +0 -1
- package/dist/commands/ping.d.ts +0 -1
- package/dist/commands/promote.d.ts +0 -1
- package/dist/commands/promote.js +0 -53
- package/dist/commands/pull.d.ts +0 -2
- package/dist/commands/push.d.ts +0 -33
- package/dist/commands/react.d.ts +0 -2
- package/dist/commands/react.js +0 -40
- package/dist/commands/reverseTicket.d.ts +0 -2
- package/dist/commands/reverseTicket.js +0 -477
- package/dist/commands/search.d.ts +0 -2
- package/dist/commands/security.d.ts +0 -1
- package/dist/commands/security.js +0 -156
- package/dist/commands/send.d.ts +0 -2
- package/dist/commands/share.d.ts +0 -2
- package/dist/commands/shared.d.ts +0 -64
- package/dist/configDir.d.ts +0 -1
- package/dist/connect.d.ts +0 -1
- package/dist/connectFlow.d.ts +0 -13
- package/dist/cursorHooks.d.ts +0 -23
- package/dist/detection.d.ts +0 -30
- package/dist/hook.d.ts +0 -6
- package/dist/inboxFormat.d.ts +0 -38
- package/dist/limitEvent.d.ts +0 -16
- package/dist/limitEventReporter.d.ts +0 -21
- package/dist/mcp.d.ts +0 -12
- package/dist/prUrl.d.ts +0 -9
- package/dist/prUrl.js +0 -33
- package/dist/repoResolver.d.ts +0 -17
- package/dist/secret.d.ts +0 -1
- package/dist/security.d.ts +0 -102
- package/dist/security.js +0 -437
- package/dist/securityDeepScan.d.ts +0 -41
- package/dist/securityDeepScan.js +0 -248
- package/dist/securityDoctor.d.ts +0 -32
- package/dist/securityDoctor.js +0 -63
- package/dist/securityFeature.d.ts +0 -5
- package/dist/securityFeature.js +0 -7
- package/dist/securityInstall.d.ts +0 -63
- package/dist/securityInstall.js +0 -239
- package/dist/securityInventory.d.ts +0 -54
- package/dist/securityInventory.js +0 -556
- package/dist/securityPilot.d.ts +0 -19
- package/dist/securityPilot.js +0 -61
- package/dist/securityPolicy.d.ts +0 -33
- package/dist/securityPolicy.js +0 -188
- package/dist/securitySession.d.ts +0 -6
- package/dist/securitySession.js +0 -72
- package/dist/transcriptModels.d.ts +0 -18
- package/dist/upgradeNudge.d.ts +0 -25
- package/dist/utils.d.ts +0 -346
- package/dist/version.d.ts +0 -1
- package/dist/versionCheck.d.ts +0 -68
package/dist/hook.js
CHANGED
|
@@ -39,30 +39,28 @@ const captureSignals_1 = require("./captureSignals");
|
|
|
39
39
|
const transcriptModels_1 = require("./transcriptModels");
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const os = __importStar(require("os"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
42
43
|
const https = __importStar(require("https"));
|
|
43
44
|
const http = __importStar(require("http"));
|
|
44
45
|
const child_process_1 = require("child_process");
|
|
45
46
|
const utils_1 = require("./utils");
|
|
46
47
|
const limitEventReporter_1 = require("./limitEventReporter");
|
|
48
|
+
const limitEvent_1 = require("./limitEvent");
|
|
49
|
+
const cursorLifecycle_1 = require("./cursorLifecycle");
|
|
47
50
|
const codexLimitScan_1 = require("./codexLimitScan");
|
|
51
|
+
const launchContext_1 = require("./launchContext");
|
|
52
|
+
const selfUpdate_1 = require("./selfUpdate");
|
|
48
53
|
const version_1 = require("./version");
|
|
54
|
+
const sync_1 = require("./packs/sync");
|
|
49
55
|
const captureLog_1 = require("./captureLog");
|
|
50
|
-
const
|
|
51
|
-
const securitySession_1 = require("./securitySession");
|
|
52
|
-
const securityPolicy_1 = require("./securityPolicy");
|
|
53
|
-
const securityFeature_1 = require("./securityFeature");
|
|
54
|
-
const securityInventory_1 = require("./securityInventory");
|
|
56
|
+
const subagents_1 = require("./subagents");
|
|
55
57
|
const captureExclusions_1 = require("./captureExclusions");
|
|
56
58
|
const cursorHooks_1 = require("./cursorHooks");
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// (e.g. "pr-push.md") rather than a "<key>" placeholder — otherwise agents
|
|
60
|
-
// paraphrase the key (pr-push) and the extension-sensitive `pull` 404s.
|
|
59
|
+
const copilotHooks_1 = require("./copilotHooks");
|
|
60
|
+
const piHooks_1 = require("./piHooks");
|
|
61
61
|
function buildDiscoveryContext(skills) {
|
|
62
62
|
if (skills.length === 0)
|
|
63
63
|
return null;
|
|
64
|
-
// Don't trust the API's ordering — rank by score so the suggested pull key is
|
|
65
|
-
// always the highest-confidence match.
|
|
66
64
|
const top = [...skills].sort((a, b) => b.score - a.score).slice(0, 3);
|
|
67
65
|
const lines = top
|
|
68
66
|
.map((s) => `- ${s.key} — ${(s.score * 100).toFixed(0)}% match`)
|
|
@@ -74,31 +72,27 @@ function buildDiscoveryContext(skills) {
|
|
|
74
72
|
"`. It encodes the team's vetted " +
|
|
75
73
|
"approach. Skip it only if it clearly doesn't apply.");
|
|
76
74
|
}
|
|
77
|
-
|
|
78
|
-
// clears the confidence threshold, inject it as additionalContext so the agent
|
|
79
|
-
// can offer to pull+run it. Fully silent on miss/error — never blocks input.
|
|
80
|
-
async function runDiscover(prompt) {
|
|
75
|
+
async function discoverContext(prompt) {
|
|
81
76
|
if (!prompt || !prompt.trim())
|
|
82
|
-
|
|
77
|
+
return null;
|
|
83
78
|
const config = (0, utils_1.readConfig)();
|
|
84
79
|
if (!config)
|
|
85
|
-
|
|
80
|
+
return null;
|
|
86
81
|
try {
|
|
87
|
-
// POST body, never a query param: raw prompts in GET query strings land
|
|
88
|
-
// in server/proxy access logs (SIG-16). The server keeps accepting legacy
|
|
89
|
-
// GETs from older CLIs during the transition.
|
|
90
82
|
const res = await postJsonForResponse(`${config.backendUrl}/api/cli/skill-discovery`, { q: prompt }, config.token);
|
|
91
83
|
const skills = res?.skills ?? [];
|
|
92
|
-
|
|
93
|
-
if (additionalContext) {
|
|
94
|
-
process.stdout.write(JSON.stringify({
|
|
95
|
-
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext },
|
|
96
|
-
}));
|
|
97
|
-
}
|
|
84
|
+
return buildDiscoveryContext(skills);
|
|
98
85
|
}
|
|
99
86
|
catch {
|
|
100
|
-
// silent — discovery is best-effort
|
|
101
87
|
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
async function runDiscover(prompt) {
|
|
91
|
+
const additionalContext = await discoverContext(prompt);
|
|
92
|
+
if (additionalContext)
|
|
93
|
+
process.stdout.write(JSON.stringify({
|
|
94
|
+
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext },
|
|
95
|
+
}));
|
|
102
96
|
process.exit(0);
|
|
103
97
|
}
|
|
104
98
|
function readTranscriptTailFile(transcriptPath, maxBytes = 512 * 1024) {
|
|
@@ -115,9 +109,6 @@ function readTranscriptTailFile(transcriptPath, maxBytes = 512 * 1024) {
|
|
|
115
109
|
fs.closeSync(handle);
|
|
116
110
|
}
|
|
117
111
|
}
|
|
118
|
-
// StopFailure (matcher rate_limit): the dev just got blocked. Read the local
|
|
119
|
-
// transcript tail for the ground-truth rate_limit line and POST a tiny event.
|
|
120
|
-
// Best-effort + silent — StopFailure output is ignored by Claude Code anyway.
|
|
121
112
|
async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
|
|
122
113
|
const config = (0, utils_1.readConfig)();
|
|
123
114
|
if (!config)
|
|
@@ -126,12 +117,9 @@ async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
|
|
|
126
117
|
const tp = hookInput.transcript_path;
|
|
127
118
|
if (tp) {
|
|
128
119
|
try {
|
|
129
|
-
// Codex invokes Stop after every turn. Bound this read so long-running
|
|
130
|
-
// sessions do not repeatedly load their entire rollout into memory.
|
|
131
120
|
transcript = readTranscriptTailFile(tp);
|
|
132
121
|
}
|
|
133
122
|
catch {
|
|
134
|
-
/* unreadable transcript — fall through to minimal event */
|
|
135
123
|
}
|
|
136
124
|
}
|
|
137
125
|
try {
|
|
@@ -144,23 +132,12 @@ async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
|
|
|
144
132
|
});
|
|
145
133
|
}
|
|
146
134
|
catch {
|
|
147
|
-
/* best-effort */
|
|
148
135
|
}
|
|
149
136
|
}
|
|
150
137
|
async function runStopFailure(hookInput) {
|
|
151
138
|
await readAndPostLimitEvent(hookInput, "claude-code", true);
|
|
152
139
|
process.exit(0);
|
|
153
140
|
}
|
|
154
|
-
// Codex path. Two things differ from Claude Code:
|
|
155
|
-
//
|
|
156
|
-
// 1. There is no event that fires when a credit/rate-limit block kills a turn.
|
|
157
|
-
// Codex skips its Stop hook for exactly that turn (measured: every normal
|
|
158
|
-
// task_complete pairs with a Stop-hook run, the blocked one never does),
|
|
159
|
-
// and its HookEventName enum has no StopFailure equivalent. So the block is
|
|
160
|
-
// reported from the developer's NEXT prompt instead — see runCodexLimitSweep.
|
|
161
|
-
// 2. Rollouts run to tens of megabytes and keep growing after the block, so a
|
|
162
|
-
// fixed tail read loses the marker (in one real 44 MB rollout it sat 1.13 MB
|
|
163
|
-
// from the end). Scan backwards for it instead.
|
|
164
141
|
async function reportCodexLimitFromFile(config, transcriptPath, sessionId) {
|
|
165
142
|
const hit = (0, codexLimitScan_1.scanCodexRolloutForLimit)(transcriptPath);
|
|
166
143
|
if (!hit)
|
|
@@ -171,8 +148,6 @@ async function reportCodexLimitFromFile(config, transcriptPath, sessionId) {
|
|
|
171
148
|
sessionId: sessionId ?? (0, codexLimitScan_1.sessionIdFromRolloutPath)(transcriptPath),
|
|
172
149
|
transcriptTail: (0, codexLimitScan_1.readTailEndingAt)(transcriptPath, hit.lineEnd),
|
|
173
150
|
});
|
|
174
|
-
// Tell the prompt sweep this one is done, so it neither re-scans the
|
|
175
|
-
// rollout nor re-posts an event the server would only deduplicate.
|
|
176
151
|
(0, codexLimitScan_1.recordCodexLimitReported)(transcriptPath, hit.event.at);
|
|
177
152
|
return true;
|
|
178
153
|
}
|
|
@@ -203,10 +178,6 @@ async function runCodexLimit(hookInput) {
|
|
|
203
178
|
process.exit(0);
|
|
204
179
|
}
|
|
205
180
|
const PROMPT_LIMIT_POST_TIMEOUT_MS = 2_500;
|
|
206
|
-
// UserPromptSubmit: the first Codiedev hook that runs after a block, because
|
|
207
|
-
// Codex runs no hook at the block itself. Scans the recent rollouts for any
|
|
208
|
-
// block not yet reported. Cheap in the normal case — a per-file size cache
|
|
209
|
-
// means an unchanged, unblocked rollout costs one statSync.
|
|
210
181
|
async function runCodexLimitSweep(sessionId) {
|
|
211
182
|
const config = (0, utils_1.readConfig)();
|
|
212
183
|
if (!config)
|
|
@@ -214,12 +185,7 @@ async function runCodexLimitSweep(sessionId) {
|
|
|
214
185
|
try {
|
|
215
186
|
const posted = await (0, codexLimitScan_1.sweepCodexLimits)({
|
|
216
187
|
sessionsDir: (0, codexLimitScan_1.codexSessionsDir)(),
|
|
217
|
-
// A shorter timeout than the default: this is on the user's prompt
|
|
218
|
-
// clock, and the next prompt retries anything that does not land.
|
|
219
188
|
post: (payload) => (0, limitEventReporter_1.postLimitEvent)(config, payload, PROMPT_LIMIT_POST_TIMEOUT_MS),
|
|
220
|
-
// Unchanged rollouts cost one statSync and do not spend the read
|
|
221
|
-
// budget, so in steady state this is nearly free. The module default
|
|
222
|
-
// covers a cold start; see DEFAULT_MAX_SCANS for why it is not small.
|
|
223
189
|
findOptions: { withinMs: 24 * 3600_000 },
|
|
224
190
|
scanOptions: { maxScanBytes: 4 * 1024 * 1024 },
|
|
225
191
|
});
|
|
@@ -241,10 +207,6 @@ async function runCodexLimitSweep(sessionId) {
|
|
|
241
207
|
});
|
|
242
208
|
}
|
|
243
209
|
}
|
|
244
|
-
// Skip-reason ping: this session is ending WITHOUT a capture, and here's why.
|
|
245
|
-
// Turns silent coverage loss into per-reason buckets on the admin Capture
|
|
246
|
-
// Health panel (patches the session's aiSessionStarts row server-side). Tiny
|
|
247
|
-
// like the sessionStart ping. Best-effort, never throws.
|
|
248
210
|
async function postSessionSkip(config, sessionId, reason) {
|
|
249
211
|
if (!sessionId)
|
|
250
212
|
return;
|
|
@@ -252,17 +214,12 @@ async function postSessionSkip(config, sessionId, reason) {
|
|
|
252
214
|
await postJson(`${config.backendUrl}/api/cli/sessionSkip`, { sessionId, reason, at: Date.now() }, config.token, PING_TIMEOUT_MS);
|
|
253
215
|
}
|
|
254
216
|
catch {
|
|
255
|
-
/* best-effort */
|
|
256
217
|
}
|
|
257
218
|
}
|
|
258
|
-
// SessionStart: the capture-coverage denominator. Record that a session opened
|
|
259
|
-
// so the backend can later compute captured ÷ started. Deliberately tiny — no
|
|
260
|
-
// transcript read, no git — so it never slows session startup. Best-effort +
|
|
261
|
-
// silent (SessionStart hook output is ignored by Claude Code).
|
|
262
219
|
async function runSessionStart(hookInput, source = "claude-code") {
|
|
263
220
|
const config = (0, utils_1.readConfig)();
|
|
264
221
|
if (!config)
|
|
265
|
-
|
|
222
|
+
return;
|
|
266
223
|
if (hookInput.session_id) {
|
|
267
224
|
try {
|
|
268
225
|
await postJson(`${config.backendUrl}/api/cli/sessionStart`, {
|
|
@@ -272,18 +229,11 @@ async function runSessionStart(hookInput, source = "claude-code") {
|
|
|
272
229
|
}, config.token, PING_TIMEOUT_MS);
|
|
273
230
|
}
|
|
274
231
|
catch {
|
|
275
|
-
/* best-effort — never block session startup */
|
|
276
232
|
}
|
|
277
233
|
}
|
|
278
|
-
|
|
234
|
+
return;
|
|
279
235
|
}
|
|
280
|
-
function postJson(url, body, bearerToken,
|
|
281
|
-
// Socket timeout. Pass a SMALL value for tiny pings that run on (or near)
|
|
282
|
-
// Claude Code's shutdown clock — a black-holed connection (proxy dropping
|
|
283
|
-
// packets without RST) otherwise hangs the hook until CC kills it. Leave
|
|
284
|
-
// unset for transcript uploads, which legitimately take a while on slow
|
|
285
|
-
// corporate links and already run in detached children with retry.
|
|
286
|
-
timeoutMs) {
|
|
236
|
+
function postJson(url, body, bearerToken, timeoutMs) {
|
|
287
237
|
return new Promise((resolve, reject) => {
|
|
288
238
|
const parsed = new URL(url);
|
|
289
239
|
const data = JSON.stringify(body);
|
|
@@ -324,9 +274,6 @@ timeoutMs) {
|
|
|
324
274
|
req.end();
|
|
325
275
|
});
|
|
326
276
|
}
|
|
327
|
-
// postJson variant that returns the parsed response body — the upload-URL
|
|
328
|
-
// handshake needs the { uploadUrl } the server hands back. No timeout: runs
|
|
329
|
-
// only in detached uploader children, never on the shutdown clock.
|
|
330
277
|
function postJsonForResponse(url, body, bearerToken, timeoutMs) {
|
|
331
278
|
return new Promise((resolve, reject) => {
|
|
332
279
|
const parsed = new URL(url);
|
|
@@ -370,138 +317,6 @@ function postJsonForResponse(url, body, bearerToken, timeoutMs) {
|
|
|
370
317
|
req.end();
|
|
371
318
|
});
|
|
372
319
|
}
|
|
373
|
-
// Local policy runs synchronously before a tool executes. Workflow telemetry
|
|
374
|
-
// is dispatched separately and fail-open, so a detector outage or timeout can
|
|
375
|
-
// never delay or override the deterministic endpoint decision.
|
|
376
|
-
async function runSecurityHook(hookInput, phase, source, uploadTelemetry = true) {
|
|
377
|
-
const config = (0, utils_1.readConfig)();
|
|
378
|
-
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
379
|
-
return;
|
|
380
|
-
const policy = (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId);
|
|
381
|
-
const sessionId = hookInput.session_id ?? hookInput.sessionId;
|
|
382
|
-
const sessionSignal = phase === "pre" ? (0, securitySession_1.readSessionSecuritySignal)(sessionId) : undefined;
|
|
383
|
-
const localDecision = (0, security_1.evaluateToolSecurity)(hookInput, phase, policy, source, sessionSignal);
|
|
384
|
-
if (phase === "post" && (0, security_1.securityInputHasSensitivePath)(hookInput)) {
|
|
385
|
-
(0, securitySession_1.recordSensitiveReadSignal)(sessionId);
|
|
386
|
-
}
|
|
387
|
-
const event = (0, security_1.buildSecurityWorkflowEvent)(hookInput, phase, source);
|
|
388
|
-
if (event) {
|
|
389
|
-
event.localDecision = localDecision.action;
|
|
390
|
-
event.localCategory = localDecision.category;
|
|
391
|
-
}
|
|
392
|
-
if (localDecision.action === "block" && phase === "pre") {
|
|
393
|
-
(0, captureLog_1.appendCaptureLog)({
|
|
394
|
-
stage: "security",
|
|
395
|
-
sessionId: hookInput.session_id ?? hookInput.sessionId ?? "unknown",
|
|
396
|
-
outcome: "ok",
|
|
397
|
-
detail: `phase=${phase} localDecision=block category=${localDecision.category}`,
|
|
398
|
-
});
|
|
399
|
-
if (uploadTelemetry)
|
|
400
|
-
dispatchSecurityEventUpload(event);
|
|
401
|
-
const response = source === "cursor"
|
|
402
|
-
? (0, cursorHooks_1.cursorPreToolBlockResponse)(localDecision.reason ?? "Codie blocked this action")
|
|
403
|
-
: {
|
|
404
|
-
hookSpecificOutput: {
|
|
405
|
-
hookEventName: "PreToolUse",
|
|
406
|
-
permissionDecision: "deny",
|
|
407
|
-
permissionDecisionReason: localDecision.reason,
|
|
408
|
-
},
|
|
409
|
-
};
|
|
410
|
-
process.stdout.write(JSON.stringify(response));
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
if (!event)
|
|
414
|
-
return;
|
|
415
|
-
if (uploadTelemetry)
|
|
416
|
-
dispatchSecurityEventUpload(event);
|
|
417
|
-
}
|
|
418
|
-
function runPromptSecurityHook(hookInput, source, uploadTelemetry = true) {
|
|
419
|
-
const config = (0, utils_1.readConfig)();
|
|
420
|
-
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
421
|
-
return false;
|
|
422
|
-
const decision = (0, security_1.evaluatePromptSecurity)(hookInput.prompt, (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId), source);
|
|
423
|
-
if (decision.action !== "block")
|
|
424
|
-
return false;
|
|
425
|
-
(0, captureLog_1.appendCaptureLog)({
|
|
426
|
-
stage: "security",
|
|
427
|
-
sessionId: hookInput.session_id ?? hookInput.sessionId ?? "unknown",
|
|
428
|
-
outcome: "ok",
|
|
429
|
-
detail: `phase=prompt localDecision=block category=${decision.category} telemetry=${uploadTelemetry ? "enabled" : "suppressed"} cwd=${hookInput.cwd ?? "unknown"}`,
|
|
430
|
-
});
|
|
431
|
-
if (uploadTelemetry) {
|
|
432
|
-
dispatchSecurityEventUpload((0, security_1.buildBlockedSecurityPromptEvent)(hookInput, source, decision.category ?? "secret_in_prompt"));
|
|
433
|
-
}
|
|
434
|
-
const response = source === "cursor"
|
|
435
|
-
? (0, cursorHooks_1.cursorPromptBlockResponse)(decision.reason ?? "Codie blocked this prompt")
|
|
436
|
-
: { decision: "block", reason: decision.reason };
|
|
437
|
-
process.stdout.write(JSON.stringify(response));
|
|
438
|
-
return true;
|
|
439
|
-
}
|
|
440
|
-
function dispatchAllowedPromptSecurityEvent(hookInput, source) {
|
|
441
|
-
const config = (0, utils_1.readConfig)();
|
|
442
|
-
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
443
|
-
return;
|
|
444
|
-
const event = (0, security_1.buildSecurityPromptEvent)(hookInput, source, Date.now, (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId));
|
|
445
|
-
if (event)
|
|
446
|
-
dispatchSecurityEventUpload(event);
|
|
447
|
-
}
|
|
448
|
-
function dispatchSecurityEventUpload(event) {
|
|
449
|
-
if (!event || !(0, securityFeature_1.isAgentSecurityEnabled)((0, utils_1.readConfig)()))
|
|
450
|
-
return;
|
|
451
|
-
try {
|
|
452
|
-
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], "security-upload"], {
|
|
453
|
-
detached: true,
|
|
454
|
-
stdio: ["pipe", "ignore", "ignore"],
|
|
455
|
-
});
|
|
456
|
-
child.stdin?.end(JSON.stringify(event));
|
|
457
|
-
child.unref();
|
|
458
|
-
}
|
|
459
|
-
catch {
|
|
460
|
-
// Telemetry is best-effort and must never delay local enforcement.
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
async function runSecurityEventUpload() {
|
|
464
|
-
const config = (0, utils_1.readConfig)();
|
|
465
|
-
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
466
|
-
return;
|
|
467
|
-
const startedAt = Date.now();
|
|
468
|
-
try {
|
|
469
|
-
const raw = await readStdin();
|
|
470
|
-
const event = JSON.parse(raw);
|
|
471
|
-
const cachedPolicy = (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId);
|
|
472
|
-
const eventWithEndpoint = (0, security_1.withEndpointIdentity)(event, {
|
|
473
|
-
endpointId: config.deviceId,
|
|
474
|
-
endpointName: config.deviceName,
|
|
475
|
-
cliVersion: version_1.CLI_VERSION,
|
|
476
|
-
securityRuntimeVersion: security_1.SECURITY_RUNTIME_VERSION,
|
|
477
|
-
securityPolicyVersion: cachedPolicy?.policyVersion,
|
|
478
|
-
});
|
|
479
|
-
const result = await postJsonForResponse(`${config.backendUrl}/api/cli/securityEvent`, { ...eventWithEndpoint }, config.token,
|
|
480
|
-
// Leave room for a cold backend and durable scheduling. This uploader is
|
|
481
|
-
// detached, so the network budget never delays the developer's prompt or
|
|
482
|
-
// tool call. Policy refreshes use the separately signed sync endpoint.
|
|
483
|
-
5_000);
|
|
484
|
-
if (result?.policy?.schemaVersion === 1) {
|
|
485
|
-
(0, securityPolicy_1.writeEndpointSecurityPolicy)(result.policy, undefined, undefined, config.companyId);
|
|
486
|
-
}
|
|
487
|
-
(0, captureLog_1.appendCaptureLog)({
|
|
488
|
-
stage: "security",
|
|
489
|
-
sessionId: event.sessionId,
|
|
490
|
-
outcome: "ok",
|
|
491
|
-
detail: `phase=${event.phase} elapsedMs=${Date.now() - startedAt} verdict=${String(result?.verdict ?? "unknown")}`,
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
catch (err) {
|
|
495
|
-
(0, captureLog_1.appendCaptureLog)({
|
|
496
|
-
stage: "security",
|
|
497
|
-
sessionId: "upload",
|
|
498
|
-
outcome: "ERROR",
|
|
499
|
-
detail: `elapsedMs=${Date.now() - startedAt} ${err.message}`,
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
// Raw-bytes POST to a Convex storage upload URL (no auth header — the URL
|
|
504
|
-
// itself is the one-time credential). Returns the parsed { storageId }.
|
|
505
320
|
function postBytes(url, bytes, contentType) {
|
|
506
321
|
return new Promise((resolve, reject) => {
|
|
507
322
|
const parsed = new URL(url);
|
|
@@ -537,9 +352,6 @@ function postBytes(url, bytes, contentType) {
|
|
|
537
352
|
req.end();
|
|
538
353
|
});
|
|
539
354
|
}
|
|
540
|
-
// Short leash for the tiny coverage pings (sessionStart / sessionSkip): they
|
|
541
|
-
// carry a few bytes and must never stall a hook. 3s is generous for a ping
|
|
542
|
-
// and a hard bound for the shutdown clock.
|
|
543
355
|
const PING_TIMEOUT_MS = 3000;
|
|
544
356
|
async function readStdin() {
|
|
545
357
|
return new Promise((resolve, reject) => {
|
|
@@ -557,50 +369,34 @@ function resolveMode(arg) {
|
|
|
557
369
|
return "codex";
|
|
558
370
|
if (arg === "capture-cursor" || arg === "backup-cursor")
|
|
559
371
|
return "cursor";
|
|
372
|
+
if (arg === "capture-copilot" || arg === "backup-copilot")
|
|
373
|
+
return "copilot";
|
|
560
374
|
if (arg === "capture-opencode" || arg === "capture-opencode-final")
|
|
561
375
|
return "opencode";
|
|
376
|
+
if (arg === "capture-pi" || arg === "capture-pi-final")
|
|
377
|
+
return "pi";
|
|
562
378
|
if (arg === "capture-desktop" || arg === "capture-desktop-final")
|
|
563
379
|
return "desktop";
|
|
564
380
|
return "claude-code";
|
|
565
381
|
}
|
|
566
382
|
async function main() {
|
|
567
|
-
// Shared upload deps used by both "upload" and "upload-deferred" branches.
|
|
568
|
-
// Extracted to keep both branches in lockstep — any new dep added here
|
|
569
|
-
// automatically applies to the deferred path too.
|
|
570
383
|
function makeUploadDeps() {
|
|
571
384
|
return {
|
|
572
385
|
post: postJson,
|
|
573
|
-
// Direct-to-storage flow for big transcripts (see utils.ts
|
|
574
|
-
// DIRECT_UPLOAD_THRESHOLD_CHARS): fetch a one-time upload URL, then
|
|
575
|
-
// POST the raw bytes to it. Keeps ~7MB+ transcripts out of the
|
|
576
|
-
// backend's 64MB captureSession isolate.
|
|
577
386
|
requestJson: postJsonForResponse,
|
|
578
387
|
uploadBytes: postBytes,
|
|
579
388
|
readFileSync: fs.readFileSync,
|
|
580
389
|
unlinkSync: fs.unlinkSync,
|
|
581
|
-
// Persist a bumped attempt count on failure so a later sweep retries
|
|
582
|
-
// (instead of dropping a session on a transient network blip).
|
|
583
390
|
writeFileSync: fs.writeFileSync,
|
|
584
|
-
// On a successful upload, drop the "last session captured" breadcrumb
|
|
585
|
-
// that `codiedev doctor` reads to confirm the session landed.
|
|
586
391
|
recordSuccess: (job) => (0, utils_1.writeLastCapture)((0, utils_1.buildLastCaptureRecord)(job, Date.now())),
|
|
587
|
-
// Write structured outcome lines (ok / ERROR / gave-up) to the
|
|
588
|
-
// rotating capture log so users and doctor can see why captures fail.
|
|
589
392
|
log: (e) => (0, captureLog_1.appendCaptureLog)(e),
|
|
590
|
-
// Write per-session upload state so the throttle can defer later
|
|
591
|
-
// non-final uploads within the same window.
|
|
592
393
|
writeSessionState: utils_1.writeSessionUploadState,
|
|
593
|
-
// Re-read local config in the uploader so adding an exclusion after a
|
|
594
|
-
// job was queued still prevents its transcript from leaving the device.
|
|
595
394
|
shouldSkipJob: (job) => {
|
|
596
395
|
const config = (0, utils_1.readConfig)();
|
|
597
396
|
return (0, captureExclusions_1.shouldSkipCaptureUploadJob)(job, config);
|
|
598
397
|
},
|
|
599
398
|
};
|
|
600
399
|
}
|
|
601
|
-
// Detached uploader child (spawned by the capture path below). The job —
|
|
602
|
-
// including transcript and token — is on disk; no stdin is needed, so handle
|
|
603
|
-
// this before readStdin() to avoid blocking on the ignored stdin pipe.
|
|
604
400
|
if (process.argv[2] === "upload") {
|
|
605
401
|
const jobFile = process.argv[3];
|
|
606
402
|
if (jobFile) {
|
|
@@ -608,10 +404,6 @@ async function main() {
|
|
|
608
404
|
}
|
|
609
405
|
process.exit(0);
|
|
610
406
|
}
|
|
611
|
-
// Deferred uploader child (spawned by runCaptureFinish when the per-turn
|
|
612
|
-
// Stop capture is inside the throttle window). Sleeps for the remaining
|
|
613
|
-
// delay, then claims the job file (if it hasn't been superseded by a newer
|
|
614
|
-
// turn) and uploads. No stdin needed — handle before readStdin().
|
|
615
407
|
if (process.argv[2] === "upload-deferred") {
|
|
616
408
|
const jobFile = process.argv[3];
|
|
617
409
|
const delayMs = Math.max(0, Number(process.argv[4]) || 0);
|
|
@@ -623,35 +415,11 @@ async function main() {
|
|
|
623
415
|
}
|
|
624
416
|
process.exit(0);
|
|
625
417
|
}
|
|
626
|
-
// Daily component drift scan runs in a detached hook process. The parent
|
|
627
|
-
// SessionStart path only performs a timestamp read, so agent startup never
|
|
628
|
-
// waits for filesystem traversal or an inventory upload.
|
|
629
|
-
if (process.argv[2] === "security-inventory-upload") {
|
|
630
|
-
const config = (0, utils_1.readConfig)();
|
|
631
|
-
if (config && (0, securityFeature_1.isAgentSecurityEnabled)(config)) {
|
|
632
|
-
const inventory = (0, securityInventory_1.buildSecurityInventory)({ workspace: process.argv[3] || process.cwd() });
|
|
633
|
-
try {
|
|
634
|
-
await (0, securityInventory_1.uploadSecurityInventory)(inventory, config);
|
|
635
|
-
}
|
|
636
|
-
catch { /* retry after the daily inventory window */ }
|
|
637
|
-
}
|
|
638
|
-
process.exit(0);
|
|
639
|
-
}
|
|
640
|
-
// Detached backup uploader child (spawned by runBackup below). The job file
|
|
641
|
-
// carries only { root, files }; the child re-reads config from disk. No stdin
|
|
642
|
-
// needed, so handle it before readStdin() like the capture uploader above.
|
|
643
418
|
if (process.argv[2] === "backup-upload") {
|
|
644
419
|
const { runBackupUpload } = await Promise.resolve().then(() => __importStar(require("./backup/run")));
|
|
645
420
|
await runBackupUpload(process.argv[3]);
|
|
646
421
|
process.exit(0);
|
|
647
422
|
}
|
|
648
|
-
// Detached capture-finish child (spawned by the SessionEnd capture path
|
|
649
|
-
// below). The job carries only the raw hook inputs; this child does ALL the
|
|
650
|
-
// heavy work — read the transcript, parse it, resolve git branch/SHAs (the
|
|
651
|
-
// per-SHA `git rev-parse` calls that were overrunning CC's shutdown window
|
|
652
|
-
// on Windows), build the payload, and dispatch the upload. Running here means
|
|
653
|
-
// none of it is on the SessionEnd shutdown clock, so it can never be
|
|
654
|
-
// "Hook cancelled". No stdin needed — handle it before readStdin().
|
|
655
423
|
if (process.argv[2] === "capture-finish") {
|
|
656
424
|
const jobFile = process.argv[3];
|
|
657
425
|
if (jobFile) {
|
|
@@ -660,26 +428,25 @@ async function main() {
|
|
|
660
428
|
await runCaptureFinish(job);
|
|
661
429
|
}
|
|
662
430
|
catch {
|
|
663
|
-
// best-effort — never throw out of the detached child
|
|
664
431
|
}
|
|
665
432
|
finally {
|
|
666
433
|
try {
|
|
667
434
|
fs.unlinkSync(jobFile);
|
|
668
435
|
}
|
|
669
436
|
catch {
|
|
670
|
-
/* ignore */
|
|
671
437
|
}
|
|
672
438
|
}
|
|
673
439
|
}
|
|
674
440
|
process.exit(0);
|
|
675
441
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
// the developer's action.
|
|
679
|
-
if (process.argv[2] === "security-upload") {
|
|
680
|
-
await runSecurityEventUpload();
|
|
442
|
+
if (process.argv[2] === "self-update") {
|
|
443
|
+
await (0, selfUpdate_1.runSelfUpdate)(process.argv[1]);
|
|
681
444
|
process.exit(0);
|
|
682
445
|
}
|
|
446
|
+
const migrated = process.env.CODIEDEV_VIA_LAUNCHER ? [] : (0, utils_1.migrateHooksToLauncher)();
|
|
447
|
+
if (migrated.length === 0)
|
|
448
|
+
(0, selfUpdate_1.maybeSpawnSelfUpdate)(process.argv[1]);
|
|
449
|
+
(0, selfUpdate_1.maybeRefreshInstructions)();
|
|
683
450
|
let hookInput = {};
|
|
684
451
|
try {
|
|
685
452
|
const raw = await readStdin();
|
|
@@ -687,64 +454,65 @@ async function main() {
|
|
|
687
454
|
const parsed = JSON.parse(raw);
|
|
688
455
|
hookInput = (process.argv[2]?.endsWith("-cursor")
|
|
689
456
|
? (0, cursorHooks_1.normalizeCursorHookInput)(parsed)
|
|
690
|
-
:
|
|
457
|
+
: process.argv[2]?.endsWith("-copilot")
|
|
458
|
+
? (0, copilotHooks_1.normalizeCopilotHookInput)(parsed)
|
|
459
|
+
: parsed);
|
|
691
460
|
}
|
|
692
461
|
}
|
|
693
462
|
catch {
|
|
694
463
|
process.exit(0);
|
|
695
464
|
}
|
|
696
|
-
|
|
697
|
-
// disable local security enforcement. Security hooks run first with telemetry
|
|
698
|
-
// disabled; all other hook modes still exit before making a network request.
|
|
699
|
-
const captureIgnored = (0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput);
|
|
700
|
-
const securityCommand = process.argv[2] ?? "";
|
|
701
|
-
const securityMatch = securityCommand.match(/^security-(pre|post|failure)(?:-(codex|cursor|codie))?$/);
|
|
702
|
-
if (securityMatch) {
|
|
703
|
-
const source = securityMatch[2] === "codex"
|
|
704
|
-
? "codex"
|
|
705
|
-
: securityMatch[2] === "cursor"
|
|
706
|
-
? "cursor"
|
|
707
|
-
: securityMatch[2] === "codie"
|
|
708
|
-
? "codie-harness"
|
|
709
|
-
: "claude-code";
|
|
710
|
-
await runSecurityHook(hookInput, securityMatch[1], source, !captureIgnored);
|
|
465
|
+
if (process.argv[2]?.startsWith("security-"))
|
|
711
466
|
process.exit(0);
|
|
712
|
-
|
|
713
|
-
// UserPromptSubmit discovery path — separate from session capture.
|
|
467
|
+
const captureIgnored = (0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput);
|
|
714
468
|
const promptCommand = process.argv[2] ?? "";
|
|
715
|
-
if (promptCommand === "security-prompt-cursor") {
|
|
716
|
-
if (runPromptSecurityHook(hookInput, "cursor", !captureIgnored))
|
|
717
|
-
return;
|
|
718
|
-
if (captureIgnored)
|
|
719
|
-
return;
|
|
720
|
-
dispatchAllowedPromptSecurityEvent(hookInput, "cursor");
|
|
721
|
-
process.exit(0);
|
|
722
|
-
}
|
|
723
469
|
if (process.argv[2] === "discover" ||
|
|
724
470
|
process.argv[2] === "discover-codex" ||
|
|
725
|
-
process.argv[2] === "discover-codie"
|
|
471
|
+
process.argv[2] === "discover-codie" ||
|
|
472
|
+
process.argv[2] === "discover-copilot" ||
|
|
473
|
+
process.argv[2] === "discover-pi") {
|
|
726
474
|
const source = promptCommand === "discover-codex"
|
|
727
475
|
? "codex"
|
|
728
476
|
: promptCommand === "discover-codie"
|
|
729
477
|
? "codie-harness"
|
|
730
|
-
: "
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
478
|
+
: promptCommand === "discover-copilot"
|
|
479
|
+
? "copilot"
|
|
480
|
+
: promptCommand === "discover-pi"
|
|
481
|
+
? "pi"
|
|
482
|
+
: "claude-code";
|
|
735
483
|
if (captureIgnored)
|
|
736
484
|
return;
|
|
737
|
-
dispatchAllowedPromptSecurityEvent(hookInput, source);
|
|
738
|
-
// A Codex block leaves no hook of its own, so this prompt is the first
|
|
739
|
-
// chance to report it. Runs before runDiscover, which exits the process.
|
|
740
485
|
if (source === "codex")
|
|
741
486
|
await runCodexLimitSweep(hookInput.session_id);
|
|
742
|
-
|
|
487
|
+
if (source === "codex")
|
|
488
|
+
(0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
|
|
489
|
+
if (source !== "copilot")
|
|
490
|
+
await runDiscover(hookInput.prompt);
|
|
743
491
|
return;
|
|
744
492
|
}
|
|
745
493
|
if (captureIgnored)
|
|
746
494
|
process.exit(0);
|
|
747
|
-
|
|
495
|
+
if (["session-start-cursor", "backup-cursor", "capture-cursor", "observe-cursor"].includes(process.argv[2])) {
|
|
496
|
+
try {
|
|
497
|
+
(0, cursorLifecycle_1.recordCursorLifecycle)(hookInput, process.argv[2] === "capture-cursor");
|
|
498
|
+
}
|
|
499
|
+
catch { }
|
|
500
|
+
}
|
|
501
|
+
if (process.argv[2] === "observe-cursor") {
|
|
502
|
+
process.stdout.write("{}");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (process.argv[2] === "limit-copilot") {
|
|
506
|
+
const config = (0, utils_1.readConfig)();
|
|
507
|
+
const event = (0, limitEvent_1.parseNativeLimitError)(hookInput);
|
|
508
|
+
if (config && event) {
|
|
509
|
+
try {
|
|
510
|
+
await (0, limitEventReporter_1.postLimitEvent)(config, { ...event, source: "copilot", sessionId: hookInput.session_id });
|
|
511
|
+
}
|
|
512
|
+
catch { }
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
748
516
|
if (process.argv[2] === "limit") {
|
|
749
517
|
await runStopFailure(hookInput);
|
|
750
518
|
return;
|
|
@@ -753,109 +521,100 @@ async function main() {
|
|
|
753
521
|
await runCodexLimit(hookInput);
|
|
754
522
|
return;
|
|
755
523
|
}
|
|
756
|
-
// SessionStart: post the capture-coverage denominator ping. Tiny + fast.
|
|
757
524
|
if (process.argv[2] === "session-start" ||
|
|
758
525
|
process.argv[2] === "session-start-cursor" ||
|
|
759
|
-
process.argv[2] === "session-start-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
child.unref();
|
|
767
|
-
}
|
|
768
|
-
catch { /* daily inventory is best-effort */ }
|
|
769
|
-
}
|
|
526
|
+
process.argv[2] === "session-start-copilot" ||
|
|
527
|
+
process.argv[2] === "session-start-opencode" ||
|
|
528
|
+
process.argv[2] === "session-start-pi") {
|
|
529
|
+
(0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
|
|
530
|
+
const packNag = process.argv[2] === "session-start-opencode" || process.argv[2] === "session-start-pi" ? null : (0, sync_1.packNagLine)();
|
|
531
|
+
if (packNag && process.argv[2] === "session-start")
|
|
532
|
+
process.stdout.write(`${packNag}\n`);
|
|
770
533
|
await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
|
|
771
534
|
? "opencode"
|
|
772
535
|
: process.argv[2] === "session-start-cursor"
|
|
773
536
|
? "cursor"
|
|
774
|
-
: "
|
|
537
|
+
: process.argv[2] === "session-start-copilot"
|
|
538
|
+
? "copilot"
|
|
539
|
+
: process.argv[2] === "session-start-pi"
|
|
540
|
+
? "pi"
|
|
541
|
+
: "claude-code");
|
|
542
|
+
if ((0, utils_1.readConfig)() && (process.argv[2] === "session-start-cursor" || process.argv[2] === "session-start-copilot")) {
|
|
543
|
+
const source = process.argv[2] === "session-start-cursor" ? "cursor" : "copilot";
|
|
544
|
+
const { TEAM_SKILL_CONTEXT, teamSkillSessionOutput } = await Promise.resolve().then(() => __importStar(require("./teamSkillContext")));
|
|
545
|
+
const prompt = hookInput.initialPrompt
|
|
546
|
+
?? hookInput.initial_prompt;
|
|
547
|
+
const discovered = typeof prompt === "string" ? await discoverContext(prompt) : null;
|
|
548
|
+
process.stdout.write(JSON.stringify(teamSkillSessionOutput(source, [TEAM_SKILL_CONTEXT, discovered, packNag].filter(Boolean).join("\n\n"))));
|
|
549
|
+
}
|
|
775
550
|
return;
|
|
776
551
|
}
|
|
777
|
-
// Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets
|
|
778
|
-
// every codiedev hook — INCLUDING SessionEnd→capture, which previously had
|
|
779
|
-
// no self-heal and could stay stripped forever while SessionStart kept
|
|
780
|
-
// pinging (the Sonifi "starts arrive, captures never" loss) — re-wired on
|
|
781
|
-
// their next Stop/SessionEnd. One read + one write, only when missing.
|
|
782
552
|
if ((process.argv[2] ?? "").endsWith("-cursor")) {
|
|
783
553
|
(0, utils_1.ensureCursorCodiedevHooksInstalled)();
|
|
784
554
|
}
|
|
555
|
+
else if ((process.argv[2] ?? "").endsWith("-copilot")) {
|
|
556
|
+
(0, utils_1.ensureCopilotCodiedevHooksInstalled)();
|
|
557
|
+
}
|
|
558
|
+
else if (/-pi(?:-final)?$/.test(process.argv[2] ?? "")) {
|
|
559
|
+
(0, utils_1.ensurePiExtensionInstalled)();
|
|
560
|
+
}
|
|
785
561
|
else {
|
|
786
562
|
(0, utils_1.ensureAllCodiedevHooksInstalled)();
|
|
787
563
|
}
|
|
788
564
|
if (process.argv[2] === "capture-codex")
|
|
789
565
|
(0, utils_1.ensureCodexLimitHookInstalled)();
|
|
566
|
+
if (/-opencode(?:-final)?$/.test(process.argv[2] ?? ""))
|
|
567
|
+
(0, utils_1.ensureOpenCodePluginCurrent)();
|
|
790
568
|
const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
|
|
569
|
+
const launch = (0, launchContext_1.hookLaunch)(process.argv[2], hookInput, {
|
|
570
|
+
copilotClientName: process.argv[2]?.endsWith("-copilot")
|
|
571
|
+
? (0, copilotHooks_1.readCopilotWorkspace)(transcript_path).clientName
|
|
572
|
+
: undefined,
|
|
573
|
+
});
|
|
791
574
|
const workspaceRoot = workspace_roots && workspace_roots.length > 0
|
|
792
575
|
? workspace_roots[0]
|
|
793
576
|
: cwd || process.cwd();
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
// detached child that backs them up privately. Best-effort, never blocks.
|
|
797
|
-
//
|
|
798
|
-
// Per-turn capture: Stop fires reliably in editor-embedded clients (VS Code
|
|
799
|
-
// extension) where SessionEnd may never come — the silent zero-capture bug.
|
|
800
|
-
// Same detached capture-finish child as SessionEnd, marked non-final so the
|
|
801
|
-
// server renews the session lease instead of summarizing immediately.
|
|
802
|
-
if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor") {
|
|
803
|
-
if (transcript_path) {
|
|
577
|
+
if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor" || process.argv[2] === "backup-copilot" || process.argv[2] === "backup-pi") {
|
|
578
|
+
if (transcript_path && !(0, utils_1.isCodexRolloutPath)(transcript_path)) {
|
|
804
579
|
try {
|
|
805
580
|
(0, utils_1.dispatchCaptureFinish)({
|
|
806
|
-
mode: process.argv[2] === "backup-cursor"
|
|
581
|
+
mode: process.argv[2] === "backup-cursor"
|
|
582
|
+
? "capture-cursor"
|
|
583
|
+
: process.argv[2] === "backup-copilot"
|
|
584
|
+
? "capture-copilot"
|
|
585
|
+
: process.argv[2] === "backup-pi"
|
|
586
|
+
? "capture-pi"
|
|
587
|
+
: "capture",
|
|
807
588
|
session_id,
|
|
808
589
|
transcript_path,
|
|
809
590
|
cwd,
|
|
810
591
|
workspace_roots,
|
|
811
|
-
|
|
592
|
+
...(launch ? { launch } : {}),
|
|
812
593
|
final: false,
|
|
813
594
|
}, process.argv[1], {
|
|
814
595
|
spawn: child_process_1.spawn,
|
|
815
596
|
writeFileSync: fs.writeFileSync,
|
|
816
597
|
openSync: fs.openSync,
|
|
817
598
|
closeSync: fs.closeSync,
|
|
818
|
-
// UNIQUE per dispatch (sid+pid+ts): Stop and SessionEnd children
|
|
819
|
-
// each own their input file exclusively — a shared per-session
|
|
820
|
-
// name let one child's `finally` unlink eat the other's input
|
|
821
|
-
// (lost final:true) or double-dispatch. See finishInputTmpFile.
|
|
822
599
|
tmpFile: (0, utils_1.finishInputTmpFile)(session_id),
|
|
823
600
|
});
|
|
824
601
|
}
|
|
825
602
|
catch {
|
|
826
|
-
// capture is best-effort; never break backup
|
|
827
603
|
}
|
|
828
604
|
}
|
|
829
605
|
const { runBackup } = await Promise.resolve().then(() => __importStar(require("./backup/run")));
|
|
830
606
|
runBackup(workspaceRoot, process.argv[1]);
|
|
831
607
|
process.exit(0);
|
|
832
608
|
}
|
|
833
|
-
// Capture path (SessionEnd). Do the ABSOLUTE MINIMUM inline so this hook
|
|
834
|
-
// exits 0 within Claude Code's SessionEnd shutdown window: a cheap config /
|
|
835
|
-
// transcript guard, then hand ALL the heavy work (transcript read + parse +
|
|
836
|
-
// per-SHA `git rev-parse` + upload) to a detached `capture-finish` child.
|
|
837
|
-
// Detaching only the upload (the old #240 fix) left the synchronous parse and
|
|
838
|
-
// git work on the shutdown clock, which still overran on Windows — where each
|
|
839
|
-
// subprocess spawn costs ~50-150ms — producing "Hook cancelled" even though
|
|
840
|
-
// the data still landed. JS can't be interrupted mid-sync-chain, so the work
|
|
841
|
-
// finished and the upload went through, but CC had already given up waiting.
|
|
842
609
|
const config = (0, utils_1.readConfig)();
|
|
843
610
|
if (!config) {
|
|
844
|
-
// No config = no token; nothing can be sent, not even a skip ping.
|
|
845
611
|
process.exit(0);
|
|
846
612
|
}
|
|
847
613
|
if (!transcript_path) {
|
|
848
|
-
// Nothing to capture — but say so, instead of leaving a start ping with
|
|
849
|
-
// no matching capture (silent coverage loss). One tiny POST; the rare
|
|
850
|
-
// no-transcript case has no other work on the shutdown clock.
|
|
851
614
|
await postSessionSkip(config, session_id, "no-transcript-path");
|
|
852
615
|
process.exit(0);
|
|
853
616
|
}
|
|
854
617
|
try {
|
|
855
|
-
// "codiedev-finish-" prefix (NOT "codiedev-capture-") so the sweep regex
|
|
856
|
-
// /^codiedev-capture-.*\.json$/ never mistakes a finish-input file for an
|
|
857
|
-
// upload job. UNIQUE per dispatch (sid+pid+ts) so a concurrent Stop-hook
|
|
858
|
-
// child's `finally` unlink can never eat this input (lost final:true).
|
|
859
618
|
const jobFile = (0, utils_1.finishInputTmpFile)(session_id);
|
|
860
619
|
(0, utils_1.dispatchCaptureFinish)({
|
|
861
620
|
mode: process.argv[2] ?? "capture",
|
|
@@ -863,9 +622,7 @@ async function main() {
|
|
|
863
622
|
transcript_path,
|
|
864
623
|
cwd,
|
|
865
624
|
workspace_roots,
|
|
866
|
-
|
|
867
|
-
// Codex routes its per-turn Stop hook through "capture-codex" —
|
|
868
|
-
// final:true there would close the server lease every turn.
|
|
625
|
+
...(launch ? { launch } : {}),
|
|
869
626
|
final: (0, utils_1.resolveCaptureFinal)(process.argv[2]),
|
|
870
627
|
}, process.argv[1], { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile: jobFile, openSync: fs.openSync, closeSync: fs.closeSync });
|
|
871
628
|
}
|
|
@@ -874,19 +631,10 @@ async function main() {
|
|
|
874
631
|
}
|
|
875
632
|
process.exit(0);
|
|
876
633
|
}
|
|
877
|
-
// Runs inside the detached `capture-finish` child spawned by the SessionEnd
|
|
878
|
-
// hook above. This is everything that used to run inline in main(): read the
|
|
879
|
-
// transcript, resolve the repo, parse stats/usage/steering, resolve the git
|
|
880
|
-
// branch + authored SHAs (the per-SHA `git rev-parse` calls that overran the
|
|
881
|
-
// shutdown window on Windows), build the payload, and fire the upload from a
|
|
882
|
-
// further-detached uploader. Running here means none of it is on the SessionEnd
|
|
883
|
-
// shutdown clock, so it can never be "Hook cancelled". Best-effort throughout.
|
|
884
634
|
async function runCaptureFinish(input) {
|
|
885
635
|
const config = (0, utils_1.readConfig)();
|
|
886
636
|
if (!config)
|
|
887
637
|
return;
|
|
888
|
-
// Re-check in the detached child: the user may have added an exclusion
|
|
889
|
-
// after the parent hook dispatched this job but before it started.
|
|
890
638
|
if ((0, captureExclusions_1.shouldIgnoreCaptureInput)(config, input)) {
|
|
891
639
|
(0, captureLog_1.appendCaptureLog)({
|
|
892
640
|
stage: "capture-finish",
|
|
@@ -901,8 +649,6 @@ async function runCaptureFinish(input) {
|
|
|
901
649
|
await postSessionSkip(config, session_id, "no-transcript-path");
|
|
902
650
|
return;
|
|
903
651
|
}
|
|
904
|
-
// Log that this child has started processing — the first diagnostic proof
|
|
905
|
-
// that the detached capture-finish child actually ran (visible in doctor).
|
|
906
652
|
(0, captureLog_1.appendCaptureLog)({ stage: "capture-finish", sessionId: session_id, outcome: "start" });
|
|
907
653
|
try {
|
|
908
654
|
await runCaptureFinishInner(input, config, session_id, transcript_path, cwd, workspace_roots);
|
|
@@ -918,10 +664,6 @@ async function runCaptureFinish(input) {
|
|
|
918
664
|
}
|
|
919
665
|
async function runCaptureFinishInner(input, config, session_id, transcript_path, cwd, workspace_roots) {
|
|
920
666
|
const mode = resolveMode(input.mode);
|
|
921
|
-
// Desktop transcripts (opencode SDK {info,parts}[] dumps) are byte-identical
|
|
922
|
-
// to opencode's, so every parser below EXCEPT parseUsage treats desktop
|
|
923
|
-
// exactly as opencode via this mapping. parseUsage is the one exception —
|
|
924
|
-
// see the rawTranscriptContent note below.
|
|
925
667
|
const parserMode = mode === "desktop" ? "opencode" : mode;
|
|
926
668
|
const workspaceRoot = workspace_roots && workspace_roots.length > 0
|
|
927
669
|
? workspace_roots[0]
|
|
@@ -930,11 +672,14 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
930
672
|
let transcriptContent;
|
|
931
673
|
try {
|
|
932
674
|
transcriptContent = fs.readFileSync(transcript_path, "utf8");
|
|
675
|
+
if (mode === "copilot" && input.final === true && !(0, copilotHooks_1.hasCopilotShutdown)(transcriptContent)) {
|
|
676
|
+
for (let waited = 0; waited < COPILOT_SHUTDOWN_WAIT_MS && !(0, copilotHooks_1.hasCopilotShutdown)(transcriptContent); waited += 250) {
|
|
677
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
678
|
+
transcriptContent = fs.readFileSync(transcript_path, "utf8");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
933
681
|
}
|
|
934
682
|
catch (err) {
|
|
935
|
-
// ENOENT → "no-transcript" (phantom desktop-app helper session, excluded
|
|
936
|
-
// from coverage server-side); anything else → "transcript-unreadable:<code>"
|
|
937
|
-
// (real transcript, read failed — a genuine, diagnosable loss).
|
|
938
683
|
const reason = (0, utils_1.transcriptReadSkipReason)(err);
|
|
939
684
|
(0, captureLog_1.appendCaptureLog)({
|
|
940
685
|
stage: "capture-finish",
|
|
@@ -947,98 +692,93 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
947
692
|
}
|
|
948
693
|
return;
|
|
949
694
|
}
|
|
950
|
-
// Snapshot the RAW transcript before normalization below flattens it to
|
|
951
|
-
// Claude Code's newline-JSONL shape — that flattening drops info.cost /
|
|
952
|
-
// info.providerID, which desktop's parseUsage (below) needs and opencode's
|
|
953
|
-
// doesn't. Every other mode leaves transcriptContent untouched here, so this
|
|
954
|
-
// is a no-op for them.
|
|
955
695
|
const rawTranscriptContent = transcriptContent;
|
|
696
|
+
let copilotSubagents;
|
|
697
|
+
const copilotWorkspace = mode === "copilot" ? (0, copilotHooks_1.readCopilotWorkspace)(transcript_path) : {};
|
|
956
698
|
if (parserMode === "opencode") {
|
|
957
699
|
transcriptContent = (0, utils_1.normalizeOpenCodeTranscript)(transcriptContent);
|
|
958
700
|
}
|
|
959
701
|
else if (parserMode === "cursor") {
|
|
960
702
|
transcriptContent = (0, cursorHooks_1.normalizeCursorTranscript)(transcriptContent);
|
|
703
|
+
const observations = (0, cursorLifecycle_1.cursorLifecycleRecords)(session_id);
|
|
704
|
+
if (observations)
|
|
705
|
+
transcriptContent += `\n${observations}`;
|
|
706
|
+
}
|
|
707
|
+
else if (parserMode === "copilot") {
|
|
708
|
+
const normalized = (0, copilotHooks_1.normalizeCopilotTranscript)(transcriptContent, { clientName: copilotWorkspace.clientName });
|
|
709
|
+
transcriptContent = normalized.content;
|
|
710
|
+
copilotSubagents = normalized.subagents.count > 0 ? normalized.subagents : undefined;
|
|
711
|
+
}
|
|
712
|
+
else if (parserMode === "pi") {
|
|
713
|
+
transcriptContent = (0, piHooks_1.normalizePiTranscript)(transcriptContent);
|
|
714
|
+
}
|
|
715
|
+
if (mode === "cursor" || mode === "copilot" || mode === "pi") {
|
|
716
|
+
try {
|
|
717
|
+
await (0, limitEventReporter_1.reportTranscriptLimitEvent)({ config, source: mode, sessionId: session_id, transcript: transcriptContent });
|
|
718
|
+
}
|
|
719
|
+
catch { }
|
|
961
720
|
}
|
|
962
|
-
// Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
|
|
963
|
-
// need the git remote of the workspace.
|
|
964
721
|
let remoteUrl = null;
|
|
965
722
|
if (parserMode === "codex") {
|
|
966
723
|
remoteUrl = (0, utils_1.extractCodexRepoUrl)(transcriptContent);
|
|
967
724
|
}
|
|
968
|
-
// The worktrees the session actually wrote to — resolved up front because
|
|
969
|
-
// they both anchor the branch/sha join keys below AND back-fill the repo tag
|
|
970
|
-
// when the launch dir is a multi-repo WORKSPACE root (a parent folder of
|
|
971
|
-
// several cloned repos) that has no remote of its own.
|
|
972
725
|
const captureDirs = (0, utils_1.resolveCaptureDirs)(transcriptContent, workspaceRoot);
|
|
973
726
|
if (!remoteUrl) {
|
|
974
727
|
remoteUrl = (0, utils_1.resolveCaptureRemoteUrl)(workspaceRoot, captureDirs);
|
|
975
728
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
// for users whose workspace couldn't resolve a remote (no git binary on lean
|
|
979
|
-
// Alpine images, no `origin`, or a submodule whose parent .git isn't mounted),
|
|
980
|
-
// with no error and a green `doctor`. Upload regardless; the backend accepts
|
|
981
|
-
// an unmatched capture (repoId null) and surfaces it for triage.
|
|
982
|
-
//
|
|
983
|
-
// Best-effort local match — backend re-resolves from `remoteUrl` against the
|
|
984
|
-
// company's current repo list, so a stale/empty local cache doesn't matter.
|
|
729
|
+
if (remoteUrl)
|
|
730
|
+
remoteUrl = (0, utils_1.stripRepoUrlCredentials)(remoteUrl);
|
|
985
731
|
const matchedRepo = remoteUrl ? (0, utils_1.matchRepo)(remoteUrl, config.repos) : null;
|
|
986
732
|
const stats = parserMode === "codex"
|
|
987
733
|
? (0, utils_1.parseCodexStats)(transcriptContent)
|
|
988
734
|
: (0, utils_1.parseClaudeCodeStats)(transcriptContent);
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
735
|
+
const emptyReason = parserMode === "cursor" && (0, cursorHooks_1.isCursorTranscriptEmpty)(rawTranscriptContent)
|
|
736
|
+
? "empty-cursor-transcript"
|
|
737
|
+
: parserMode === "copilot" && (0, copilotHooks_1.isCopilotTranscriptEmpty)(rawTranscriptContent)
|
|
738
|
+
? "empty-copilot-transcript"
|
|
739
|
+
: parserMode === "pi" && (0, piHooks_1.isPiTranscriptEmpty)(rawTranscriptContent)
|
|
740
|
+
? "empty-pi-transcript"
|
|
741
|
+
: null;
|
|
742
|
+
if (emptyReason) {
|
|
994
743
|
(0, captureLog_1.appendCaptureLog)({
|
|
995
744
|
stage: "capture-finish",
|
|
996
745
|
sessionId: session_id,
|
|
997
746
|
outcome: "skip",
|
|
998
|
-
detail:
|
|
747
|
+
detail: emptyReason,
|
|
999
748
|
});
|
|
1000
|
-
// A non-final stop snapshot is only a backup; reporting it as a skipped
|
|
1001
|
-
// session would close/remove server-side start coverage while the session
|
|
1002
|
-
// is still alive. The real SessionEnd remains responsible for a skip ping.
|
|
1003
749
|
if ((0, utils_1.shouldReportCaptureSkip)(input.final)) {
|
|
1004
|
-
await postSessionSkip(config, session_id,
|
|
750
|
+
await postSessionSkip(config, session_id, emptyReason);
|
|
1005
751
|
}
|
|
1006
752
|
return;
|
|
1007
753
|
}
|
|
1008
754
|
const transcriptBase64 = Buffer.from(transcriptContent, "utf8").toString("base64");
|
|
1009
|
-
// Session→merge join keys: the branch + HEAD sha the work happened on. Matched
|
|
1010
|
-
// server-side against a merged MR/PR's source_branch / commits for AI attribution.
|
|
1011
|
-
// Capture from the worktree the agent actually worked in (the launch dir stays
|
|
1012
|
-
// on main/detached when work happens in a `git worktree`), not workspaceRoot.
|
|
1013
|
-
// One session can touch several worktrees (→ several PRs); capture EVERY
|
|
1014
|
-
// worktree's branch+sha so all the resulting PRs attribute. branch/headSha
|
|
1015
|
-
// remain the primary (most-recent) values for back-compat.
|
|
1016
755
|
const keys = (0, utils_1.collectCaptureKeys)(captureDirs);
|
|
1017
756
|
const gitRefs = (0, utils_1.collectCaptureGitRefs)(captureDirs);
|
|
1018
|
-
// Union the worktree-dir branches with the branches the agent actually
|
|
1019
|
-
// committed/pushed to (parsed from the transcript's git output). Like the
|
|
1020
|
-
// SHAs below, these are independent of end-of-session filesystem state, so
|
|
1021
|
-
// they rescue branch-join when the worktree was REMOVED before SessionEnd
|
|
1022
|
-
// (the dominant miss: dir-resolution falls back to the parent's branch, never
|
|
1023
|
-
// the `worktree-<name>` one the PR was opened from). Indexed server-side under
|
|
1024
|
-
// every branch in the array, so the worktree's PR attributes regardless.
|
|
1025
757
|
const authoredBranches = (0, utils_1.parseAuthoredBranches)(transcriptContent);
|
|
1026
|
-
const
|
|
758
|
+
const recordedBranches = copilotWorkspace.branch ? [copilotWorkspace.branch] : [];
|
|
759
|
+
const branches = [...new Set([...keys.branches, ...authoredBranches, ...recordedBranches])];
|
|
1027
760
|
const branch = keys.branch ?? branches[0] ?? null;
|
|
1028
|
-
// Union the worktree HEAD shas with the SHAs the agent actually committed/
|
|
1029
|
-
// pushed (parsed from the transcript's git output, resolved via the shared
|
|
1030
|
-
// object store). The authored shas are independent of end-of-session
|
|
1031
|
-
// filesystem state, so they rescue attribution when a worktree is stale/gone.
|
|
1032
761
|
const authoredShas = (0, utils_1.parseAuthoredShas)(transcriptContent, workspaceRoot);
|
|
1033
762
|
const headShas = [...new Set([...keys.headShas, ...authoredShas])];
|
|
1034
763
|
const headSha = keys.headSha ?? headShas[0] ?? null;
|
|
1035
764
|
const pullRequestUrls = (0, utils_1.parsePullRequestUrls)(transcriptContent);
|
|
1036
|
-
// Cost/velocity/steering signals parsed straight from the transcript we're
|
|
1037
|
-
// already holding — no extra hooks, no extra git/process calls. parseUsage
|
|
1038
|
-
// keeps the literal mode (it has its own desktop parser) and reads the RAW
|
|
1039
|
-
// pre-normalization content for desktop; parseSteering routes through
|
|
1040
|
-
// parserMode like the parsers above.
|
|
1041
765
|
const usage = (0, utils_1.parseUsage)(mode === "desktop" ? rawTranscriptContent : transcriptContent, mode);
|
|
766
|
+
const subagents = mode === "claude-code"
|
|
767
|
+
? (0, subagents_1.readSubagentTranscripts)(transcript_path, session_id)
|
|
768
|
+
: { contents: [], count: 0, truncated: false };
|
|
769
|
+
const subagentUsage = (0, subagents_1.foldSubagentModelUsage)([], subagents, mode);
|
|
770
|
+
for (const u of subagentUsage) {
|
|
771
|
+
usage.inputTokens = (usage.inputTokens ?? 0) + u.inputTokens;
|
|
772
|
+
usage.outputTokens = (usage.outputTokens ?? 0) + u.outputTokens;
|
|
773
|
+
usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + u.cacheReadTokens;
|
|
774
|
+
usage.cacheCreationTokens = (usage.cacheCreationTokens ?? 0) + u.cacheCreationTokens;
|
|
775
|
+
usage.cacheTokens = (usage.cacheTokens ?? 0) + u.cacheReadTokens + u.cacheCreationTokens;
|
|
776
|
+
}
|
|
777
|
+
const hasUsage = mode === "cursor" ? (0, cursorHooks_1.cursorTranscriptHasUsage)(transcriptContent)
|
|
778
|
+
: mode === "copilot" ? (0, transcriptModels_1.parseTranscriptModels)(transcriptContent, mode).some(model => model.tokensAvailable !== false)
|
|
779
|
+
: true;
|
|
780
|
+
if (mode === "cursor" && !usage.model)
|
|
781
|
+
usage.model = (0, cursorHooks_1.cursorObservedModel)(transcriptContent) ?? null;
|
|
1042
782
|
const sessionStartedAt = (0, utils_1.parseSessionStart)(transcriptContent);
|
|
1043
783
|
const steering = (0, utils_1.parseSteering)(transcriptContent, parserMode);
|
|
1044
784
|
const payload = (0, utils_1.buildCapturePayload)({
|
|
@@ -1057,33 +797,26 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
1057
797
|
headShas,
|
|
1058
798
|
gitRefs,
|
|
1059
799
|
pullRequestUrls,
|
|
1060
|
-
inputTokens: usage.inputTokens,
|
|
1061
|
-
outputTokens: usage.outputTokens,
|
|
1062
|
-
cacheTokens: usage.cacheTokens,
|
|
1063
|
-
cacheReadTokens: usage.cacheReadTokens,
|
|
1064
|
-
cacheCreationTokens: usage.cacheCreationTokens,
|
|
800
|
+
inputTokens: hasUsage ? usage.inputTokens : undefined,
|
|
801
|
+
outputTokens: hasUsage ? usage.outputTokens : undefined,
|
|
802
|
+
cacheTokens: hasUsage ? usage.cacheTokens : undefined,
|
|
803
|
+
cacheReadTokens: hasUsage ? usage.cacheReadTokens : undefined,
|
|
804
|
+
cacheCreationTokens: hasUsage ? usage.cacheCreationTokens : undefined,
|
|
1065
805
|
model: usage.model,
|
|
1066
|
-
captureSignals: (0, captureSignals_1.parseCaptureSignals)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode),
|
|
1067
|
-
modelUsage: (0, transcriptModels_1.parseTranscriptModels)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode),
|
|
806
|
+
captureSignals: withCopilotSubagents((0, subagents_1.foldSubagentSignals)((0, captureSignals_1.withLaunch)((0, captureSignals_1.parseCaptureSignals)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode), input.launch), subagents, mode), copilotSubagents),
|
|
807
|
+
modelUsage: (0, subagents_1.foldSubagentModelUsage)((0, transcriptModels_1.parseTranscriptModels)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode), subagents, mode),
|
|
1068
808
|
sessionStartedAt,
|
|
1069
809
|
interruptCount: steering.interrupts,
|
|
1070
810
|
rejectionCount: steering.rejections,
|
|
1071
|
-
// Carry the final flag into the HTTP body — the server uses it to decide
|
|
1072
|
-
// whether to close the session lease (true) or renew it (false/absent).
|
|
1073
811
|
final: input.final,
|
|
1074
|
-
// Desktop-only measured spend (undefined for every other source).
|
|
1075
812
|
costUsd: usage.costUsd,
|
|
1076
813
|
});
|
|
1077
|
-
// Throttle decision: final captures go immediately; non-final (per-turn Stop)
|
|
1078
|
-
// are deferred if we uploaded this session recently, to avoid per-turn churn.
|
|
1079
814
|
const uploadState = (0, utils_1.readSessionUploadState)(session_id ?? "");
|
|
1080
815
|
const decision = (0, utils_1.decideDispatch)({
|
|
1081
816
|
final: input.final === true,
|
|
1082
817
|
lastUploadAt: uploadState?.lastUploadAt ?? null,
|
|
1083
818
|
now: Date.now(),
|
|
1084
819
|
});
|
|
1085
|
-
// Log capture-finish outcome with dispatch context so doctor can see whether
|
|
1086
|
-
// the upload went immediately or was deferred (and by how much).
|
|
1087
820
|
(0, captureLog_1.appendCaptureLog)({
|
|
1088
821
|
stage: "capture-finish",
|
|
1089
822
|
sessionId: session_id,
|
|
@@ -1092,11 +825,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
1092
825
|
? `dispatch=deferred delayMs=${decision.delayMs} final=${input.final === true} repo=${remoteUrl ?? "-"} branch=${branch ?? "-"} msgs=${stats.messageCount}`
|
|
1093
826
|
: `dispatch=immediate final=${input.final === true} repo=${remoteUrl ?? "-"} branch=${branch ?? "-"} msgs=${stats.messageCount}`,
|
|
1094
827
|
});
|
|
1095
|
-
// Fire the upload from a further-detached child. We're already off the
|
|
1096
|
-
// shutdown clock here, but reusing dispatchCaptureDetached keeps the upload
|
|
1097
|
-
// retry + last-capture breadcrumb logic (runCaptureUpload) intact verbatim.
|
|
1098
|
-
// Per-session job file: one file per session; newer turns overwrite it,
|
|
1099
|
-
// which is the "supersede" mechanism for the deferred path.
|
|
1100
828
|
try {
|
|
1101
829
|
const hookScriptPath = process.argv[1];
|
|
1102
830
|
const tmpFile = (0, utils_1.captureUploadTmpFile)(session_id, decision.mode);
|
|
@@ -1107,21 +835,8 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
1107
835
|
body: payload,
|
|
1108
836
|
scopePaths,
|
|
1109
837
|
}, hookScriptPath, { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile, openSync: fs.openSync, closeSync: fs.closeSync });
|
|
1110
|
-
// The immediate payload is durable before removing the older scheduled
|
|
1111
|
-
// snapshot. Do this for every immediate dispatch, not only SessionEnd:
|
|
1112
|
-
// first/outside-window snapshots also supersede a sleeping deferred job.
|
|
1113
838
|
if (session_id)
|
|
1114
839
|
(0, utils_1.cancelDeferredCaptureUpload)(session_id);
|
|
1115
|
-
// Retry any earlier capture jobs that failed to upload (transient network
|
|
1116
|
-
// blips), excluding the one we just dispatched. Best-effort, detached.
|
|
1117
|
-
// Skip the sweep on deferred path — the deferred child will handle uploading.
|
|
1118
|
-
//
|
|
1119
|
-
// Orphan pings are COLLECTED and awaited below: this child process calls
|
|
1120
|
-
// process.exit(0) as soon as runCaptureFinish resolves, so a
|
|
1121
|
-
// fire-and-forget POST here would be killed mid-flight — after the
|
|
1122
|
-
// orphan file was already unlinked, i.e. the loss would stay invisible
|
|
1123
|
-
// forever. The await is off the shutdown clock (detached child) and
|
|
1124
|
-
// bounded by PING_TIMEOUT_MS.
|
|
1125
840
|
const orphanPings = [];
|
|
1126
841
|
(0, utils_1.sweepPendingCaptures)({
|
|
1127
842
|
dir: os.tmpdir(),
|
|
@@ -1133,9 +848,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
1133
848
|
exclude: tmpFile,
|
|
1134
849
|
openSync: fs.openSync,
|
|
1135
850
|
closeSync: fs.closeSync,
|
|
1136
|
-
// Orphaned finish-inputs (capture-finish child died before writing an
|
|
1137
|
-
// upload job): unrecoverable payload, but report the loss so it lands
|
|
1138
|
-
// as a skip-reason instead of a silent coverage hole.
|
|
1139
851
|
unlinkSync: fs.unlinkSync,
|
|
1140
852
|
reportOrphan: (sid) => {
|
|
1141
853
|
orphanPings.push(postSessionSkip(config, sid, "finish-orphaned"));
|
|
@@ -1144,9 +856,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
1144
856
|
await Promise.all(orphanPings);
|
|
1145
857
|
}
|
|
1146
858
|
else {
|
|
1147
|
-
// Deferred: write the job file (newer turns overwrite = supersede), then
|
|
1148
|
-
// spawn the deferred-upload child which will sleep, claim, and upload.
|
|
1149
|
-
// Same stdio/fd-hygiene contract as dispatchCaptureDetached.
|
|
1150
859
|
(0, utils_1.dispatchDeferredUpload)({
|
|
1151
860
|
url: `${config.backendUrl}/api/captureSession`,
|
|
1152
861
|
token: config.token,
|
|
@@ -1159,6 +868,10 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
1159
868
|
process.stderr.write(`[codiedev-hook] Warning: Failed to dispatch session capture — ${err.message}\n`);
|
|
1160
869
|
}
|
|
1161
870
|
}
|
|
871
|
+
const COPILOT_SHUTDOWN_WAIT_MS = 5000;
|
|
872
|
+
function withCopilotSubagents(signals, subagents) {
|
|
873
|
+
return subagents ? { ...signals, subagents } : signals;
|
|
874
|
+
}
|
|
1162
875
|
main().catch(() => {
|
|
1163
876
|
process.exit(0);
|
|
1164
877
|
});
|