codiedev 0.9.60 → 0.9.62
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 +4 -0
- package/dist/captureSignals.js +18 -0
- 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 +133 -36
- package/dist/launchContext.d.ts +20 -6
- package/dist/launchContext.js +65 -10
- 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/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 +19 -3
- package/dist/utils.js +162 -43
- package/package.json +7 -4
|
@@ -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,6 +44,8 @@ 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");
|
|
48
50
|
const launchContext_1 = require("./launchContext");
|
|
49
51
|
const version_1 = require("./version");
|
|
@@ -51,6 +53,7 @@ const captureLog_1 = require("./captureLog");
|
|
|
51
53
|
const subagents_1 = require("./subagents");
|
|
52
54
|
const captureExclusions_1 = require("./captureExclusions");
|
|
53
55
|
const cursorHooks_1 = require("./cursorHooks");
|
|
56
|
+
const copilotHooks_1 = require("./copilotHooks");
|
|
54
57
|
// Pure builder for the discovery additionalContext. Returns null when there are
|
|
55
58
|
// no matches. The suggested `codiedev pull` command carries the EXACT top key
|
|
56
59
|
// (e.g. "pr-push.md") rather than a "<key>" placeholder — otherwise agents
|
|
@@ -74,28 +77,31 @@ function buildDiscoveryContext(skills) {
|
|
|
74
77
|
// UserPromptSubmit: query skill discovery for the prompt and, if a team skill
|
|
75
78
|
// clears the confidence threshold, inject it as additionalContext so the agent
|
|
76
79
|
// can offer to pull+run it. Fully silent on miss/error — never blocks input.
|
|
77
|
-
async function
|
|
80
|
+
async function discoverContext(prompt) {
|
|
78
81
|
if (!prompt || !prompt.trim())
|
|
79
|
-
|
|
82
|
+
return null;
|
|
80
83
|
const config = (0, utils_1.readConfig)();
|
|
81
84
|
if (!config)
|
|
82
|
-
|
|
85
|
+
return null;
|
|
83
86
|
try {
|
|
84
87
|
// POST body, never a query param: raw prompts in GET query strings land
|
|
85
88
|
// in server/proxy access logs (SIG-16). The server keeps accepting legacy
|
|
86
89
|
// GETs from older CLIs during the transition.
|
|
87
90
|
const res = await postJsonForResponse(`${config.backendUrl}/api/cli/skill-discovery`, { q: prompt }, config.token);
|
|
88
91
|
const skills = res?.skills ?? [];
|
|
89
|
-
|
|
90
|
-
if (additionalContext) {
|
|
91
|
-
process.stdout.write(JSON.stringify({
|
|
92
|
-
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext },
|
|
93
|
-
}));
|
|
94
|
-
}
|
|
92
|
+
return buildDiscoveryContext(skills);
|
|
95
93
|
}
|
|
96
94
|
catch {
|
|
97
95
|
// silent — discovery is best-effort
|
|
98
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
|
+
}));
|
|
99
105
|
process.exit(0);
|
|
100
106
|
}
|
|
101
107
|
function readTranscriptTailFile(transcriptPath, maxBytes = 512 * 1024) {
|
|
@@ -257,7 +263,7 @@ async function postSessionSkip(config, sessionId, reason) {
|
|
|
257
263
|
async function runSessionStart(hookInput, source = "claude-code") {
|
|
258
264
|
const config = (0, utils_1.readConfig)();
|
|
259
265
|
if (!config)
|
|
260
|
-
|
|
266
|
+
return;
|
|
261
267
|
if (hookInput.session_id) {
|
|
262
268
|
try {
|
|
263
269
|
await postJson(`${config.backendUrl}/api/cli/sessionStart`, {
|
|
@@ -270,7 +276,7 @@ async function runSessionStart(hookInput, source = "claude-code") {
|
|
|
270
276
|
/* best-effort — never block session startup */
|
|
271
277
|
}
|
|
272
278
|
}
|
|
273
|
-
|
|
279
|
+
return;
|
|
274
280
|
}
|
|
275
281
|
function postJson(url, body, bearerToken,
|
|
276
282
|
// Socket timeout. Pass a SMALL value for tiny pings that run on (or near)
|
|
@@ -417,6 +423,8 @@ function resolveMode(arg) {
|
|
|
417
423
|
return "codex";
|
|
418
424
|
if (arg === "capture-cursor" || arg === "backup-cursor")
|
|
419
425
|
return "cursor";
|
|
426
|
+
if (arg === "capture-copilot" || arg === "backup-copilot")
|
|
427
|
+
return "copilot";
|
|
420
428
|
if (arg === "capture-opencode" || arg === "capture-opencode-final")
|
|
421
429
|
return "opencode";
|
|
422
430
|
if (arg === "capture-desktop" || arg === "capture-desktop-final")
|
|
@@ -526,7 +534,9 @@ async function main() {
|
|
|
526
534
|
const parsed = JSON.parse(raw);
|
|
527
535
|
hookInput = (process.argv[2]?.endsWith("-cursor")
|
|
528
536
|
? (0, cursorHooks_1.normalizeCursorHookInput)(parsed)
|
|
529
|
-
:
|
|
537
|
+
: process.argv[2]?.endsWith("-copilot")
|
|
538
|
+
? (0, copilotHooks_1.normalizeCopilotHookInput)(parsed)
|
|
539
|
+
: parsed);
|
|
530
540
|
}
|
|
531
541
|
}
|
|
532
542
|
catch {
|
|
@@ -539,16 +549,39 @@ async function main() {
|
|
|
539
549
|
if ((0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput)) {
|
|
540
550
|
process.exit(0);
|
|
541
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
|
+
}
|
|
542
573
|
// UserPromptSubmit discovery path — separate from session capture.
|
|
543
574
|
if (process.argv[2] === "discover" ||
|
|
544
575
|
process.argv[2] === "discover-codex" ||
|
|
545
|
-
process.argv[2] === "discover-codie"
|
|
576
|
+
process.argv[2] === "discover-codie" ||
|
|
577
|
+
process.argv[2] === "discover-copilot") {
|
|
546
578
|
// A Codex block leaves no hook of its own, so this prompt is the first
|
|
547
579
|
// chance to report it. Runs before runDiscover, which exits the process.
|
|
548
580
|
if (process.argv[2] === "discover-codex") {
|
|
549
581
|
await runCodexLimitSweep(hookInput.session_id);
|
|
550
582
|
}
|
|
551
|
-
|
|
583
|
+
if (process.argv[2] !== "discover-copilot")
|
|
584
|
+
await runDiscover(hookInput.prompt);
|
|
552
585
|
return;
|
|
553
586
|
}
|
|
554
587
|
// StopFailure (matcher rate_limit): post the rate-limit event.
|
|
@@ -563,12 +596,23 @@ async function main() {
|
|
|
563
596
|
// SessionStart: post the capture-coverage denominator ping. Tiny + fast.
|
|
564
597
|
if (process.argv[2] === "session-start" ||
|
|
565
598
|
process.argv[2] === "session-start-cursor" ||
|
|
599
|
+
process.argv[2] === "session-start-copilot" ||
|
|
566
600
|
process.argv[2] === "session-start-opencode") {
|
|
567
601
|
await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
|
|
568
602
|
? "opencode"
|
|
569
603
|
: process.argv[2] === "session-start-cursor"
|
|
570
604
|
? "cursor"
|
|
571
|
-
: "
|
|
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
|
+
}
|
|
572
616
|
return;
|
|
573
617
|
}
|
|
574
618
|
// Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets
|
|
@@ -579,6 +623,9 @@ async function main() {
|
|
|
579
623
|
if ((process.argv[2] ?? "").endsWith("-cursor")) {
|
|
580
624
|
(0, utils_1.ensureCursorCodiedevHooksInstalled)();
|
|
581
625
|
}
|
|
626
|
+
else if ((process.argv[2] ?? "").endsWith("-copilot")) {
|
|
627
|
+
(0, utils_1.ensureCopilotCodiedevHooksInstalled)();
|
|
628
|
+
}
|
|
582
629
|
else {
|
|
583
630
|
(0, utils_1.ensureAllCodiedevHooksInstalled)();
|
|
584
631
|
}
|
|
@@ -587,7 +634,11 @@ async function main() {
|
|
|
587
634
|
const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
|
|
588
635
|
// Read here, in the process the harness spawned: the detached capture child
|
|
589
636
|
// is reparented once this exits and can no longer see who launched it.
|
|
590
|
-
const launch = (0, launchContext_1.hookLaunch)(process.argv[2], hookInput
|
|
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
|
+
});
|
|
591
642
|
const workspaceRoot = workspace_roots && workspace_roots.length > 0
|
|
592
643
|
? workspace_roots[0]
|
|
593
644
|
: cwd || process.cwd();
|
|
@@ -599,13 +650,15 @@ async function main() {
|
|
|
599
650
|
// extension) where SessionEnd may never come — the silent zero-capture bug.
|
|
600
651
|
// Same detached capture-finish child as SessionEnd, marked non-final so the
|
|
601
652
|
// server renews the session lease instead of summarizing immediately.
|
|
602
|
-
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") {
|
|
603
654
|
if (transcript_path) {
|
|
604
655
|
try {
|
|
605
656
|
(0, utils_1.dispatchCaptureFinish)({
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
657
|
+
mode: process.argv[2] === "backup-cursor"
|
|
658
|
+
? "capture-cursor"
|
|
659
|
+
: process.argv[2] === "backup-copilot"
|
|
660
|
+
? "capture-copilot"
|
|
661
|
+
: "capture",
|
|
609
662
|
session_id,
|
|
610
663
|
transcript_path,
|
|
611
664
|
cwd,
|
|
@@ -667,10 +720,9 @@ async function main() {
|
|
|
667
720
|
cwd,
|
|
668
721
|
workspace_roots,
|
|
669
722
|
...(launch ? { launch } : {}),
|
|
670
|
-
// final:true
|
|
671
|
-
// per-turn Stop hook through "capture-codex" —
|
|
672
|
-
// close the server lease every turn.
|
|
673
|
-
// Codex remains the legacy final-omitted per-turn path.
|
|
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.
|
|
674
726
|
final: (0, utils_1.resolveCaptureFinal)(process.argv[2]),
|
|
675
727
|
}, process.argv[1], { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile: jobFile, openSync: fs.openSync, closeSync: fs.closeSync });
|
|
676
728
|
}
|
|
@@ -735,6 +787,15 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
735
787
|
let transcriptContent;
|
|
736
788
|
try {
|
|
737
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
|
+
}
|
|
738
799
|
}
|
|
739
800
|
catch (err) {
|
|
740
801
|
// ENOENT → "no-transcript" (phantom desktop-app helper session, excluded
|
|
@@ -758,11 +819,27 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
758
819
|
// doesn't. Every other mode leaves transcriptContent untouched here, so this
|
|
759
820
|
// is a no-op for them.
|
|
760
821
|
const rawTranscriptContent = transcriptContent;
|
|
822
|
+
let copilotSubagents;
|
|
823
|
+
const copilotWorkspace = mode === "copilot" ? (0, copilotHooks_1.readCopilotWorkspace)(transcript_path) : {};
|
|
761
824
|
if (parserMode === "opencode") {
|
|
762
825
|
transcriptContent = (0, utils_1.normalizeOpenCodeTranscript)(transcriptContent);
|
|
763
826
|
}
|
|
764
827
|
else if (parserMode === "cursor") {
|
|
765
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 */ }
|
|
766
843
|
}
|
|
767
844
|
// Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
|
|
768
845
|
// need the git remote of the workspace.
|
|
@@ -791,19 +868,26 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
791
868
|
const stats = parserMode === "codex"
|
|
792
869
|
? (0, utils_1.parseCodexStats)(transcriptContent)
|
|
793
870
|
: (0, utils_1.parseClaudeCodeStats)(transcriptContent);
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
|
|
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) {
|
|
797
881
|
(0, captureLog_1.appendCaptureLog)({
|
|
798
882
|
stage: "capture-finish",
|
|
799
883
|
sessionId: session_id,
|
|
800
884
|
outcome: "skip",
|
|
801
|
-
detail:
|
|
885
|
+
detail: emptyReason,
|
|
802
886
|
});
|
|
803
887
|
// A non-final stop snapshot is only a backup; reporting it as a skipped
|
|
804
888
|
// session would close server-side coverage while the session is alive.
|
|
805
889
|
if ((0, utils_1.shouldReportCaptureSkip)(input.final)) {
|
|
806
|
-
await postSessionSkip(config, session_id,
|
|
890
|
+
await postSessionSkip(config, session_id, emptyReason);
|
|
807
891
|
}
|
|
808
892
|
return;
|
|
809
893
|
}
|
|
@@ -825,7 +909,9 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
825
909
|
// the `worktree-<name>` one the PR was opened from). Indexed server-side under
|
|
826
910
|
// every branch in the array, so the worktree's PR attributes regardless.
|
|
827
911
|
const authoredBranches = (0, utils_1.parseAuthoredBranches)(transcriptContent);
|
|
828
|
-
|
|
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])];
|
|
829
915
|
const branch = keys.branch ?? branches[0] ?? null;
|
|
830
916
|
// Union the worktree HEAD shas with the SHAs the agent actually committed/
|
|
831
917
|
// pushed (parsed from the transcript's git output, resolved via the shared
|
|
@@ -855,6 +941,11 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
855
941
|
usage.cacheCreationTokens = (usage.cacheCreationTokens ?? 0) + u.cacheCreationTokens;
|
|
856
942
|
usage.cacheTokens = (usage.cacheTokens ?? 0) + u.cacheReadTokens + u.cacheCreationTokens;
|
|
857
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;
|
|
858
949
|
const sessionStartedAt = (0, utils_1.parseSessionStart)(transcriptContent);
|
|
859
950
|
const steering = (0, utils_1.parseSteering)(transcriptContent, parserMode);
|
|
860
951
|
const payload = (0, utils_1.buildCapturePayload)({
|
|
@@ -873,13 +964,13 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
873
964
|
headShas,
|
|
874
965
|
gitRefs,
|
|
875
966
|
pullRequestUrls,
|
|
876
|
-
inputTokens: usage.inputTokens,
|
|
877
|
-
outputTokens: usage.outputTokens,
|
|
878
|
-
cacheTokens: usage.cacheTokens,
|
|
879
|
-
cacheReadTokens: usage.cacheReadTokens,
|
|
880
|
-
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,
|
|
881
972
|
model: usage.model,
|
|
882
|
-
captureSignals: (0, subagents_1.foldSubagentSignals)((0, captureSignals_1.withLaunch)((0, captureSignals_1.parseCaptureSignals)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode), input.launch), subagents, 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),
|
|
883
974
|
modelUsage: (0, subagents_1.foldSubagentModelUsage)((0, transcriptModels_1.parseTranscriptModels)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode), subagents, mode),
|
|
884
975
|
sessionStartedAt,
|
|
885
976
|
interruptCount: steering.interrupts,
|
|
@@ -974,6 +1065,12 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
974
1065
|
process.stderr.write(`[codiedev-hook] Warning: Failed to dispatch session capture — ${err.message}\n`);
|
|
975
1066
|
}
|
|
976
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
|
+
}
|
|
977
1074
|
main().catch(() => {
|
|
978
1075
|
process.exit(0);
|
|
979
1076
|
});
|
package/dist/launchContext.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
export declare function codieLaunchFromArgv(argv: readonly string[]): string;
|
|
2
2
|
/** One launching process's command line → a Cursor launch value. */
|
|
3
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;
|
|
4
12
|
type Ps = (pid: number) => {
|
|
5
13
|
ppid: number;
|
|
6
14
|
command: string;
|
|
@@ -15,18 +23,24 @@ export declare function windowsAncestors(startPid: number, depth: number): Array
|
|
|
15
23
|
ppid: number;
|
|
16
24
|
command: string;
|
|
17
25
|
}> | null;
|
|
18
|
-
|
|
19
|
-
* Walk up to `depth` ancestors (a shell or `npx` usually sits between Cursor
|
|
20
|
-
* and the hook) and return the first Cursor launch found.
|
|
21
|
-
*/
|
|
22
|
-
export declare function detectCursorLaunch(startPid?: number, deps?: {
|
|
26
|
+
type LaunchDeps = {
|
|
23
27
|
platform?: string;
|
|
24
28
|
ps?: Ps;
|
|
25
29
|
depth?: number;
|
|
26
30
|
windows?: typeof windowsAncestors;
|
|
27
|
-
}
|
|
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;
|
|
28
39
|
/** The launch value a hook invocation should carry into its capture job. */
|
|
29
40
|
export declare function hookLaunch(hookCommand: string | undefined, input: {
|
|
30
41
|
launch?: unknown;
|
|
42
|
+
}, deps?: {
|
|
43
|
+
copilotClientName?: string;
|
|
44
|
+
detectCopilot?: () => string | undefined;
|
|
31
45
|
}): string | undefined;
|
|
32
46
|
export {};
|
package/dist/launchContext.js
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.codieLaunchFromArgv = codieLaunchFromArgv;
|
|
4
4
|
exports.cursorLaunchFromCommand = cursorLaunchFromCommand;
|
|
5
|
+
exports.copilotLaunchFromCommand = copilotLaunchFromCommand;
|
|
5
6
|
exports.windowsAncestors = windowsAncestors;
|
|
6
7
|
exports.detectCursorLaunch = detectCursorLaunch;
|
|
8
|
+
exports.detectCopilotLaunch = detectCopilotLaunch;
|
|
7
9
|
exports.hookLaunch = hookLaunch;
|
|
8
10
|
const node_child_process_1 = require("node:child_process");
|
|
9
11
|
/**
|
|
10
12
|
* How a harness that records nothing about its own launch was started.
|
|
11
13
|
* Claude Code (`entrypoint`) and Codex (`session_meta.source`) stamp this in
|
|
12
|
-
* the transcript; Cursor and
|
|
13
|
-
* process that launched it. Raw, namespaced values; what counts as
|
|
14
|
+
* the transcript; Cursor, Codie and the Copilot CLI do not, so the hook reads
|
|
15
|
+
* it from the process that launched it. Raw, namespaced values; what counts as
|
|
14
16
|
* "automated" is decided in reporting, not here.
|
|
15
17
|
*/
|
|
16
18
|
const VALUE = /^[a-z0-9][a-z0-9_.:-]{0,39}$/;
|
|
@@ -72,6 +74,45 @@ function cursorFlags(tokens) {
|
|
|
72
74
|
}
|
|
73
75
|
return flags;
|
|
74
76
|
}
|
|
77
|
+
/** The Copilot CLI binary or its npm launcher (`copilot`, `copilot.exe`, `copilot.cmd`). */
|
|
78
|
+
const COPILOT_BIN = /(^|[/\\])copilot(\.(exe|cmd|ps1|js))?$/i;
|
|
79
|
+
const NODE_BIN = /(^|[/\\])node(\.exe)?$/i;
|
|
80
|
+
/**
|
|
81
|
+
* One launching process's command line → a Copilot CLI launch value. The CLI
|
|
82
|
+
* runs as `…/@github/copilot-<platform>/copilot [flags]` under
|
|
83
|
+
* `node …/bin/copilot [flags]`; `-p`/`--prompt` is headless (scripted). VS
|
|
84
|
+
* Code's agent (`copilot-runtime --headless`) is named by workspace.yaml
|
|
85
|
+
* instead, so it never matches here.
|
|
86
|
+
*/
|
|
87
|
+
function copilotLaunchFromCommand(command) {
|
|
88
|
+
const tokens = tokenize(command);
|
|
89
|
+
// Only the program itself (or the script `node` runs): a shell command line
|
|
90
|
+
// that merely mentions "copilot" as a word is not the Copilot CLI.
|
|
91
|
+
const bin = COPILOT_BIN.test(tokens[0] ?? "")
|
|
92
|
+
? 0
|
|
93
|
+
: NODE_BIN.test(tokens[0] ?? "") && COPILOT_BIN.test(tokens[1] ?? "")
|
|
94
|
+
? 1
|
|
95
|
+
: -1;
|
|
96
|
+
if (bin < 0)
|
|
97
|
+
return undefined;
|
|
98
|
+
for (let i = bin + 1; i < tokens.length; i++) {
|
|
99
|
+
const t = tokens[i];
|
|
100
|
+
if (t === "-p" || t === "--prompt" || t.startsWith("--prompt="))
|
|
101
|
+
return "copilot:print";
|
|
102
|
+
// The prompt itself can mention "-p": stop at the first non-flag token.
|
|
103
|
+
if (!t.startsWith("-"))
|
|
104
|
+
break;
|
|
105
|
+
if (COPILOT_VALUE_FLAGS.has(t))
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
return "copilot:cli";
|
|
109
|
+
}
|
|
110
|
+
/** Copilot CLI flags that take a value; the token after them is not a flag. */
|
|
111
|
+
const COPILOT_VALUE_FLAGS = new Set([
|
|
112
|
+
"--model", "--add-dir", "--allow-tool", "--deny-tool", "--resume", "--log-dir",
|
|
113
|
+
"--log-level", "--agent", "--config-dir", "--allow-url", "--deny-url",
|
|
114
|
+
"--additional-mcp-config", "--stream", "--output-format",
|
|
115
|
+
]);
|
|
75
116
|
const psPosix = (pid) => {
|
|
76
117
|
const result = (0, node_child_process_1.spawnSync)("ps", ["-o", "ppid=,args=", "-p", String(pid)], { encoding: "utf8", timeout: 500 });
|
|
77
118
|
const line = result.status === 0 ? result.stdout.trim() : "";
|
|
@@ -105,18 +146,15 @@ function windowsAncestors(startPid, depth) {
|
|
|
105
146
|
return null;
|
|
106
147
|
}
|
|
107
148
|
}
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
* and the hook) and return the first Cursor launch found.
|
|
111
|
-
*/
|
|
112
|
-
function detectCursorLaunch(startPid = process.ppid, deps = {}) {
|
|
149
|
+
/** First launch value `fromCommand` finds among up to `depth` ancestors. */
|
|
150
|
+
function detectLaunch(fromCommand, startPid, deps) {
|
|
113
151
|
const depth = deps.depth ?? 5;
|
|
114
152
|
if ((deps.platform ?? process.platform) === "win32" && !deps.ps) {
|
|
115
153
|
const chain = (deps.windows ?? windowsAncestors)(startPid, depth);
|
|
116
154
|
if (!chain)
|
|
117
155
|
return undefined;
|
|
118
156
|
for (const row of chain) {
|
|
119
|
-
const launch =
|
|
157
|
+
const launch = fromCommand(row.command);
|
|
120
158
|
if (launch)
|
|
121
159
|
return launch;
|
|
122
160
|
}
|
|
@@ -128,18 +166,35 @@ function detectCursorLaunch(startPid = process.ppid, deps = {}) {
|
|
|
128
166
|
const row = ps(pid);
|
|
129
167
|
if (!row)
|
|
130
168
|
return undefined;
|
|
131
|
-
const launch =
|
|
169
|
+
const launch = fromCommand(row.command);
|
|
132
170
|
if (launch)
|
|
133
171
|
return launch;
|
|
134
172
|
pid = row.ppid;
|
|
135
173
|
}
|
|
136
174
|
return undefined;
|
|
137
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Walk up to `depth` ancestors (a shell or `npx` usually sits between Cursor
|
|
178
|
+
* and the hook) and return the first Cursor launch found.
|
|
179
|
+
*/
|
|
180
|
+
function detectCursorLaunch(startPid = process.ppid, deps = {}) {
|
|
181
|
+
return detectLaunch(cursorLaunchFromCommand, startPid, deps);
|
|
182
|
+
}
|
|
183
|
+
/** The Copilot CLI that launched this hook (`copilot:print` / `copilot:cli`). */
|
|
184
|
+
function detectCopilotLaunch(startPid = process.ppid, deps = {}) {
|
|
185
|
+
return detectLaunch(copilotLaunchFromCommand, startPid, deps);
|
|
186
|
+
}
|
|
138
187
|
/** The launch value a hook invocation should carry into its capture job. */
|
|
139
|
-
function hookLaunch(hookCommand, input) {
|
|
188
|
+
function hookLaunch(hookCommand, input, deps = {}) {
|
|
140
189
|
if (typeof input.launch === "string" && VALUE.test(input.launch))
|
|
141
190
|
return input.launch;
|
|
142
191
|
if (hookCommand?.endsWith("-cursor"))
|
|
143
192
|
return detectCursorLaunch();
|
|
193
|
+
if (hookCommand?.endsWith("-copilot")) {
|
|
194
|
+
// VS Code names itself in workspace.yaml; only the CLI needs the walk.
|
|
195
|
+
if (deps.copilotClientName === "vscode-agent-host")
|
|
196
|
+
return "copilot:vscode";
|
|
197
|
+
return (deps.detectCopilot ?? detectCopilotLaunch)();
|
|
198
|
+
}
|
|
144
199
|
return undefined;
|
|
145
200
|
}
|
package/dist/limitEvent.d.ts
CHANGED
|
@@ -9,4 +9,7 @@ export declare function classifyLimitKind(text: string): LimitKind;
|
|
|
9
9
|
export declare function parseLimitEventFromTranscript(transcript: string): LimitEvent | null;
|
|
10
10
|
/** Parse the structured rate-limit snapshot written to Codex rollout JSONL. */
|
|
11
11
|
export declare function parseCodexLimitEventFromTranscript(transcript: string): LimitEvent | null;
|
|
12
|
+
/** Only explicit provider errors qualify; never scan user/assistant prose. */
|
|
13
|
+
export declare function parseNativeLimitError(input: Record<string, unknown>, now?: number): LimitEvent | null;
|
|
14
|
+
export declare function parseNativeLimitEventFromTranscript(transcript: string): LimitEvent | null;
|
|
12
15
|
export declare function tailForLimitEvent(transcript: string, maxChars?: number): string;
|