codiedev 0.9.60 → 0.9.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,10 @@
1
+ export declare function inspectCopilotHooks(file?: string, security?: boolean): {
2
+ status: "not-installed";
3
+ detail: string;
4
+ } | {
5
+ status: "fail";
6
+ detail: string;
7
+ } | {
8
+ status: "pass";
9
+ detail: string;
10
+ };
@@ -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
+ }
@@ -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
- /** Add Claude-compatible message discriminators while preserving Cursor JSONL. */
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;
@@ -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
- /** Add Claude-compatible message discriminators while preserving Cursor JSONL. */
29
+ /** Normalize Cursor message roles and native tools at the capture boundary. */
28
30
  function normalizeCursorTranscript(content) {
29
- return content.split("\n").map((line) => {
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
- if ((entry.role === "user" || entry.role === "assistant") && entry.type === undefined) {
35
- return JSON.stringify({ ...entry, type: entry.role });
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
- // Tolerant downstream parsers skip malformed/non-JSON lines.
54
+ // Preserve malformed/non-JSON lines; tolerant downstream parsers skip
55
+ // anything they cannot consume.
40
56
  }
41
57
  return line;
42
- }).join("\n");
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
+ }
@@ -0,0 +1,2 @@
1
+ export declare function recordCursorLifecycle(input: Record<string, unknown>, final: boolean): void;
2
+ export declare function cursorLifecycleRecords(sessionId: string | undefined): string;