codiedev 0.9.60 → 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 +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 +157 -42
- package/package.json +8 -5
|
@@ -0,0 +1,458 @@
|
|
|
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.COPILOT_VSCODE_CLIENT = void 0;
|
|
37
|
+
exports.copilotHome = copilotHome;
|
|
38
|
+
exports.copilotSessionDir = copilotSessionDir;
|
|
39
|
+
exports.copilotTranscriptPath = copilotTranscriptPath;
|
|
40
|
+
exports.parseCopilotWorkspace = parseCopilotWorkspace;
|
|
41
|
+
exports.readCopilotWorkspace = readCopilotWorkspace;
|
|
42
|
+
exports.copilotLaunch = copilotLaunch;
|
|
43
|
+
exports.normalizeCopilotHookInput = normalizeCopilotHookInput;
|
|
44
|
+
exports.copilotPreToolBlockResponse = copilotPreToolBlockResponse;
|
|
45
|
+
exports.isCopilotTranscriptEmpty = isCopilotTranscriptEmpty;
|
|
46
|
+
exports.hasCopilotShutdown = hasCopilotShutdown;
|
|
47
|
+
exports.normalizeCopilotTranscript = normalizeCopilotTranscript;
|
|
48
|
+
const fs = __importStar(require("node:fs"));
|
|
49
|
+
const os = __importStar(require("node:os"));
|
|
50
|
+
const path = __importStar(require("node:path"));
|
|
51
|
+
/**
|
|
52
|
+
* GitHub Copilot: VS Code Copilot Chat's agent and the Copilot CLI run on the
|
|
53
|
+
* same runtime and write the same session record,
|
|
54
|
+
* `$COPILOT_HOME/session-state/<sessionId>/events.jsonl` (plus
|
|
55
|
+
* `workspace.yaml`), and both read user hooks from `$COPILOT_HOME/hooks/*.json`.
|
|
56
|
+
* Everything Copilot-specific is translated here, at the process boundary, so
|
|
57
|
+
* the shared capture pipeline only ever sees the Claude-shaped transcript it
|
|
58
|
+
* already parses.
|
|
59
|
+
*/
|
|
60
|
+
/** `$COPILOT_HOME`, else `~/.copilot` (the runtime honours the same variable). */
|
|
61
|
+
function copilotHome(env = process.env, home = os.homedir()) {
|
|
62
|
+
const override = env.COPILOT_HOME?.trim();
|
|
63
|
+
return override ? path.resolve(override) : path.join(home, ".copilot");
|
|
64
|
+
}
|
|
65
|
+
const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
66
|
+
function copilotSessionDir(sessionId, home = copilotHome()) {
|
|
67
|
+
return SESSION_ID.test(sessionId) ? path.join(home, "session-state", sessionId) : undefined;
|
|
68
|
+
}
|
|
69
|
+
/** sessionEnd carries no transcript path; the session id names the file. */
|
|
70
|
+
function copilotTranscriptPath(sessionId, home = copilotHome()) {
|
|
71
|
+
const dir = sessionId ? copilotSessionDir(sessionId, home) : undefined;
|
|
72
|
+
return dir ? path.join(dir, "events.jsonl") : undefined;
|
|
73
|
+
}
|
|
74
|
+
/** workspace.yaml is flat `key: value` lines; no YAML dependency needed. */
|
|
75
|
+
function parseCopilotWorkspace(yaml) {
|
|
76
|
+
const fields = new Map();
|
|
77
|
+
for (const line of yaml.split(/\r?\n/)) {
|
|
78
|
+
const match = line.match(/^([a-z_]+):\s*(.*)$/);
|
|
79
|
+
if (!match)
|
|
80
|
+
continue;
|
|
81
|
+
let value = match[2].trim();
|
|
82
|
+
if (/^(["']).*\1$/.test(value))
|
|
83
|
+
value = value.slice(1, -1);
|
|
84
|
+
if (value)
|
|
85
|
+
fields.set(match[1], value);
|
|
86
|
+
}
|
|
87
|
+
const pick = (key) => fields.get(key);
|
|
88
|
+
return {
|
|
89
|
+
...(pick("id") ? { id: pick("id") } : {}),
|
|
90
|
+
...(pick("cwd") ? { cwd: pick("cwd") } : {}),
|
|
91
|
+
...(pick("git_root") ? { gitRoot: pick("git_root") } : {}),
|
|
92
|
+
...(pick("branch") ? { branch: pick("branch") } : {}),
|
|
93
|
+
...(pick("client_name") ? { clientName: pick("client_name") } : {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** The workspace.yaml beside a transcript (or for a session id); {} if absent. */
|
|
97
|
+
function readCopilotWorkspace(transcriptPathOrDir) {
|
|
98
|
+
if (!transcriptPathOrDir)
|
|
99
|
+
return {};
|
|
100
|
+
const dir = transcriptPathOrDir.endsWith(".jsonl") ? path.dirname(transcriptPathOrDir) : transcriptPathOrDir;
|
|
101
|
+
try {
|
|
102
|
+
return parseCopilotWorkspace(fs.readFileSync(path.join(dir, "workspace.yaml"), "utf8"));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.COPILOT_VSCODE_CLIENT = "vscode-agent-host";
|
|
109
|
+
const isVSCodeClient = (name) => name === exports.COPILOT_VSCODE_CLIENT || name === "vscode";
|
|
110
|
+
/**
|
|
111
|
+
* How the session was launched. VS Code names itself in workspace.yaml; the
|
|
112
|
+
* CLI doesn't record whether it ran headless, so that comes from the process
|
|
113
|
+
* that launched the hook (`detected`, see launchContext). Unknown stays
|
|
114
|
+
* unknown rather than guessing "human".
|
|
115
|
+
*/
|
|
116
|
+
function copilotLaunch(clientName, detected) {
|
|
117
|
+
if (isVSCodeClient(clientName))
|
|
118
|
+
return "copilot:vscode";
|
|
119
|
+
return detected;
|
|
120
|
+
}
|
|
121
|
+
const firstString = (...values) => values.find((value) => typeof value === "string" && value.trim().length > 0);
|
|
122
|
+
/**
|
|
123
|
+
* Copilot's hook payloads are camelCase (`sessionId`, `transcriptPath`,
|
|
124
|
+
* `toolName`/`toolArgs`/`toolResult`); preToolUse in the CLI batches calls as
|
|
125
|
+
* `toolCalls: [{id, name, args}]`. Translate to the shared snake_case shape.
|
|
126
|
+
* The FIRST tool call fills tool_name/tool_input; `copilotToolCalls` keeps all
|
|
127
|
+
* of them so security can check each.
|
|
128
|
+
*/
|
|
129
|
+
function normalizeCopilotHookInput(input, home = copilotHome()) {
|
|
130
|
+
const sessionId = firstString(input.session_id, input.sessionId);
|
|
131
|
+
const transcriptPath = firstString(input.transcript_path, input.transcriptPath)
|
|
132
|
+
?? copilotTranscriptPath(sessionId, home);
|
|
133
|
+
const calls = copilotToolCalls(input);
|
|
134
|
+
const first = calls[0];
|
|
135
|
+
const toolResult = input.toolResult ?? input.tool_response;
|
|
136
|
+
return {
|
|
137
|
+
...input,
|
|
138
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
139
|
+
...(transcriptPath ? { transcript_path: transcriptPath } : {}),
|
|
140
|
+
...(first ? { tool_name: first.name, tool_input: first.input } : {}),
|
|
141
|
+
...(first?.id ? { tool_use_id: first.id } : {}),
|
|
142
|
+
...(toolResult !== undefined ? { tool_response: toolResult } : {}),
|
|
143
|
+
...(calls.length > 1 ? { copilotToolCalls: calls } : {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function copilotToolCalls(input) {
|
|
147
|
+
const parse = (args) => {
|
|
148
|
+
if (typeof args !== "string")
|
|
149
|
+
return args;
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(args);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return args;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
if (Array.isArray(input.toolCalls)) {
|
|
158
|
+
return input.toolCalls.flatMap((raw) => {
|
|
159
|
+
const call = raw;
|
|
160
|
+
const name = firstString(call?.name, call?.toolName);
|
|
161
|
+
return name
|
|
162
|
+
? [{ ...(typeof call.id === "string" ? { id: call.id } : {}), name, input: parse(call.args ?? call.toolArgs) }]
|
|
163
|
+
: [];
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const name = firstString(input.toolName, input.tool_name);
|
|
167
|
+
return name ? [{ name, input: parse(input.toolArgs ?? input.tool_input) }] : [];
|
|
168
|
+
}
|
|
169
|
+
/** Copilot's preToolUse deny output. */
|
|
170
|
+
function copilotPreToolBlockResponse(reason) {
|
|
171
|
+
return { permissionDecision: "deny", permissionDecisionReason: reason };
|
|
172
|
+
}
|
|
173
|
+
/** Only a byte-empty record is safe to discard (same rule as Cursor). */
|
|
174
|
+
function isCopilotTranscriptEmpty(content) {
|
|
175
|
+
return content.trim().length === 0;
|
|
176
|
+
}
|
|
177
|
+
/** session.shutdown carries the per-model token totals; written after sessionEnd. */
|
|
178
|
+
function hasCopilotShutdown(content) {
|
|
179
|
+
return content.includes('"session.shutdown"');
|
|
180
|
+
}
|
|
181
|
+
// Copilot tool names → the Claude names the shared parsers key on (Bash
|
|
182
|
+
// commands for git output and worktree dirs; Edit/Write for the dirs a session
|
|
183
|
+
// wrote to). Anything else keeps Copilot's own name.
|
|
184
|
+
function claudeTool(name, args) {
|
|
185
|
+
const a = (args && typeof args === "object" ? args : {});
|
|
186
|
+
const lower = name.toLowerCase();
|
|
187
|
+
const file = firstString(a.path, a.file_path, a.filePath);
|
|
188
|
+
if (lower === "bash" || lower === "shell" || lower === "powershell") {
|
|
189
|
+
return { name: "Bash", input: { ...a, command: firstString(a.command, a.cmd) ?? "" } };
|
|
190
|
+
}
|
|
191
|
+
if ((lower === "edit" || lower === "str_replace" || lower === "str_replace_editor" || lower === "apply_patch") && file) {
|
|
192
|
+
return { name: "Edit", input: { ...a, file_path: file } };
|
|
193
|
+
}
|
|
194
|
+
if ((lower === "create" || lower === "write") && file)
|
|
195
|
+
return { name: "Write", input: { ...a, file_path: file } };
|
|
196
|
+
if (lower === "view" && file)
|
|
197
|
+
return { name: "Read", input: { ...a, file_path: file } };
|
|
198
|
+
// The skill tool's runs are counted from skill.invoked (which also covers
|
|
199
|
+
// skills the user invoked directly); drop its argument so it isn't counted twice.
|
|
200
|
+
if (lower === "skill")
|
|
201
|
+
return { name: "skill", input: {} };
|
|
202
|
+
return { name, input: args ?? {} };
|
|
203
|
+
}
|
|
204
|
+
const num = (value) => (typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0);
|
|
205
|
+
/** Image-only prompts have empty content. Keep attachment descriptions without
|
|
206
|
+
* reading local files, uploading image bytes, or inventing OCR prompt text. */
|
|
207
|
+
function copilotUserContent(data) {
|
|
208
|
+
const content = [];
|
|
209
|
+
if (typeof data.content === "string" && data.content)
|
|
210
|
+
content.push({ type: "text", text: data.content });
|
|
211
|
+
if (Array.isArray(data.attachments)) {
|
|
212
|
+
for (const attachment of data.attachments) {
|
|
213
|
+
if (!attachment || typeof attachment !== "object")
|
|
214
|
+
continue;
|
|
215
|
+
const { displayName, mimeType, type } = attachment;
|
|
216
|
+
const name = typeof displayName === "string" ? displayName : "unnamed attachment";
|
|
217
|
+
const kind = typeof mimeType === "string" ? mimeType : typeof type === "string" ? type : "attachment";
|
|
218
|
+
content.push({ type: "text", text: `[Attachment: ${name} (${kind}); attachment contents unavailable in captured transcript]` });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return content.length ? content : [{ type: "text", text: "" }];
|
|
222
|
+
}
|
|
223
|
+
function resultText(data) {
|
|
224
|
+
const result = data.result;
|
|
225
|
+
if (typeof result === "string")
|
|
226
|
+
return result;
|
|
227
|
+
if (result && typeof result === "object") {
|
|
228
|
+
const { content, detailedContent } = result;
|
|
229
|
+
return [content, detailedContent].filter((v) => typeof v === "string").join("\n");
|
|
230
|
+
}
|
|
231
|
+
return typeof data.error?.message === "string" ? data.error.message : "";
|
|
232
|
+
}
|
|
233
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
234
|
+
function premiumOf(data, at) {
|
|
235
|
+
return {
|
|
236
|
+
premiumRequests: count(data.totalPremiumRequests),
|
|
237
|
+
nanoAiu: count(data.totalNanoAiu),
|
|
238
|
+
...(at ? { at } : {}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function sumAgentMetrics(agents) {
|
|
242
|
+
const totals = {};
|
|
243
|
+
for (const metrics of agents) {
|
|
244
|
+
for (const [model, metric] of Object.entries(metrics ?? {})) {
|
|
245
|
+
const into = (totals[model] ??= {});
|
|
246
|
+
for (const [key, value] of Object.entries(metric?.usage ?? {}))
|
|
247
|
+
into[key] = (into[key] ?? 0) + num(value);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return Object.fromEntries(Object.entries(totals).map(([model, usage]) => [model, { usage }]));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* events.jsonl → Claude-shaped JSONL:
|
|
254
|
+
* session.start → system record (cwd, branch, entrypoint for VS Code)
|
|
255
|
+
* user.message → user text
|
|
256
|
+
* assistant.message → assistant text + tool_use blocks (model on every call)
|
|
257
|
+
* tool.execution_complete→ user tool_result (git output feeds branch/sha joins)
|
|
258
|
+
* skill.invoked → copilot.skill_invoked (one skill run)
|
|
259
|
+
* session.shutdown → copilot.usage per model (tokens, sub-agents
|
|
260
|
+
* included; requests counted from the assistant
|
|
261
|
+
* calls, so 0 here) + copilot.usage_summary
|
|
262
|
+
* session.usage_checkpoint → copilot.usage_summary (premium requests and
|
|
263
|
+
* nano-AIU so far; superseded by shutdown)
|
|
264
|
+
* subagent.started → counted as a fleet sub-agent
|
|
265
|
+
* Hooks, permissions, system prompts and checkpoints are dropped.
|
|
266
|
+
*/
|
|
267
|
+
function normalizeCopilotTranscript(raw, opts = {}) {
|
|
268
|
+
const out = [];
|
|
269
|
+
const subagentCalls = new Set();
|
|
270
|
+
let shutdownAgents = 0;
|
|
271
|
+
let assistantModel = false;
|
|
272
|
+
// Copilot's subscription usage: the latest checkpoint, superseded by shutdown.
|
|
273
|
+
let premium;
|
|
274
|
+
let checkpointModels = [];
|
|
275
|
+
let final = false;
|
|
276
|
+
for (const line of raw.split("\n")) {
|
|
277
|
+
if (!line.trim())
|
|
278
|
+
continue;
|
|
279
|
+
let event;
|
|
280
|
+
try {
|
|
281
|
+
event = JSON.parse(line);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const data = event?.data ?? {};
|
|
287
|
+
const timestamp = typeof event.timestamp === "string" ? event.timestamp : undefined;
|
|
288
|
+
const uuid = typeof event.id === "string" ? event.id : undefined;
|
|
289
|
+
const base = { ...(timestamp ? { timestamp } : {}), ...(uuid ? { uuid } : {}) };
|
|
290
|
+
switch (event.type) {
|
|
291
|
+
case "session.start": {
|
|
292
|
+
const context = data.context ?? {};
|
|
293
|
+
out.push(JSON.stringify({
|
|
294
|
+
type: "system",
|
|
295
|
+
subtype: "copilot_session_start",
|
|
296
|
+
...base,
|
|
297
|
+
...(typeof data.sessionId === "string" ? { sessionId: data.sessionId } : {}),
|
|
298
|
+
...(typeof context.cwd === "string" ? { cwd: context.cwd } : {}),
|
|
299
|
+
...(typeof context.branch === "string" ? { gitBranch: context.branch } : {}),
|
|
300
|
+
...(typeof data.copilotVersion === "string" ? { version: data.copilotVersion } : {}),
|
|
301
|
+
...(isVSCodeClient(opts.clientName) ? { entrypoint: "copilot:vscode" } : {}),
|
|
302
|
+
}));
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
case "session.error":
|
|
306
|
+
out.push(JSON.stringify({ type: "agent.error", ...base, error: data.error ?? { message: data.message, code: data.errorType } }));
|
|
307
|
+
break;
|
|
308
|
+
case "hook.start":
|
|
309
|
+
if (data.hookType === "sessionEnd" && data.input?.reason === "abort") {
|
|
310
|
+
out.push(JSON.stringify({ type: "agent.interruption", ...base, id: uuid }));
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
case "user.message":
|
|
314
|
+
out.push(JSON.stringify({
|
|
315
|
+
type: "user",
|
|
316
|
+
...base,
|
|
317
|
+
message: { role: "user", content: copilotUserContent(data) },
|
|
318
|
+
}));
|
|
319
|
+
break;
|
|
320
|
+
case "subagent.started":
|
|
321
|
+
if (typeof data.toolCallId === "string")
|
|
322
|
+
subagentCalls.add(data.toolCallId);
|
|
323
|
+
break;
|
|
324
|
+
case "session.usage_checkpoint":
|
|
325
|
+
if (!final)
|
|
326
|
+
premium = premiumOf(data, timestamp);
|
|
327
|
+
if (Array.isArray(data.modelCacheState)) {
|
|
328
|
+
checkpointModels = data.modelCacheState
|
|
329
|
+
.map((m) => (m && typeof m === "object" ? m.modelId : undefined))
|
|
330
|
+
.filter((m) => typeof m === "string" && m.length > 0);
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
case "assistant.message": {
|
|
334
|
+
if (typeof data.model === "string" && data.model)
|
|
335
|
+
assistantModel = true;
|
|
336
|
+
const tools = Array.isArray(data.toolRequests) ? data.toolRequests : [];
|
|
337
|
+
const content = [];
|
|
338
|
+
if (typeof data.content === "string" && data.content)
|
|
339
|
+
content.push({ type: "text", text: data.content });
|
|
340
|
+
for (const request of tools) {
|
|
341
|
+
if (typeof request?.name !== "string")
|
|
342
|
+
continue;
|
|
343
|
+
const mapped = claudeTool(request.name, request.arguments);
|
|
344
|
+
content.push({ type: "tool_use", id: request.toolCallId, name: mapped.name, input: mapped.input });
|
|
345
|
+
}
|
|
346
|
+
out.push(JSON.stringify({
|
|
347
|
+
type: "assistant",
|
|
348
|
+
...base,
|
|
349
|
+
tokensAvailable: false,
|
|
350
|
+
message: {
|
|
351
|
+
id: firstString(data.apiCallId, data.messageId, uuid),
|
|
352
|
+
role: "assistant",
|
|
353
|
+
content,
|
|
354
|
+
// One model call. Copilot reports tokens only at shutdown. A record
|
|
355
|
+
// with no model is the runtime's own step (e.g. read_agent after a
|
|
356
|
+
// fleet sub-agent finishes), not a model call, so it isn't counted.
|
|
357
|
+
...(typeof data.model === "string" && data.model
|
|
358
|
+
? {
|
|
359
|
+
model: data.model,
|
|
360
|
+
usage: { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
|
|
361
|
+
}
|
|
362
|
+
: {}),
|
|
363
|
+
},
|
|
364
|
+
}));
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case "tool.execution_complete":
|
|
368
|
+
if (data.error?.code === "user_denied") {
|
|
369
|
+
out.push(JSON.stringify({ type: "agent.rejection", ...base, id: data.toolCallId ?? uuid }));
|
|
370
|
+
}
|
|
371
|
+
// A space after the array bracket keeps the shared git-output scan
|
|
372
|
+
// (`[branch sha]` commit lines) from anchoring on JSON's own "[".
|
|
373
|
+
out.push(JSON.stringify({
|
|
374
|
+
type: "user",
|
|
375
|
+
...base,
|
|
376
|
+
message: {
|
|
377
|
+
role: "user",
|
|
378
|
+
content: [{
|
|
379
|
+
type: "tool_result",
|
|
380
|
+
tool_use_id: data.toolCallId,
|
|
381
|
+
content: resultText(data),
|
|
382
|
+
...(data.success === false ? { is_error: true } : {}),
|
|
383
|
+
}],
|
|
384
|
+
},
|
|
385
|
+
}).replace('"content":[{', '"content":[ {'));
|
|
386
|
+
break;
|
|
387
|
+
case "skill.invoked":
|
|
388
|
+
if (typeof data.name === "string" && data.name) {
|
|
389
|
+
out.push(JSON.stringify({ type: "copilot.skill_invoked", ...base, skill: data.name }));
|
|
390
|
+
}
|
|
391
|
+
break;
|
|
392
|
+
case "session.shutdown": {
|
|
393
|
+
final = true;
|
|
394
|
+
premium = premiumOf(data, timestamp);
|
|
395
|
+
const agents = Object.entries(data.agentMetrics ?? {});
|
|
396
|
+
shutdownAgents = agents.filter(([agent]) => agent !== "main").length;
|
|
397
|
+
// modelMetrics is the whole session, sub-agents included; older
|
|
398
|
+
// records without it are summed from the per-agent metrics.
|
|
399
|
+
const metrics = data.modelMetrics ?? sumAgentMetrics(agents.map(([, agent]) => agent?.modelMetrics));
|
|
400
|
+
for (const [model, metric] of Object.entries(metrics)) {
|
|
401
|
+
const usage = metric?.usage ?? {};
|
|
402
|
+
if (![usage.inputTokens, usage.outputTokens, usage.cacheReadTokens, usage.cacheWriteTokens].some(value => typeof value === "number" && Number.isFinite(value)))
|
|
403
|
+
continue;
|
|
404
|
+
const cacheRead = num(usage.cacheReadTokens);
|
|
405
|
+
out.push(JSON.stringify({
|
|
406
|
+
type: "copilot.usage",
|
|
407
|
+
...base,
|
|
408
|
+
requests: 0,
|
|
409
|
+
message: {
|
|
410
|
+
id: `copilot-usage:${model}`,
|
|
411
|
+
model,
|
|
412
|
+
usage: {
|
|
413
|
+
// Copilot's inputTokens include the cached prompt; the shared
|
|
414
|
+
// shape counts fresh input separately from cache reads.
|
|
415
|
+
input_tokens: Math.max(0, num(usage.inputTokens) - cacheRead),
|
|
416
|
+
output_tokens: num(usage.outputTokens),
|
|
417
|
+
cache_read_input_tokens: cacheRead,
|
|
418
|
+
cache_creation_input_tokens: num(usage.cacheWriteTokens),
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
default:
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// No model on any assistant call (seen in open VS Code chats): name the
|
|
430
|
+
// models from the last checkpoint. No tokens are invented; requests stay 0.
|
|
431
|
+
if (!assistantModel) {
|
|
432
|
+
for (const model of checkpointModels) {
|
|
433
|
+
out.push(JSON.stringify({
|
|
434
|
+
type: "copilot.model",
|
|
435
|
+
tokensAvailable: false,
|
|
436
|
+
requests: 0,
|
|
437
|
+
message: {
|
|
438
|
+
id: `copilot-model:${model}`,
|
|
439
|
+
model,
|
|
440
|
+
usage: { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
|
|
441
|
+
},
|
|
442
|
+
}));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (premium && (premium.premiumRequests !== undefined || premium.nanoAiu !== undefined)) {
|
|
446
|
+
out.push(JSON.stringify({
|
|
447
|
+
type: "copilot.usage_summary",
|
|
448
|
+
...(premium.at ? { timestamp: premium.at } : {}),
|
|
449
|
+
...(premium.premiumRequests !== undefined ? { premiumRequests: premium.premiumRequests } : {}),
|
|
450
|
+
...(premium.nanoAiu !== undefined ? { nanoAiu: premium.nanoAiu } : {}),
|
|
451
|
+
}));
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
content: out.join("\n") + (out.length ? "\n" : ""),
|
|
455
|
+
subagents: { count: Math.max(subagentCalls.size, shutdownAgents), truncated: false },
|
|
456
|
+
final,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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.inspectCopilotHooks = inspectCopilotHooks;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const copilotHooks_1 = require("./copilotHooks");
|
|
40
|
+
const detection_1 = require("./detection");
|
|
41
|
+
function inspectCopilotHooks(file = path.join((0, copilotHooks_1.copilotHome)(), "hooks", "codiedev.json"), security = false) {
|
|
42
|
+
if (!fs.existsSync(file))
|
|
43
|
+
return { status: "not-installed", detail: `${file} not found` };
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
46
|
+
const expected = security
|
|
47
|
+
? [["userPromptSubmitted", "discover-copilot"], ["preToolUse", "security-pre-copilot"], ["postToolUse", "security-post-copilot"]]
|
|
48
|
+
: [["sessionStart", "session-start-copilot"], ["agentStop", "backup-copilot"], ["sessionEnd", "capture-copilot"], ["errorOccurred", "limit-copilot"]];
|
|
49
|
+
const missing = expected.filter(([event, subcommand]) => {
|
|
50
|
+
const entries = parsed.hooks?.[event];
|
|
51
|
+
return !Array.isArray(entries) || !entries.some((entry) => {
|
|
52
|
+
// Both platforms must have the managed command, not just the current one.
|
|
53
|
+
const commands = [entry.bash ?? entry.command, entry.powershell ?? entry.command];
|
|
54
|
+
return commands.every((command) => typeof command === "string"
|
|
55
|
+
&& (0, detection_1.isCodiedevHookCommand)(command) && command.trim().endsWith(subcommand));
|
|
56
|
+
});
|
|
57
|
+
}).map(([event]) => event);
|
|
58
|
+
if (parsed.version !== 1 || missing.length)
|
|
59
|
+
return { status: "fail", detail: `invalid or missing hooks: ${missing.join(", ") || "version"}` };
|
|
60
|
+
return { status: "pass", detail: security ? "prompt guard and tool telemetry hooks installed" : "session start, backup, capture and limit hooks installed" };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return { status: "fail", detail: "invalid Copilot hook configuration" };
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/cursorHooks.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/** Normalize Cursor's native hook payload into the shared capture shape. */
|
|
2
2
|
export declare function normalizeCursorHookInput<T extends Record<string, unknown>>(input: T): T & Record<string, unknown>;
|
|
3
|
-
/**
|
|
3
|
+
/** Normalize Cursor message roles and native tools at the capture boundary. */
|
|
4
4
|
export declare function normalizeCursorTranscript(content: string): string;
|
|
5
5
|
/** Cursor may change its private JSONL record shape independently of the hook
|
|
6
6
|
* contract. Only a byte-empty snapshot is safe to discard; a non-empty file
|
|
7
7
|
* that the current normalizer cannot parse must still be uploaded for backend
|
|
8
8
|
* recovery and diagnosis. */
|
|
9
9
|
export declare function isCursorTranscriptEmpty(content: string): boolean;
|
|
10
|
+
/** The IDE transcript often omits usage entirely; absence must not become
|
|
11
|
+
* measured zero. CLI records with explicit token fields retain their totals. */
|
|
12
|
+
export declare function cursorTranscriptHasUsage(content: string): boolean;
|
|
13
|
+
export declare function cursorObservedModel(content: string): string | undefined;
|
package/dist/cursorHooks.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.normalizeCursorHookInput = normalizeCursorHookInput;
|
|
4
4
|
exports.normalizeCursorTranscript = normalizeCursorTranscript;
|
|
5
5
|
exports.isCursorTranscriptEmpty = isCursorTranscriptEmpty;
|
|
6
|
+
exports.cursorTranscriptHasUsage = cursorTranscriptHasUsage;
|
|
7
|
+
exports.cursorObservedModel = cursorObservedModel;
|
|
6
8
|
/** Normalize Cursor's native hook payload into the shared capture shape. */
|
|
7
9
|
function normalizeCursorHookInput(input) {
|
|
8
10
|
const sessionId = firstString(input.session_id, input.sessionId, input.conversation_id);
|
|
@@ -24,22 +26,37 @@ function normalizeCursorHookInput(input) {
|
|
|
24
26
|
...(duration !== undefined ? { duration_ms: duration } : {}),
|
|
25
27
|
};
|
|
26
28
|
}
|
|
27
|
-
/**
|
|
29
|
+
/** Normalize Cursor message roles and native tools at the capture boundary. */
|
|
28
30
|
function normalizeCursorTranscript(content) {
|
|
29
|
-
return content
|
|
31
|
+
return content
|
|
32
|
+
.split("\n")
|
|
33
|
+
.map((line) => {
|
|
30
34
|
if (!line.trim())
|
|
31
35
|
return line;
|
|
32
36
|
try {
|
|
33
37
|
const entry = JSON.parse(line);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
const type = entry.type ?? entry.role;
|
|
39
|
+
if (type !== "user" && type !== "assistant")
|
|
40
|
+
return line;
|
|
41
|
+
const message = entry.message;
|
|
42
|
+
const content = Array.isArray(message?.content) ? message.content.map((block) => {
|
|
43
|
+
if (!block || block.type !== "tool_use")
|
|
44
|
+
return block;
|
|
45
|
+
const input = block.input ?? {};
|
|
46
|
+
const name = block.name === "Shell" ? "Bash" : block.name === "StrReplace" ? "Edit" : block.name;
|
|
47
|
+
return { ...block, name, input: { ...input,
|
|
48
|
+
...(typeof input.path === "string" && input.file_path === undefined ? { file_path: input.path } : {}),
|
|
49
|
+
} };
|
|
50
|
+
}) : message?.content;
|
|
51
|
+
return JSON.stringify({ ...entry, type, ...(message ? { message: { ...message, content } } : {}) });
|
|
37
52
|
}
|
|
38
53
|
catch {
|
|
39
|
-
//
|
|
54
|
+
// Preserve malformed/non-JSON lines; tolerant downstream parsers skip
|
|
55
|
+
// anything they cannot consume.
|
|
40
56
|
}
|
|
41
57
|
return line;
|
|
42
|
-
})
|
|
58
|
+
})
|
|
59
|
+
.join("\n");
|
|
43
60
|
}
|
|
44
61
|
/** Cursor may change its private JSONL record shape independently of the hook
|
|
45
62
|
* contract. Only a byte-empty snapshot is safe to discard; a non-empty file
|
|
@@ -54,3 +71,29 @@ function firstString(...values) {
|
|
|
54
71
|
function firstNumber(...values) {
|
|
55
72
|
return values.find((value) => typeof value === "number" && Number.isFinite(value));
|
|
56
73
|
}
|
|
74
|
+
/** The IDE transcript often omits usage entirely; absence must not become
|
|
75
|
+
* measured zero. CLI records with explicit token fields retain their totals. */
|
|
76
|
+
function cursorTranscriptHasUsage(content) {
|
|
77
|
+
return content.split("\n").some((line) => {
|
|
78
|
+
try {
|
|
79
|
+
const usage = JSON.parse(line).message?.usage;
|
|
80
|
+
return usage && ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"]
|
|
81
|
+
.some((key) => typeof usage[key] === "number" && Number.isFinite(usage[key]));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function cursorObservedModel(content) {
|
|
89
|
+
let model;
|
|
90
|
+
for (const line of content.split("\n")) {
|
|
91
|
+
try {
|
|
92
|
+
const event = JSON.parse(line);
|
|
93
|
+
if (event.subtype === "cursor_model" && typeof event.model === "string")
|
|
94
|
+
model = event.model;
|
|
95
|
+
}
|
|
96
|
+
catch { /* partial records */ }
|
|
97
|
+
}
|
|
98
|
+
return model;
|
|
99
|
+
}
|