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