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,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
|
+
}
|
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;
|
package/dist/limitEvent.js
CHANGED
|
@@ -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,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;
|
package/dist/transcriptModels.js
CHANGED
|
@@ -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
|
|
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;
|