codiedev 0.9.59 → 0.9.61
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/dist/captureSignals.d.ts +21 -0
- package/dist/captureSignals.js +52 -1
- package/dist/cli.js +3 -1
- package/dist/commands/doctor.js +11 -0
- package/dist/connect.js +9 -1
- package/dist/copilotHooks.d.ts +85 -0
- package/dist/copilotHooks.js +458 -0
- package/dist/copilotInstallation.d.ts +10 -0
- package/dist/copilotInstallation.js +65 -0
- package/dist/cursorHooks.d.ts +5 -1
- package/dist/cursorHooks.js +50 -7
- package/dist/cursorLifecycle.d.ts +2 -0
- package/dist/cursorLifecycle.js +115 -0
- package/dist/hook.js +154 -36
- package/dist/launchContext.d.ts +46 -0
- package/dist/launchContext.js +200 -0
- package/dist/limitEvent.d.ts +3 -0
- package/dist/limitEvent.js +42 -0
- package/dist/limitEventReporter.d.ts +1 -1
- package/dist/limitEventReporter.js +1 -1
- package/dist/nativeSteering.d.ts +5 -0
- package/dist/nativeSteering.js +27 -0
- package/dist/subagents.d.ts +36 -0
- package/dist/subagents.js +146 -0
- package/dist/teamSkillContext.d.ts +9 -0
- package/dist/teamSkillContext.js +11 -0
- package/dist/transcriptModels.d.ts +2 -0
- package/dist/transcriptModels.js +8 -2
- package/dist/utils.d.ts +22 -3
- package/dist/utils.js +164 -46
- package/package.json +8 -5
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.recordCursorLifecycle = recordCursorLifecycle;
|
|
37
|
+
exports.cursorLifecycleRecords = cursorLifecycleRecords;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
41
|
+
const configDir_1 = require("./configDir");
|
|
42
|
+
const limitEvent_1 = require("./limitEvent");
|
|
43
|
+
/** Retain aborted-turn evidence across non-final captures, which are snapshots. */
|
|
44
|
+
function fileFor(sessionId) {
|
|
45
|
+
const key = crypto.createHash("sha256").update(sessionId).digest("hex");
|
|
46
|
+
return path.join((0, configDir_1.codiedevStateDir)(), "cursor-lifecycle", `${key}.json`);
|
|
47
|
+
}
|
|
48
|
+
function recordCursorLifecycle(input, final) {
|
|
49
|
+
if (typeof input.session_id !== "string")
|
|
50
|
+
return;
|
|
51
|
+
const status = input.status ?? input.reason;
|
|
52
|
+
const file = fileFor(input.session_id);
|
|
53
|
+
let state = { ids: [] };
|
|
54
|
+
try {
|
|
55
|
+
state = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
56
|
+
}
|
|
57
|
+
catch { /* first observation */ }
|
|
58
|
+
const model = typeof input.model_id === "string" && input.model_id.trim() ? input.model_id
|
|
59
|
+
: typeof input.model === "string" && input.model.trim() ? input.model : undefined;
|
|
60
|
+
if (model)
|
|
61
|
+
state.model = model;
|
|
62
|
+
const event = typeof input.hook_event_name === "string" ? input.hook_event_name : final ? "sessionEnd" : "stop";
|
|
63
|
+
// sessionEnd can arrive long after the agent stopped. Do not count that
|
|
64
|
+
// window-open time as active work. These are observed hook times, not
|
|
65
|
+
// invented per-message timestamps.
|
|
66
|
+
if (event !== "sessionEnd" || !state.observations?.length) {
|
|
67
|
+
const timestamp = new Date().toISOString();
|
|
68
|
+
state.observations = [...(state.observations ?? []), { event, timestamp, ...(model ? { model } : {}) }].slice(-1000);
|
|
69
|
+
}
|
|
70
|
+
const limit = event === "postToolUse" || event === "postToolUseFailure" ? null : (0, limitEvent_1.parseNativeLimitError)(input);
|
|
71
|
+
if (limit) {
|
|
72
|
+
const generationId = typeof input.generation_id === "string" ? input.generation_id : undefined;
|
|
73
|
+
const last = state.limits?.at(-1);
|
|
74
|
+
const retry = last && (generationId ? last.generationId === generationId
|
|
75
|
+
: final && state.lastStatus === "error" && last.reason === limit.reason && last.resetText === limit.resetText);
|
|
76
|
+
if (!retry)
|
|
77
|
+
state.limits = [...(state.limits ?? []), { ...limit, ...(generationId ? { generationId } : {}) }].slice(-1000);
|
|
78
|
+
}
|
|
79
|
+
if (event === "subagentStart" && typeof input.subagent_id === "string") {
|
|
80
|
+
state.subagents = [...new Set([...(state.subagents ?? []), input.subagent_id])].slice(-1000);
|
|
81
|
+
}
|
|
82
|
+
if (event === "postToolUseFailure" && input.failure_type === "permission_denied") {
|
|
83
|
+
const rejectionId = typeof input.tool_use_id === "string" ? input.tool_use_id : `${Date.now()}-${state.rejections?.length ?? 0}`;
|
|
84
|
+
state.rejections = [...new Set([...(state.rejections ?? []), rejectionId])].slice(-1000);
|
|
85
|
+
}
|
|
86
|
+
const aborted = status === "aborted" || input.is_interrupt === true;
|
|
87
|
+
// sessionEnd after an aborted stop is the same interruption. A repeated
|
|
88
|
+
// stop with the same generation_id is likewise a retry, not another turn.
|
|
89
|
+
const id = typeof input.generation_id === "string" ? input.generation_id
|
|
90
|
+
: `${Date.now()}-${state.ids.length}`;
|
|
91
|
+
if (aborted && !(final && state.lastStatus === "aborted") && !state.ids.includes(id))
|
|
92
|
+
state.ids.push(id);
|
|
93
|
+
if (typeof status === "string")
|
|
94
|
+
state.lastStatus = status;
|
|
95
|
+
state.ids = state.ids.slice(-1000);
|
|
96
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
97
|
+
fs.writeFileSync(file, JSON.stringify(state), { mode: 0o600 });
|
|
98
|
+
}
|
|
99
|
+
function cursorLifecycleRecords(sessionId) {
|
|
100
|
+
if (!sessionId)
|
|
101
|
+
return "";
|
|
102
|
+
try {
|
|
103
|
+
const state = JSON.parse(fs.readFileSync(fileFor(sessionId), "utf8"));
|
|
104
|
+
return [...(state.observations ?? []).map((observation) => JSON.stringify({ type: "system", subtype: "cursor_hook_observation", ...observation })),
|
|
105
|
+
...(state.model ? [JSON.stringify({ type: "system", subtype: "cursor_model", model: state.model })] : []),
|
|
106
|
+
...(state.rejections ?? []).map(id => JSON.stringify({ type: "agent.rejection", id })),
|
|
107
|
+
...(state.subagents?.length ? [JSON.stringify({ type: "cursor.subagent_summary", count: state.subagents.length, truncated: state.subagents.length >= 1000 })] : []),
|
|
108
|
+
...state.ids.map((id) => JSON.stringify({ type: "agent.interruption", id })),
|
|
109
|
+
...(state.limits ?? []).map(({ generationId: _generationId, ...event }) => JSON.stringify({ type: "agent.limit", ...event })),
|
|
110
|
+
].join("\n");
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
}
|
package/dist/hook.js
CHANGED
|
@@ -44,11 +44,16 @@ const http = __importStar(require("http"));
|
|
|
44
44
|
const child_process_1 = require("child_process");
|
|
45
45
|
const utils_1 = require("./utils");
|
|
46
46
|
const limitEventReporter_1 = require("./limitEventReporter");
|
|
47
|
+
const limitEvent_1 = require("./limitEvent");
|
|
48
|
+
const cursorLifecycle_1 = require("./cursorLifecycle");
|
|
47
49
|
const codexLimitScan_1 = require("./codexLimitScan");
|
|
50
|
+
const launchContext_1 = require("./launchContext");
|
|
48
51
|
const version_1 = require("./version");
|
|
49
52
|
const captureLog_1 = require("./captureLog");
|
|
53
|
+
const subagents_1 = require("./subagents");
|
|
50
54
|
const captureExclusions_1 = require("./captureExclusions");
|
|
51
55
|
const cursorHooks_1 = require("./cursorHooks");
|
|
56
|
+
const copilotHooks_1 = require("./copilotHooks");
|
|
52
57
|
// Pure builder for the discovery additionalContext. Returns null when there are
|
|
53
58
|
// no matches. The suggested `codiedev pull` command carries the EXACT top key
|
|
54
59
|
// (e.g. "pr-push.md") rather than a "<key>" placeholder — otherwise agents
|
|
@@ -72,28 +77,31 @@ function buildDiscoveryContext(skills) {
|
|
|
72
77
|
// UserPromptSubmit: query skill discovery for the prompt and, if a team skill
|
|
73
78
|
// clears the confidence threshold, inject it as additionalContext so the agent
|
|
74
79
|
// can offer to pull+run it. Fully silent on miss/error — never blocks input.
|
|
75
|
-
async function
|
|
80
|
+
async function discoverContext(prompt) {
|
|
76
81
|
if (!prompt || !prompt.trim())
|
|
77
|
-
|
|
82
|
+
return null;
|
|
78
83
|
const config = (0, utils_1.readConfig)();
|
|
79
84
|
if (!config)
|
|
80
|
-
|
|
85
|
+
return null;
|
|
81
86
|
try {
|
|
82
87
|
// POST body, never a query param: raw prompts in GET query strings land
|
|
83
88
|
// in server/proxy access logs (SIG-16). The server keeps accepting legacy
|
|
84
89
|
// GETs from older CLIs during the transition.
|
|
85
90
|
const res = await postJsonForResponse(`${config.backendUrl}/api/cli/skill-discovery`, { q: prompt }, config.token);
|
|
86
91
|
const skills = res?.skills ?? [];
|
|
87
|
-
|
|
88
|
-
if (additionalContext) {
|
|
89
|
-
process.stdout.write(JSON.stringify({
|
|
90
|
-
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext },
|
|
91
|
-
}));
|
|
92
|
-
}
|
|
92
|
+
return buildDiscoveryContext(skills);
|
|
93
93
|
}
|
|
94
94
|
catch {
|
|
95
95
|
// silent — discovery is best-effort
|
|
96
96
|
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
async function runDiscover(prompt) {
|
|
100
|
+
const additionalContext = await discoverContext(prompt);
|
|
101
|
+
if (additionalContext)
|
|
102
|
+
process.stdout.write(JSON.stringify({
|
|
103
|
+
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext },
|
|
104
|
+
}));
|
|
97
105
|
process.exit(0);
|
|
98
106
|
}
|
|
99
107
|
function readTranscriptTailFile(transcriptPath, maxBytes = 512 * 1024) {
|
|
@@ -255,7 +263,7 @@ async function postSessionSkip(config, sessionId, reason) {
|
|
|
255
263
|
async function runSessionStart(hookInput, source = "claude-code") {
|
|
256
264
|
const config = (0, utils_1.readConfig)();
|
|
257
265
|
if (!config)
|
|
258
|
-
|
|
266
|
+
return;
|
|
259
267
|
if (hookInput.session_id) {
|
|
260
268
|
try {
|
|
261
269
|
await postJson(`${config.backendUrl}/api/cli/sessionStart`, {
|
|
@@ -268,7 +276,7 @@ async function runSessionStart(hookInput, source = "claude-code") {
|
|
|
268
276
|
/* best-effort — never block session startup */
|
|
269
277
|
}
|
|
270
278
|
}
|
|
271
|
-
|
|
279
|
+
return;
|
|
272
280
|
}
|
|
273
281
|
function postJson(url, body, bearerToken,
|
|
274
282
|
// Socket timeout. Pass a SMALL value for tiny pings that run on (or near)
|
|
@@ -415,6 +423,8 @@ function resolveMode(arg) {
|
|
|
415
423
|
return "codex";
|
|
416
424
|
if (arg === "capture-cursor" || arg === "backup-cursor")
|
|
417
425
|
return "cursor";
|
|
426
|
+
if (arg === "capture-copilot" || arg === "backup-copilot")
|
|
427
|
+
return "copilot";
|
|
418
428
|
if (arg === "capture-opencode" || arg === "capture-opencode-final")
|
|
419
429
|
return "opencode";
|
|
420
430
|
if (arg === "capture-desktop" || arg === "capture-desktop-final")
|
|
@@ -524,7 +534,9 @@ async function main() {
|
|
|
524
534
|
const parsed = JSON.parse(raw);
|
|
525
535
|
hookInput = (process.argv[2]?.endsWith("-cursor")
|
|
526
536
|
? (0, cursorHooks_1.normalizeCursorHookInput)(parsed)
|
|
527
|
-
:
|
|
537
|
+
: process.argv[2]?.endsWith("-copilot")
|
|
538
|
+
? (0, copilotHooks_1.normalizeCopilotHookInput)(parsed)
|
|
539
|
+
: parsed);
|
|
528
540
|
}
|
|
529
541
|
}
|
|
530
542
|
catch {
|
|
@@ -537,16 +549,39 @@ async function main() {
|
|
|
537
549
|
if ((0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput)) {
|
|
538
550
|
process.exit(0);
|
|
539
551
|
}
|
|
552
|
+
if (["session-start-cursor", "backup-cursor", "capture-cursor", "observe-cursor"].includes(process.argv[2])) {
|
|
553
|
+
try {
|
|
554
|
+
(0, cursorLifecycle_1.recordCursorLifecycle)(hookInput, process.argv[2] === "capture-cursor");
|
|
555
|
+
}
|
|
556
|
+
catch { /* best-effort local evidence */ }
|
|
557
|
+
}
|
|
558
|
+
if (process.argv[2] === "observe-cursor") {
|
|
559
|
+
process.stdout.write("{}");
|
|
560
|
+
return; // local evidence only
|
|
561
|
+
}
|
|
562
|
+
if (process.argv[2] === "limit-copilot") {
|
|
563
|
+
const config = (0, utils_1.readConfig)();
|
|
564
|
+
const event = (0, limitEvent_1.parseNativeLimitError)(hookInput);
|
|
565
|
+
if (config && event) {
|
|
566
|
+
try {
|
|
567
|
+
await (0, limitEventReporter_1.postLimitEvent)(config, { ...event, source: "copilot", sessionId: hookInput.session_id });
|
|
568
|
+
}
|
|
569
|
+
catch { /* best-effort */ }
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
540
573
|
// UserPromptSubmit discovery path — separate from session capture.
|
|
541
574
|
if (process.argv[2] === "discover" ||
|
|
542
575
|
process.argv[2] === "discover-codex" ||
|
|
543
|
-
process.argv[2] === "discover-codie"
|
|
576
|
+
process.argv[2] === "discover-codie" ||
|
|
577
|
+
process.argv[2] === "discover-copilot") {
|
|
544
578
|
// A Codex block leaves no hook of its own, so this prompt is the first
|
|
545
579
|
// chance to report it. Runs before runDiscover, which exits the process.
|
|
546
580
|
if (process.argv[2] === "discover-codex") {
|
|
547
581
|
await runCodexLimitSweep(hookInput.session_id);
|
|
548
582
|
}
|
|
549
|
-
|
|
583
|
+
if (process.argv[2] !== "discover-copilot")
|
|
584
|
+
await runDiscover(hookInput.prompt);
|
|
550
585
|
return;
|
|
551
586
|
}
|
|
552
587
|
// StopFailure (matcher rate_limit): post the rate-limit event.
|
|
@@ -561,12 +596,23 @@ async function main() {
|
|
|
561
596
|
// SessionStart: post the capture-coverage denominator ping. Tiny + fast.
|
|
562
597
|
if (process.argv[2] === "session-start" ||
|
|
563
598
|
process.argv[2] === "session-start-cursor" ||
|
|
599
|
+
process.argv[2] === "session-start-copilot" ||
|
|
564
600
|
process.argv[2] === "session-start-opencode") {
|
|
565
601
|
await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
|
|
566
602
|
? "opencode"
|
|
567
603
|
: process.argv[2] === "session-start-cursor"
|
|
568
604
|
? "cursor"
|
|
569
|
-
: "
|
|
605
|
+
: process.argv[2] === "session-start-copilot"
|
|
606
|
+
? "copilot"
|
|
607
|
+
: "claude-code");
|
|
608
|
+
if ((0, utils_1.readConfig)() && (process.argv[2] === "session-start-cursor" || process.argv[2] === "session-start-copilot")) {
|
|
609
|
+
const source = process.argv[2] === "session-start-cursor" ? "cursor" : "copilot";
|
|
610
|
+
const { TEAM_SKILL_CONTEXT, teamSkillSessionOutput } = await Promise.resolve().then(() => __importStar(require("./teamSkillContext")));
|
|
611
|
+
const prompt = hookInput.initialPrompt
|
|
612
|
+
?? hookInput.initial_prompt;
|
|
613
|
+
const discovered = typeof prompt === "string" ? await discoverContext(prompt) : null;
|
|
614
|
+
process.stdout.write(JSON.stringify(teamSkillSessionOutput(source, [TEAM_SKILL_CONTEXT, discovered].filter(Boolean).join("\n\n"))));
|
|
615
|
+
}
|
|
570
616
|
return;
|
|
571
617
|
}
|
|
572
618
|
// Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets
|
|
@@ -577,12 +623,22 @@ async function main() {
|
|
|
577
623
|
if ((process.argv[2] ?? "").endsWith("-cursor")) {
|
|
578
624
|
(0, utils_1.ensureCursorCodiedevHooksInstalled)();
|
|
579
625
|
}
|
|
626
|
+
else if ((process.argv[2] ?? "").endsWith("-copilot")) {
|
|
627
|
+
(0, utils_1.ensureCopilotCodiedevHooksInstalled)();
|
|
628
|
+
}
|
|
580
629
|
else {
|
|
581
630
|
(0, utils_1.ensureAllCodiedevHooksInstalled)();
|
|
582
631
|
}
|
|
583
632
|
if (process.argv[2] === "capture-codex")
|
|
584
633
|
(0, utils_1.ensureCodexLimitHookInstalled)();
|
|
585
634
|
const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
|
|
635
|
+
// Read here, in the process the harness spawned: the detached capture child
|
|
636
|
+
// is reparented once this exits and can no longer see who launched it.
|
|
637
|
+
const launch = (0, launchContext_1.hookLaunch)(process.argv[2], hookInput, {
|
|
638
|
+
copilotClientName: process.argv[2]?.endsWith("-copilot")
|
|
639
|
+
? (0, copilotHooks_1.readCopilotWorkspace)(transcript_path).clientName
|
|
640
|
+
: undefined,
|
|
641
|
+
});
|
|
586
642
|
const workspaceRoot = workspace_roots && workspace_roots.length > 0
|
|
587
643
|
? workspace_roots[0]
|
|
588
644
|
: cwd || process.cwd();
|
|
@@ -594,17 +650,20 @@ async function main() {
|
|
|
594
650
|
// extension) where SessionEnd may never come — the silent zero-capture bug.
|
|
595
651
|
// Same detached capture-finish child as SessionEnd, marked non-final so the
|
|
596
652
|
// server renews the session lease instead of summarizing immediately.
|
|
597
|
-
if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor") {
|
|
653
|
+
if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor" || process.argv[2] === "backup-copilot") {
|
|
598
654
|
if (transcript_path) {
|
|
599
655
|
try {
|
|
600
656
|
(0, utils_1.dispatchCaptureFinish)({
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
657
|
+
mode: process.argv[2] === "backup-cursor"
|
|
658
|
+
? "capture-cursor"
|
|
659
|
+
: process.argv[2] === "backup-copilot"
|
|
660
|
+
? "capture-copilot"
|
|
661
|
+
: "capture",
|
|
604
662
|
session_id,
|
|
605
663
|
transcript_path,
|
|
606
664
|
cwd,
|
|
607
665
|
workspace_roots,
|
|
666
|
+
...(launch ? { launch } : {}),
|
|
608
667
|
// non-final: server should renew the lease, not close it yet.
|
|
609
668
|
final: false,
|
|
610
669
|
}, process.argv[1], {
|
|
@@ -660,10 +719,10 @@ async function main() {
|
|
|
660
719
|
transcript_path,
|
|
661
720
|
cwd,
|
|
662
721
|
workspace_roots,
|
|
663
|
-
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
722
|
+
...(launch ? { launch } : {}),
|
|
723
|
+
// final:true for real SessionEnd signals (Claude Code, Cursor, Copilot).
|
|
724
|
+
// Codex routes its per-turn Stop hook through "capture-codex" —
|
|
725
|
+
// final:true there would close the server lease every turn.
|
|
667
726
|
final: (0, utils_1.resolveCaptureFinal)(process.argv[2]),
|
|
668
727
|
}, process.argv[1], { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile: jobFile, openSync: fs.openSync, closeSync: fs.closeSync });
|
|
669
728
|
}
|
|
@@ -728,6 +787,15 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
728
787
|
let transcriptContent;
|
|
729
788
|
try {
|
|
730
789
|
transcriptContent = fs.readFileSync(transcript_path, "utf8");
|
|
790
|
+
// Copilot writes session.shutdown (the token totals) just AFTER sessionEnd
|
|
791
|
+
// fires. This child is detached, so wait briefly for it on the final
|
|
792
|
+
// capture. VS Code may never write one; then tokens stay unreported.
|
|
793
|
+
if (mode === "copilot" && input.final === true && !(0, copilotHooks_1.hasCopilotShutdown)(transcriptContent)) {
|
|
794
|
+
for (let waited = 0; waited < COPILOT_SHUTDOWN_WAIT_MS && !(0, copilotHooks_1.hasCopilotShutdown)(transcriptContent); waited += 250) {
|
|
795
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
796
|
+
transcriptContent = fs.readFileSync(transcript_path, "utf8");
|
|
797
|
+
}
|
|
798
|
+
}
|
|
731
799
|
}
|
|
732
800
|
catch (err) {
|
|
733
801
|
// ENOENT → "no-transcript" (phantom desktop-app helper session, excluded
|
|
@@ -751,11 +819,27 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
751
819
|
// doesn't. Every other mode leaves transcriptContent untouched here, so this
|
|
752
820
|
// is a no-op for them.
|
|
753
821
|
const rawTranscriptContent = transcriptContent;
|
|
822
|
+
let copilotSubagents;
|
|
823
|
+
const copilotWorkspace = mode === "copilot" ? (0, copilotHooks_1.readCopilotWorkspace)(transcript_path) : {};
|
|
754
824
|
if (parserMode === "opencode") {
|
|
755
825
|
transcriptContent = (0, utils_1.normalizeOpenCodeTranscript)(transcriptContent);
|
|
756
826
|
}
|
|
757
827
|
else if (parserMode === "cursor") {
|
|
758
828
|
transcriptContent = (0, cursorHooks_1.normalizeCursorTranscript)(transcriptContent);
|
|
829
|
+
const observations = (0, cursorLifecycle_1.cursorLifecycleRecords)(session_id);
|
|
830
|
+
if (observations)
|
|
831
|
+
transcriptContent += `\n${observations}`;
|
|
832
|
+
}
|
|
833
|
+
else if (parserMode === "copilot") {
|
|
834
|
+
const normalized = (0, copilotHooks_1.normalizeCopilotTranscript)(transcriptContent, { clientName: copilotWorkspace.clientName });
|
|
835
|
+
transcriptContent = normalized.content;
|
|
836
|
+
copilotSubagents = normalized.subagents.count > 0 ? normalized.subagents : undefined;
|
|
837
|
+
}
|
|
838
|
+
if (mode === "cursor" || mode === "copilot") {
|
|
839
|
+
try {
|
|
840
|
+
await (0, limitEventReporter_1.reportTranscriptLimitEvent)({ config, source: mode, sessionId: session_id, transcript: transcriptContent });
|
|
841
|
+
}
|
|
842
|
+
catch { /* capture proceeds if limit reporting fails */ }
|
|
759
843
|
}
|
|
760
844
|
// Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
|
|
761
845
|
// need the git remote of the workspace.
|
|
@@ -784,19 +868,26 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
784
868
|
const stats = parserMode === "codex"
|
|
785
869
|
? (0, utils_1.parseCodexStats)(transcriptContent)
|
|
786
870
|
: (0, utils_1.parseClaudeCodeStats)(transcriptContent);
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
|
|
871
|
+
// Cursor can emit duplicate lifecycle notifications while one transcript
|
|
872
|
+
// path is still byte-empty. Never use parsed message count here: Cursor's
|
|
873
|
+
// private JSONL shape can evolve independently, and parser drift must not
|
|
874
|
+
// silently discard a real non-empty session.
|
|
875
|
+
const emptyReason = parserMode === "cursor" && (0, cursorHooks_1.isCursorTranscriptEmpty)(rawTranscriptContent)
|
|
876
|
+
? "empty-cursor-transcript"
|
|
877
|
+
: parserMode === "copilot" && (0, copilotHooks_1.isCopilotTranscriptEmpty)(rawTranscriptContent)
|
|
878
|
+
? "empty-copilot-transcript"
|
|
879
|
+
: null;
|
|
880
|
+
if (emptyReason) {
|
|
790
881
|
(0, captureLog_1.appendCaptureLog)({
|
|
791
882
|
stage: "capture-finish",
|
|
792
883
|
sessionId: session_id,
|
|
793
884
|
outcome: "skip",
|
|
794
|
-
detail:
|
|
885
|
+
detail: emptyReason,
|
|
795
886
|
});
|
|
796
887
|
// A non-final stop snapshot is only a backup; reporting it as a skipped
|
|
797
888
|
// session would close server-side coverage while the session is alive.
|
|
798
889
|
if ((0, utils_1.shouldReportCaptureSkip)(input.final)) {
|
|
799
|
-
await postSessionSkip(config, session_id,
|
|
890
|
+
await postSessionSkip(config, session_id, emptyReason);
|
|
800
891
|
}
|
|
801
892
|
return;
|
|
802
893
|
}
|
|
@@ -818,7 +909,9 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
818
909
|
// the `worktree-<name>` one the PR was opened from). Indexed server-side under
|
|
819
910
|
// every branch in the array, so the worktree's PR attributes regardless.
|
|
820
911
|
const authoredBranches = (0, utils_1.parseAuthoredBranches)(transcriptContent);
|
|
821
|
-
|
|
912
|
+
// Copilot records the branch it started on in workspace.yaml.
|
|
913
|
+
const recordedBranches = copilotWorkspace.branch ? [copilotWorkspace.branch] : [];
|
|
914
|
+
const branches = [...new Set([...keys.branches, ...authoredBranches, ...recordedBranches])];
|
|
822
915
|
const branch = keys.branch ?? branches[0] ?? null;
|
|
823
916
|
// Union the worktree HEAD shas with the SHAs the agent actually committed/
|
|
824
917
|
// pushed (parsed from the transcript's git output, resolved via the shared
|
|
@@ -834,6 +927,25 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
834
927
|
// pre-normalization content for desktop; parseSteering routes through
|
|
835
928
|
// parserMode like the parsers above.
|
|
836
929
|
const usage = (0, utils_1.parseUsage)(mode === "desktop" ? rawTranscriptContent : transcriptContent, mode);
|
|
930
|
+
// Claude Code subagents: their transcripts sit beside this one and are part
|
|
931
|
+
// of this session's work (skill runs, tool calls, tokens). Folded in here,
|
|
932
|
+
// never uploaded as sessions of their own.
|
|
933
|
+
const subagents = mode === "claude-code"
|
|
934
|
+
? (0, subagents_1.readSubagentTranscripts)(transcript_path, session_id)
|
|
935
|
+
: { contents: [], count: 0, truncated: false };
|
|
936
|
+
const subagentUsage = (0, subagents_1.foldSubagentModelUsage)([], subagents, mode);
|
|
937
|
+
for (const u of subagentUsage) {
|
|
938
|
+
usage.inputTokens = (usage.inputTokens ?? 0) + u.inputTokens;
|
|
939
|
+
usage.outputTokens = (usage.outputTokens ?? 0) + u.outputTokens;
|
|
940
|
+
usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + u.cacheReadTokens;
|
|
941
|
+
usage.cacheCreationTokens = (usage.cacheCreationTokens ?? 0) + u.cacheCreationTokens;
|
|
942
|
+
usage.cacheTokens = (usage.cacheTokens ?? 0) + u.cacheReadTokens + u.cacheCreationTokens;
|
|
943
|
+
}
|
|
944
|
+
const hasUsage = mode === "cursor" ? (0, cursorHooks_1.cursorTranscriptHasUsage)(transcriptContent)
|
|
945
|
+
: mode === "copilot" ? (0, transcriptModels_1.parseTranscriptModels)(transcriptContent, mode).some(model => model.tokensAvailable !== false)
|
|
946
|
+
: true;
|
|
947
|
+
if (mode === "cursor" && !usage.model)
|
|
948
|
+
usage.model = (0, cursorHooks_1.cursorObservedModel)(transcriptContent) ?? null;
|
|
837
949
|
const sessionStartedAt = (0, utils_1.parseSessionStart)(transcriptContent);
|
|
838
950
|
const steering = (0, utils_1.parseSteering)(transcriptContent, parserMode);
|
|
839
951
|
const payload = (0, utils_1.buildCapturePayload)({
|
|
@@ -852,14 +964,14 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
852
964
|
headShas,
|
|
853
965
|
gitRefs,
|
|
854
966
|
pullRequestUrls,
|
|
855
|
-
inputTokens: usage.inputTokens,
|
|
856
|
-
outputTokens: usage.outputTokens,
|
|
857
|
-
cacheTokens: usage.cacheTokens,
|
|
858
|
-
cacheReadTokens: usage.cacheReadTokens,
|
|
859
|
-
cacheCreationTokens: usage.cacheCreationTokens,
|
|
967
|
+
inputTokens: hasUsage ? usage.inputTokens : undefined,
|
|
968
|
+
outputTokens: hasUsage ? usage.outputTokens : undefined,
|
|
969
|
+
cacheTokens: hasUsage ? usage.cacheTokens : undefined,
|
|
970
|
+
cacheReadTokens: hasUsage ? usage.cacheReadTokens : undefined,
|
|
971
|
+
cacheCreationTokens: hasUsage ? usage.cacheCreationTokens : undefined,
|
|
860
972
|
model: usage.model,
|
|
861
|
-
captureSignals: (0, captureSignals_1.parseCaptureSignals)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode),
|
|
862
|
-
modelUsage: (0, transcriptModels_1.parseTranscriptModels)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode),
|
|
973
|
+
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),
|
|
974
|
+
modelUsage: (0, subagents_1.foldSubagentModelUsage)((0, transcriptModels_1.parseTranscriptModels)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode), subagents, mode),
|
|
863
975
|
sessionStartedAt,
|
|
864
976
|
interruptCount: steering.interrupts,
|
|
865
977
|
rejectionCount: steering.rejections,
|
|
@@ -953,6 +1065,12 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
953
1065
|
process.stderr.write(`[codiedev-hook] Warning: Failed to dispatch session capture — ${err.message}\n`);
|
|
954
1066
|
}
|
|
955
1067
|
}
|
|
1068
|
+
// How long the final Copilot capture waits for session.shutdown.
|
|
1069
|
+
const COPILOT_SHUTDOWN_WAIT_MS = 5000;
|
|
1070
|
+
/** Copilot fleet sub-agents run inside the one events.jsonl. */
|
|
1071
|
+
function withCopilotSubagents(signals, subagents) {
|
|
1072
|
+
return subagents ? { ...signals, subagents } : signals;
|
|
1073
|
+
}
|
|
956
1074
|
main().catch(() => {
|
|
957
1075
|
process.exit(0);
|
|
958
1076
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export declare function codieLaunchFromArgv(argv: readonly string[]): string;
|
|
2
|
+
/** One launching process's command line → a Cursor launch value. */
|
|
3
|
+
export declare function cursorLaunchFromCommand(command: string): string | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* One launching process's command line → a Copilot CLI launch value. The CLI
|
|
6
|
+
* runs as `…/@github/copilot-<platform>/copilot [flags]` under
|
|
7
|
+
* `node …/bin/copilot [flags]`; `-p`/`--prompt` is headless (scripted). VS
|
|
8
|
+
* Code's agent (`copilot-runtime --headless`) is named by workspace.yaml
|
|
9
|
+
* instead, so it never matches here.
|
|
10
|
+
*/
|
|
11
|
+
export declare function copilotLaunchFromCommand(command: string): string | undefined;
|
|
12
|
+
type Ps = (pid: number) => {
|
|
13
|
+
ppid: number;
|
|
14
|
+
command: string;
|
|
15
|
+
} | null;
|
|
16
|
+
/**
|
|
17
|
+
* Windows: one PowerShell call returns the whole ancestor chain (spawning
|
|
18
|
+
* PowerShell per level would cost ~0.5s each). CommandLine needs CIM, which
|
|
19
|
+
* every supported Windows has; any failure means "unknown", never a guess.
|
|
20
|
+
*/
|
|
21
|
+
export declare function windowsAncestors(startPid: number, depth: number): Array<{
|
|
22
|
+
pid: number;
|
|
23
|
+
ppid: number;
|
|
24
|
+
command: string;
|
|
25
|
+
}> | null;
|
|
26
|
+
type LaunchDeps = {
|
|
27
|
+
platform?: string;
|
|
28
|
+
ps?: Ps;
|
|
29
|
+
depth?: number;
|
|
30
|
+
windows?: typeof windowsAncestors;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Walk up to `depth` ancestors (a shell or `npx` usually sits between Cursor
|
|
34
|
+
* and the hook) and return the first Cursor launch found.
|
|
35
|
+
*/
|
|
36
|
+
export declare function detectCursorLaunch(startPid?: number, deps?: LaunchDeps): string | undefined;
|
|
37
|
+
/** The Copilot CLI that launched this hook (`copilot:print` / `copilot:cli`). */
|
|
38
|
+
export declare function detectCopilotLaunch(startPid?: number, deps?: LaunchDeps): string | undefined;
|
|
39
|
+
/** The launch value a hook invocation should carry into its capture job. */
|
|
40
|
+
export declare function hookLaunch(hookCommand: string | undefined, input: {
|
|
41
|
+
launch?: unknown;
|
|
42
|
+
}, deps?: {
|
|
43
|
+
copilotClientName?: string;
|
|
44
|
+
detectCopilot?: () => string | undefined;
|
|
45
|
+
}): string | undefined;
|
|
46
|
+
export {};
|