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.
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.codieLaunchFromArgv = codieLaunchFromArgv;
4
+ exports.cursorLaunchFromCommand = cursorLaunchFromCommand;
5
+ exports.copilotLaunchFromCommand = copilotLaunchFromCommand;
6
+ exports.windowsAncestors = windowsAncestors;
7
+ exports.detectCursorLaunch = detectCursorLaunch;
8
+ exports.detectCopilotLaunch = detectCopilotLaunch;
9
+ exports.hookLaunch = hookLaunch;
10
+ const node_child_process_1 = require("node:child_process");
11
+ /**
12
+ * How a harness that records nothing about its own launch was started.
13
+ * Claude Code (`entrypoint`) and Codex (`session_meta.source`) stamp this in
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
16
+ * "automated" is decided in reporting, not here.
17
+ */
18
+ const VALUE = /^[a-z0-9][a-z0-9_.:-]{0,39}$/;
19
+ /** Codie (OpenCode) subcommands, read in-process by the capture plugin. */
20
+ const CODIE_SUBCOMMANDS = ["run", "serve", "acp", "web", "attach"];
21
+ function codieLaunchFromArgv(argv) {
22
+ const sub = argv.find((a) => CODIE_SUBCOMMANDS.includes(a));
23
+ return sub ? `codie:${sub}` : "codie:tui";
24
+ }
25
+ /**
26
+ * Split a command line into tokens, honouring double quotes (Windows quotes
27
+ * paths with spaces: "C:\\Program Files\\…\\node.exe" "…\\index.js" -p hi).
28
+ */
29
+ function tokenize(command) {
30
+ const tokens = [];
31
+ for (const m of command.matchAll(/"([^"]*)"|(\S+)/g))
32
+ tokens.push(m[1] ?? m[2]);
33
+ return tokens;
34
+ }
35
+ /** A path segment named cursor-agent / agent (Unix or Windows separators). */
36
+ const CURSOR_AGENT_PATH = /(^|[/\\])cursor-agent([/\\]|\.(exe|cmd|ps1)$|$)/i;
37
+ const AGENT_BIN = /(^|[/\\])(cursor-)?agent(\.(exe|cmd|ps1))?$/i;
38
+ /** One launching process's command line → a Cursor launch value. */
39
+ function cursorLaunchFromCommand(command) {
40
+ const tokens = tokenize(command);
41
+ // The CLI runs as `<launcher> [node] …/cursor-agent/versions/<v>/index.js [flags] [prompt]`
42
+ // on macOS/Linux (~/.local/share/cursor-agent) and Windows (%LOCALAPPDATA%\cursor-agent).
43
+ const isAgent = tokens.some((t) => CURSOR_AGENT_PATH.test(t) || /[/\\]\.local[/\\]share[/\\]cursor/i.test(t))
44
+ && tokens.some((t) => AGENT_BIN.test(t) || /index\.js$/i.test(t));
45
+ if (isAgent) {
46
+ return cursorFlags(tokens).some((f) => f === "-p" || f === "--print" || f.startsWith("--print="))
47
+ ? "cursor:print"
48
+ : "cursor:cli";
49
+ }
50
+ if (/Cursor\.app|Cursor Helper|[/\\]cursor(\.exe)?(\s|"|$)/i.test(command))
51
+ return "cursor:ide";
52
+ return undefined;
53
+ }
54
+ /** Cursor flags that take a value; the token after them is not the prompt. */
55
+ const CURSOR_VALUE_FLAGS = new Set(["--model", "-m", "--output-format", "--api-key", "--resume", "--workspace", "--mode", "-H", "--header"]);
56
+ /**
57
+ * The flags before the prompt. The prompt is on the command line too, so a
58
+ * prompt mentioning "-p" must not read as headless: stop at the first token
59
+ * that is neither a flag nor a flag's value. Starts after the launcher script
60
+ * (`…/index.js`) when present, else after the agent binary.
61
+ */
62
+ function cursorFlags(tokens) {
63
+ let start = tokens.findIndex((t) => /index\.js$/i.test(t));
64
+ if (start < 0)
65
+ start = tokens.findIndex((t) => AGENT_BIN.test(t));
66
+ const flags = [];
67
+ for (let i = (start >= 0 ? start : 0) + 1; i < tokens.length; i++) {
68
+ const t = tokens[i];
69
+ if (!t.startsWith("-"))
70
+ break;
71
+ flags.push(t);
72
+ if (CURSOR_VALUE_FLAGS.has(t))
73
+ i++;
74
+ }
75
+ return flags;
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
+ ]);
116
+ const psPosix = (pid) => {
117
+ const result = (0, node_child_process_1.spawnSync)("ps", ["-o", "ppid=,args=", "-p", String(pid)], { encoding: "utf8", timeout: 500 });
118
+ const line = result.status === 0 ? result.stdout.trim() : "";
119
+ const match = line.match(/^(\d+)\s+(.*)$/s);
120
+ return match ? { ppid: Number(match[1]), command: match[2] } : null;
121
+ };
122
+ /**
123
+ * Windows: one PowerShell call returns the whole ancestor chain (spawning
124
+ * PowerShell per level would cost ~0.5s each). CommandLine needs CIM, which
125
+ * every supported Windows has; any failure means "unknown", never a guess.
126
+ */
127
+ function windowsAncestors(startPid, depth) {
128
+ const script = `$p=${Math.trunc(startPid)};$o=@();for($i=0;$i -lt ${Math.trunc(depth)} -and $p -gt 0;$i++){` +
129
+ `$x=Get-CimInstance Win32_Process -Filter "ProcessId=$p" -ErrorAction SilentlyContinue;if(!$x){break};` +
130
+ `$o+=[pscustomobject]@{pid=[int]$p;ppid=[int]$x.ParentProcessId;command=[string]$x.CommandLine};$p=$x.ParentProcessId};` +
131
+ `ConvertTo-Json -InputObject @($o) -Compress`;
132
+ const result = (0, node_child_process_1.spawnSync)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
133
+ encoding: "utf8",
134
+ // A cold PowerShell start can take several seconds on a busy machine; the
135
+ // capture hook has 30s, and an unanswered walk just means "unknown".
136
+ timeout: 10000,
137
+ windowsHide: true,
138
+ });
139
+ if (result.status !== 0 || !result.stdout.trim())
140
+ return null;
141
+ try {
142
+ const rows = JSON.parse(result.stdout);
143
+ return rows.map((r) => ({ pid: Number(r.pid), ppid: Number(r.ppid), command: r.command ?? "" }));
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ /** First launch value `fromCommand` finds among up to `depth` ancestors. */
150
+ function detectLaunch(fromCommand, startPid, deps) {
151
+ const depth = deps.depth ?? 5;
152
+ if ((deps.platform ?? process.platform) === "win32" && !deps.ps) {
153
+ const chain = (deps.windows ?? windowsAncestors)(startPid, depth);
154
+ if (!chain)
155
+ return undefined;
156
+ for (const row of chain) {
157
+ const launch = fromCommand(row.command);
158
+ if (launch)
159
+ return launch;
160
+ }
161
+ return undefined;
162
+ }
163
+ const ps = deps.ps ?? psPosix;
164
+ let pid = startPid;
165
+ for (let i = 0; i < depth && pid > 1; i++) {
166
+ const row = ps(pid);
167
+ if (!row)
168
+ return undefined;
169
+ const launch = fromCommand(row.command);
170
+ if (launch)
171
+ return launch;
172
+ pid = row.ppid;
173
+ }
174
+ return undefined;
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
+ }
187
+ /** The launch value a hook invocation should carry into its capture job. */
188
+ function hookLaunch(hookCommand, input, deps = {}) {
189
+ if (typeof input.launch === "string" && VALUE.test(input.launch))
190
+ return input.launch;
191
+ if (hookCommand?.endsWith("-cursor"))
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
+ }
199
+ return undefined;
200
+ }
@@ -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;
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.classifyLimitKind = classifyLimitKind;
4
4
  exports.parseLimitEventFromTranscript = parseLimitEventFromTranscript;
5
5
  exports.parseCodexLimitEventFromTranscript = parseCodexLimitEventFromTranscript;
6
+ exports.parseNativeLimitError = parseNativeLimitError;
7
+ exports.parseNativeLimitEventFromTranscript = parseNativeLimitEventFromTranscript;
6
8
  exports.tailForLimitEvent = tailForLimitEvent;
7
9
  function classifyLimitKind(text) {
8
10
  const t = text.toLowerCase();
@@ -129,6 +131,46 @@ function parseCodexLimitEventFromTranscript(transcript) {
129
131
  }
130
132
  return found;
131
133
  }
134
+ /** Only explicit provider errors qualify; never scan user/assistant prose. */
135
+ function parseNativeLimitError(input, now = Date.now()) {
136
+ const context = input.errorContext ?? input.error_context;
137
+ if (context !== undefined && context !== "model_call")
138
+ return null;
139
+ const error = input.error;
140
+ const fields = error && typeof error === "object" ? error : {};
141
+ const code = String(fields.code ?? fields.name ?? input.error_code ?? "");
142
+ const text = typeof error === "string" ? error
143
+ : typeof fields.message === "string" ? fields.message
144
+ : typeof input.error_message === "string" ? input.error_message : "";
145
+ if (!/^(?:429|rate[_-]?limit(?:ed|_exceeded|_error)?|RateLimitError|quota_exceeded)$/i.test(code)
146
+ && !/\b(?:rate limit (?:exceeded|reached)|too many requests|usage limit (?:exceeded|reached)|premium requests? (?:exhausted|limit reached))\b/i.test(text))
147
+ return null;
148
+ const rawAt = input.timestamp;
149
+ const parsedAt = typeof rawAt === "number" ? rawAt : typeof rawAt === "string" ? Date.parse(rawAt) : NaN;
150
+ return { kind: classifyLimitKind(text), at: Number.isFinite(parsedAt) && parsedAt > 0 ? parsedAt : now,
151
+ resetText: parseResetText(text), reason: code || "rate_limit" };
152
+ }
153
+ function parseNativeLimitEventFromTranscript(transcript) {
154
+ let found = null;
155
+ for (const line of transcript.split("\n")) {
156
+ try {
157
+ const entry = JSON.parse(line);
158
+ if (entry.type === "agent.limit" && Number.isFinite(entry.at) && entry.at > 0
159
+ && ["session", "user_spend", "org_spend", "other"].includes(entry.kind)) {
160
+ found = { kind: entry.kind, at: entry.at, resetText: typeof entry.resetText === "string" ? entry.resetText : null,
161
+ ...(typeof entry.reason === "string" ? { reason: entry.reason } : {}) };
162
+ continue;
163
+ }
164
+ if (!entry.timestamp)
165
+ continue; // no stable timestamp means snapshots cannot deduplicate
166
+ if (entry.type === "agent.error" || entry.type === "session.error") {
167
+ found = parseNativeLimitError({ ...(entry.data ?? entry), timestamp: entry.timestamp }) ?? found;
168
+ }
169
+ }
170
+ catch { /* tolerant of incomplete transcript tails */ }
171
+ }
172
+ return found;
173
+ }
132
174
  // Tail of the raw transcript to ship with the limit event so the server can
133
175
  // summarize "what they were working on" immediately — without waiting for the
134
176
  // SessionEnd capture (which can lag hours, or drop entirely on Windows).
@@ -1,5 +1,5 @@
1
1
  import { type LimitEvent } from "./limitEvent";
2
- export type LimitEventSource = "claude-code" | "codex";
2
+ export type LimitEventSource = "claude-code" | "codex" | "cursor" | "copilot";
3
3
  export type LimitEventPayload = LimitEvent & {
4
4
  source: LimitEventSource;
5
5
  sessionId?: string;
@@ -76,7 +76,7 @@ function postLimitEvent(config, payload, timeoutMs = 5_000) {
76
76
  async function reportTranscriptLimitEvent(args) {
77
77
  const parse = args.source === "codex"
78
78
  ? limitEvent_1.parseCodexLimitEventFromTranscript
79
- : limitEvent_1.parseLimitEventFromTranscript;
79
+ : args.source === "cursor" || args.source === "copilot" ? limitEvent_1.parseNativeLimitEventFromTranscript : limitEvent_1.parseLimitEventFromTranscript;
80
80
  const parsed = parse(args.transcript);
81
81
  if (!parsed && !args.emitMinimalOnMiss)
82
82
  return false;
@@ -0,0 +1,5 @@
1
+ /** Native records only: quoted denial/interrupt text in conversation is not evidence. */
2
+ export declare function parseNativeSteering(content: string): {
3
+ interrupts: number;
4
+ rejections: number;
5
+ };
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseNativeSteering = parseNativeSteering;
4
+ /** Native records only: quoted denial/interrupt text in conversation is not evidence. */
5
+ function parseNativeSteering(content) {
6
+ const interrupts = new Set();
7
+ const abortedTurns = new Set();
8
+ const rejections = new Set();
9
+ for (const [index, line] of content.split("\n").entries()) {
10
+ try {
11
+ const event = JSON.parse(line);
12
+ const id = String(event.id ?? event.uuid ?? event.toolCallId ?? index);
13
+ if (event.type === "agent.interruption")
14
+ interrupts.add(id);
15
+ if (event.type === "turn_ended" && event.status === "aborted")
16
+ abortedTurns.add(id);
17
+ if (event.type === "agent.rejection")
18
+ rejections.add(id);
19
+ // Cursor tool failures distinguish a user-denied permission from an
20
+ // ordinary tool failure. Don't guess from free-form error messages.
21
+ if (event.type === "tool_result" && event.error?.code === "user_denied")
22
+ rejections.add(id);
23
+ }
24
+ catch { /* incomplete transcript records are ignored */ }
25
+ }
26
+ return { interrupts: Math.max(interrupts.size, abortedTurns.size), rejections: rejections.size };
27
+ }
@@ -0,0 +1,36 @@
1
+ import { type CaptureSignals } from "./captureSignals";
2
+ import { type TranscriptModelUsage } from "./transcriptModels";
3
+ /**
4
+ * Claude Code writes each subagent's transcript next to its parent's:
5
+ *
6
+ * ~/.claude/projects/<project>/<sessionId>.jsonl (parent)
7
+ * ~/.claude/projects/<project>/<sessionId>/subagents/agent-*.jsonl
8
+ *
9
+ * Lines there carry `isSidechain: true` and the parent's sessionId. They are
10
+ * part of the parent session's work, not sessions of their own: their skill
11
+ * runs, tool calls and token usage fold into the parent's signals and usage,
12
+ * and they are never uploaded separately. Other harnesses (Codex, Codie,
13
+ * Cursor) keep sub-agents inside the one transcript or don't have them, so
14
+ * this is Claude Code only.
15
+ */
16
+ export declare const SUBAGENT_LIMITS: {
17
+ files: number;
18
+ bytes: number;
19
+ };
20
+ export declare function subagentDir(transcriptPath: string, sessionId?: string): string;
21
+ export type SubagentTranscripts = {
22
+ contents: string[];
23
+ count: number;
24
+ truncated: boolean;
25
+ };
26
+ /** Read the session's subagent transcripts in full, up to 20 files / 20 MB
27
+ * in total (oldest first); anything past that is counted as truncated. */
28
+ export declare function readSubagentTranscripts(transcriptPath: string, sessionId?: string, limits?: {
29
+ files: number;
30
+ bytes: number;
31
+ }): SubagentTranscripts;
32
+ /** Parent signals plus every subagent's skill runs, pulls and tool calls.
33
+ * Launch values, timing and record counts stay the parent's own. */
34
+ export declare function foldSubagentSignals(parent: CaptureSignals, subs: SubagentTranscripts, source?: string): CaptureSignals;
35
+ /** Parent model usage plus each subagent's, summed per model. */
36
+ export declare function foldSubagentModelUsage(parent: TranscriptModelUsage[], subs: SubagentTranscripts, source?: string): TranscriptModelUsage[];
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SUBAGENT_LIMITS = void 0;
7
+ exports.subagentDir = subagentDir;
8
+ exports.readSubagentTranscripts = readSubagentTranscripts;
9
+ exports.foldSubagentSignals = foldSubagentSignals;
10
+ exports.foldSubagentModelUsage = foldSubagentModelUsage;
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const captureSignals_1 = require("./captureSignals");
14
+ const transcriptModels_1 = require("./transcriptModels");
15
+ /**
16
+ * Claude Code writes each subagent's transcript next to its parent's:
17
+ *
18
+ * ~/.claude/projects/<project>/<sessionId>.jsonl (parent)
19
+ * ~/.claude/projects/<project>/<sessionId>/subagents/agent-*.jsonl
20
+ *
21
+ * Lines there carry `isSidechain: true` and the parent's sessionId. They are
22
+ * part of the parent session's work, not sessions of their own: their skill
23
+ * runs, tool calls and token usage fold into the parent's signals and usage,
24
+ * and they are never uploaded separately. Other harnesses (Codex, Codie,
25
+ * Cursor) keep sub-agents inside the one transcript or don't have them, so
26
+ * this is Claude Code only.
27
+ */
28
+ exports.SUBAGENT_LIMITS = { files: 20, bytes: 20 * 1024 * 1024 };
29
+ function subagentDir(transcriptPath, sessionId) {
30
+ const dir = node_path_1.default.dirname(transcriptPath);
31
+ const id = sessionId || node_path_1.default.basename(transcriptPath).replace(/\.jsonl$/i, "");
32
+ return node_path_1.default.join(dir, id, "subagents");
33
+ }
34
+ /** Read the session's subagent transcripts in full, up to 20 files / 20 MB
35
+ * in total (oldest first); anything past that is counted as truncated. */
36
+ function readSubagentTranscripts(transcriptPath, sessionId, limits = exports.SUBAGENT_LIMITS) {
37
+ const dir = subagentDir(transcriptPath, sessionId);
38
+ let names;
39
+ try {
40
+ names = node_fs_1.default.readdirSync(dir).filter((n) => /^agent-[^/]+\.jsonl$/.test(n));
41
+ }
42
+ catch {
43
+ return { contents: [], count: 0, truncated: false };
44
+ }
45
+ const files = names
46
+ .map((n) => {
47
+ const full = node_path_1.default.join(dir, n);
48
+ try {
49
+ const st = node_fs_1.default.statSync(full);
50
+ return st.isFile() ? { full, size: st.size, mtime: st.mtimeMs } : null;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ })
56
+ .filter((f) => f !== null)
57
+ .sort((a, b) => a.mtime - b.mtime || a.full.localeCompare(b.full));
58
+ const contents = [];
59
+ let bytes = 0;
60
+ let truncated = false;
61
+ for (const f of files) {
62
+ if (contents.length >= limits.files || bytes + f.size > limits.bytes) {
63
+ truncated = true;
64
+ break;
65
+ }
66
+ try {
67
+ contents.push(node_fs_1.default.readFileSync(f.full, "utf8"));
68
+ bytes += f.size;
69
+ }
70
+ catch {
71
+ truncated = true;
72
+ }
73
+ }
74
+ return { contents, count: files.length, truncated };
75
+ }
76
+ /** Parent signals plus every subagent's skill runs, pulls and tool calls.
77
+ * Launch values, timing and record counts stay the parent's own. */
78
+ function foldSubagentSignals(parent, subs, source) {
79
+ if (subs.count === 0)
80
+ return parent;
81
+ const counts = new Map();
82
+ const add = (list, fallback) => {
83
+ for (const c of list ?? fallback.map((skill) => ({ skill, count: 1 })))
84
+ counts.set(c.skill, (counts.get(c.skill) ?? 0) + c.count);
85
+ };
86
+ add(parent.skillInvocationCounts, parent.skillInvocations);
87
+ const invocations = new Set(parent.skillInvocations);
88
+ const pulls = new Set(parent.skillPulls);
89
+ const tools = new Map(parent.toolCalls.map((t) => [t.name, t.calls]));
90
+ let truncated = parent.truncated;
91
+ for (const content of subs.contents) {
92
+ const s = (0, captureSignals_1.parseCaptureSignals)(content, source);
93
+ add(s.skillInvocationCounts, s.skillInvocations);
94
+ for (const k of s.skillInvocations)
95
+ invocations.add(k);
96
+ for (const k of s.skillPulls)
97
+ pulls.add(k);
98
+ for (const t of s.toolCalls)
99
+ tools.set(t.name, (tools.get(t.name) ?? 0) + t.calls);
100
+ truncated ||= s.truncated;
101
+ }
102
+ const cap = (xs) => {
103
+ if (xs.length > 100)
104
+ truncated = true;
105
+ return xs.slice(0, 100);
106
+ };
107
+ return {
108
+ ...parent,
109
+ skillInvocations: cap([...invocations].sort()),
110
+ skillInvocationCounts: cap([...counts].map(([skill, count]) => ({ skill, count: Math.min(count, 10_000) })).sort((a, b) => a.skill.localeCompare(b.skill))),
111
+ skillPulls: cap([...pulls].sort()),
112
+ toolCalls: cap([...tools].map(([name, calls]) => ({ name, calls })).sort((a, b) => a.name.localeCompare(b.name))),
113
+ subagents: { count: subs.count, truncated: subs.truncated },
114
+ truncated,
115
+ };
116
+ }
117
+ /** Parent model usage plus each subagent's, summed per model. */
118
+ function foldSubagentModelUsage(parent, subs, source) {
119
+ if (subs.contents.length === 0)
120
+ return parent;
121
+ const byModel = new Map();
122
+ const add = (u) => {
123
+ const cur = byModel.get(u.model);
124
+ if (!cur) {
125
+ byModel.set(u.model, { ...u });
126
+ return;
127
+ }
128
+ cur.requests += u.requests;
129
+ cur.inputTokens += u.inputTokens;
130
+ cur.outputTokens += u.outputTokens;
131
+ cur.cacheReadTokens += u.cacheReadTokens;
132
+ cur.cacheCreationTokens += u.cacheCreationTokens;
133
+ if (u.costUsd !== undefined)
134
+ cur.costUsd = (cur.costUsd ?? 0) + u.costUsd;
135
+ if (u.reportedCostUsd !== undefined)
136
+ cur.reportedCostUsd = (cur.reportedCostUsd ?? 0) + u.reportedCostUsd;
137
+ if (u.reasoningTokens !== undefined)
138
+ cur.reasoningTokens = (cur.reasoningTokens ?? 0) + u.reasoningTokens;
139
+ };
140
+ for (const u of parent)
141
+ add(u);
142
+ for (const content of subs.contents)
143
+ for (const u of (0, transcriptModels_1.parseTranscriptModels)(content, source))
144
+ add(u);
145
+ return [...byModel.values()].slice(0, 64);
146
+ }
@@ -0,0 +1,9 @@
1
+ /** Session-start context uses each client's supported output contract. */
2
+ export declare const TEAM_SKILL_CONTEXT = "This machine is connected to Codiedev, the team's artifact and skill library.\nBefore a substantial coding task, run codiedev search \"<brief task description>\" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.\nUse codiedev push <file.md> to share an artifact when asked, codiedev ask \"<question>\" for team knowledge, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.";
3
+ export declare function teamSkillSessionOutput(source: "cursor" | "copilot", context?: string): {
4
+ additional_context: string;
5
+ additionalContext?: undefined;
6
+ } | {
7
+ additionalContext: string;
8
+ additional_context?: undefined;
9
+ };
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TEAM_SKILL_CONTEXT = void 0;
4
+ exports.teamSkillSessionOutput = teamSkillSessionOutput;
5
+ /** Session-start context uses each client's supported output contract. */
6
+ exports.TEAM_SKILL_CONTEXT = `This machine is connected to Codiedev, the team's artifact and skill library.
7
+ Before a substantial coding task, run codiedev search "<brief task description>" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.
8
+ Use codiedev push <file.md> to share an artifact when asked, codiedev ask "<question>" for team knowledge, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.`;
9
+ function teamSkillSessionOutput(source, context = exports.TEAM_SKILL_CONTEXT) {
10
+ return source === "cursor" ? { additional_context: context } : { additionalContext: context };
11
+ }
@@ -1,6 +1,8 @@
1
1
  /** Compact per-model usage, derived from captured transcript records. */
2
2
  export type TranscriptModelUsage = {
3
3
  model: string;
4
+ /** False for Copilot model/request hints before token totals arrive. */
5
+ tokensAvailable?: boolean;
4
6
  /** Assistant message IDs; nonzero cumulative usage events for Codex. */
5
7
  requests: number;
6
8
  inputTokens: number;
@@ -44,6 +44,7 @@ function* transcriptRecords(content, source) {
44
44
  function parseTranscriptModels(content, source) {
45
45
  const buckets = new Map();
46
46
  const messages = new Map();
47
+ const tokenEvidence = new Set();
47
48
  let model = "Unattributed";
48
49
  let previous = {
49
50
  inputTokens: 0,
@@ -143,9 +144,12 @@ function parseTranscriptModels(content, source) {
143
144
  model =
144
145
  typeof message.model === "string" ? message.model : "Unattributed";
145
146
  const raw = message.usage;
147
+ // Copilot's shutdown token totals carry `requests: 0`: its calls are
148
+ // already counted from the assistant records.
149
+ const requests = event.requests;
146
150
  usage = {
147
151
  ...empty(model),
148
- requests: 1,
152
+ requests: typeof requests === "number" && Number.isFinite(requests) ? Math.max(0, requests) : 1,
149
153
  inputTokens: positive(raw.input_tokens),
150
154
  outputTokens: positive(raw.output_tokens),
151
155
  cacheReadTokens: positive(raw.cache_read_input_tokens),
@@ -153,6 +157,8 @@ function parseTranscriptModels(content, source) {
153
157
  };
154
158
  messageId = message.id ?? event.uuid ?? messageId;
155
159
  }
160
+ if (source === "copilot" && event.type === "copilot.usage")
161
+ tokenEvidence.add(usage.model);
156
162
  // Client placeholders are not model executions.
157
163
  if (!usage.model.trim() || usage.model.startsWith("<"))
158
164
  continue;
@@ -197,7 +203,7 @@ function parseTranscriptModels(content, source) {
197
203
  delete buckets.get(key).costUsd;
198
204
  for (const key of incompleteReasoning)
199
205
  delete buckets.get(key).reasoningTokens;
200
- return [...buckets.values()];
206
+ return [...buckets.values()].map(row => source === "copilot" && !tokenEvidence.has(row.model) ? { ...row, tokensAvailable: false } : row);
201
207
  }
202
208
  function totalModelTokens(row) {
203
209
  return (row.inputTokens +
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import type { CaptureSignals } from "./captureSignals";
3
- import type { TranscriptModelUsage } from "./transcriptModels";
3
+ import { type TranscriptModelUsage } from "./transcriptModels";
4
4
  import { spawn as nodeSpawn } from "child_process";
5
5
  import type { CaptureLogEntry } from "./captureLog";
6
6
  import { codiedevStateDir } from "./configDir";
@@ -89,6 +89,7 @@ export interface CapturePayloadInput {
89
89
  final?: boolean;
90
90
  costUsd?: number;
91
91
  }
92
+ export declare function stripRepoUrlCredentials(url: string): string;
92
93
  export declare function buildCapturePayload(input: CapturePayloadInput): Record<string, unknown>;
93
94
  export declare function hashToken(token: string): string;
94
95
  export declare function extractHookNodePath(cmd: string | undefined): string | undefined;
@@ -96,6 +97,12 @@ export declare function resetStableNodeCacheForTests(): void;
96
97
  export declare function claudeCodeInstalled(): boolean;
97
98
  export declare function codexInstalled(): boolean;
98
99
  export declare function cursorInstalled(): boolean;
100
+ /**
101
+ * GitHub Copilot: `$COPILOT_HOME` / ~/.copilot (either surface has run once),
102
+ * the `copilot` CLI on PATH, or VS Code's Copilot Chat extension. VS Code's
103
+ * agent and the CLI read the same user hooks, so one install covers both.
104
+ */
105
+ export declare function copilotInstalled(): boolean;
99
106
  export declare function openCodeInstalled(): boolean;
100
107
  /** Install a dormant global OpenCode adapter. Installing it unconditionally
101
108
  * keeps `codiedev connect` as the only setup step even when Codie/OpenCode is
@@ -116,6 +123,8 @@ export declare function ensureAllCodiedevHooksInstalled(): void;
116
123
  export declare function ensureSessionStartHookInstalled(): void;
117
124
  export declare function installClaudeCodeInstructions(): void;
118
125
  export declare function installCodexInstructions(): void;
126
+ export declare function copilotInstructionsPath(): string;
127
+ export declare function installCopilotInstructions(): void;
119
128
  export declare function installCursorInstructions(): void;
120
129
  /**
121
130
  * Install the Codiedev MCP server into Claude Code's user-scope config.
@@ -173,6 +182,13 @@ export declare function installCodexMcp(): boolean;
173
182
  export declare function installCodexHook(): void;
174
183
  export declare function ensureCodexLimitHookInstalled(): void;
175
184
  export declare function installCursorHook(): void;
185
+ export declare function copilotHooksPath(): string;
186
+ export declare function installCopilotHook(agentSecurityEnabled?: boolean): void;
187
+ /** Self-heal the Copilot hook file after upgrades or drift (like Cursor's). */
188
+ export declare function ensureCopilotCodiedevHooksInstalled(): void;
189
+ /** Repair Cursor's complete flat hook set after upgrades or configuration
190
+ * drift. This is deliberately separate from Claude Code's nested-hook healer:
191
+ * the schemas and event names differ, and mixing them risks corrupting both. */
176
192
  export declare function ensureCursorCodiedevHooksInstalled(): void;
177
193
  export interface ParsedStats {
178
194
  messageCount: number;
@@ -184,7 +200,7 @@ export interface SteeringStats {
184
200
  }
185
201
  export declare function parseClaudeSteering(content: string): SteeringStats;
186
202
  export declare function parseCodexSteering(content: string): SteeringStats;
187
- export declare function parseSteering(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode"): SteeringStats;
203
+ export declare function parseSteering(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "copilot"): SteeringStats;
188
204
  export interface UsageStats {
189
205
  inputTokens: number;
190
206
  outputTokens: number;
@@ -196,7 +212,7 @@ export interface UsageStats {
196
212
  /** Engine-reported spend (opencode SDK `info.cost`). Desktop mode only. */
197
213
  costUsd?: number;
198
214
  }
199
- export declare function parseUsage(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "desktop"): UsageStats;
215
+ export declare function parseUsage(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "desktop" | "copilot"): UsageStats;
200
216
  export declare function parseSessionStart(content: string): number | null;
201
217
  export declare function parseClaudeCodeStats(content: string): ParsedStats;
202
218
  /** Convert OpenCode's SDK message array (`{info, parts}[]`) into the same
@@ -231,6 +247,9 @@ export interface CaptureFinishInput {
231
247
  cwd?: string;
232
248
  workspace_roots?: string[];
233
249
  final?: boolean;
250
+ /** How the harness was launched when its transcript does not record it
251
+ * (Cursor, Codie). See launchContext.ts. */
252
+ launch?: string;
234
253
  }
235
254
  export declare function dispatchCaptureFinish(input: CaptureFinishInput, hookScriptPath: string, deps: DispatchDeps): void;
236
255
  export declare function dispatchCaptureDetached(job: CaptureUploadJob, hookScriptPath: string, deps: DispatchDeps): void;