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.
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.classifyLimitKind = classifyLimitKind;
4
4
  exports.parseLimitEventFromTranscript = parseLimitEventFromTranscript;
5
5
  exports.parseCodexLimitEventFromTranscript = parseCodexLimitEventFromTranscript;
6
+ exports.parseNativeLimitError = parseNativeLimitError;
7
+ exports.parseNativeLimitEventFromTranscript = parseNativeLimitEventFromTranscript;
6
8
  exports.tailForLimitEvent = tailForLimitEvent;
7
9
  function classifyLimitKind(text) {
8
10
  const t = text.toLowerCase();
@@ -129,6 +131,46 @@ function parseCodexLimitEventFromTranscript(transcript) {
129
131
  }
130
132
  return found;
131
133
  }
134
+ /** Only explicit provider errors qualify; never scan user/assistant prose. */
135
+ function parseNativeLimitError(input, now = Date.now()) {
136
+ const context = input.errorContext ?? input.error_context;
137
+ if (context !== undefined && context !== "model_call")
138
+ return null;
139
+ const error = input.error;
140
+ const fields = error && typeof error === "object" ? error : {};
141
+ const code = String(fields.code ?? fields.name ?? input.error_code ?? "");
142
+ const text = typeof error === "string" ? error
143
+ : typeof fields.message === "string" ? fields.message
144
+ : typeof input.error_message === "string" ? input.error_message : "";
145
+ if (!/^(?:429|rate[_-]?limit(?:ed|_exceeded|_error)?|RateLimitError|quota_exceeded)$/i.test(code)
146
+ && !/\b(?:rate limit (?:exceeded|reached)|too many requests|usage limit (?:exceeded|reached)|premium requests? (?:exhausted|limit reached))\b/i.test(text))
147
+ return null;
148
+ const rawAt = input.timestamp;
149
+ const parsedAt = typeof rawAt === "number" ? rawAt : typeof rawAt === "string" ? Date.parse(rawAt) : NaN;
150
+ return { kind: classifyLimitKind(text), at: Number.isFinite(parsedAt) && parsedAt > 0 ? parsedAt : now,
151
+ resetText: parseResetText(text), reason: code || "rate_limit" };
152
+ }
153
+ function parseNativeLimitEventFromTranscript(transcript) {
154
+ let found = null;
155
+ for (const line of transcript.split("\n")) {
156
+ try {
157
+ const entry = JSON.parse(line);
158
+ if (entry.type === "agent.limit" && Number.isFinite(entry.at) && entry.at > 0
159
+ && ["session", "user_spend", "org_spend", "other"].includes(entry.kind)) {
160
+ found = { kind: entry.kind, at: entry.at, resetText: typeof entry.resetText === "string" ? entry.resetText : null,
161
+ ...(typeof entry.reason === "string" ? { reason: entry.reason } : {}) };
162
+ continue;
163
+ }
164
+ if (!entry.timestamp)
165
+ continue; // no stable timestamp means snapshots cannot deduplicate
166
+ if (entry.type === "agent.error" || entry.type === "session.error") {
167
+ found = parseNativeLimitError({ ...(entry.data ?? entry), timestamp: entry.timestamp }) ?? found;
168
+ }
169
+ }
170
+ catch { /* tolerant of incomplete transcript tails */ }
171
+ }
172
+ return found;
173
+ }
132
174
  // Tail of the raw transcript to ship with the limit event so the server can
133
175
  // summarize "what they were working on" immediately — without waiting for the
134
176
  // SessionEnd capture (which can lag hours, or drop entirely on Windows).
@@ -1,5 +1,5 @@
1
1
  import { type LimitEvent } from "./limitEvent";
2
- export type LimitEventSource = "claude-code" | "codex";
2
+ export type LimitEventSource = "claude-code" | "codex" | "cursor" | "copilot";
3
3
  export type LimitEventPayload = LimitEvent & {
4
4
  source: LimitEventSource;
5
5
  sessionId?: string;
@@ -76,7 +76,7 @@ function postLimitEvent(config, payload, timeoutMs = 5_000) {
76
76
  async function reportTranscriptLimitEvent(args) {
77
77
  const parse = args.source === "codex"
78
78
  ? limitEvent_1.parseCodexLimitEventFromTranscript
79
- : limitEvent_1.parseLimitEventFromTranscript;
79
+ : args.source === "cursor" || args.source === "copilot" ? limitEvent_1.parseNativeLimitEventFromTranscript : limitEvent_1.parseLimitEventFromTranscript;
80
80
  const parsed = parse(args.transcript);
81
81
  if (!parsed && !args.emitMinimalOnMiss)
82
82
  return false;
@@ -0,0 +1,5 @@
1
+ /** Native records only: quoted denial/interrupt text in conversation is not evidence. */
2
+ export declare function parseNativeSteering(content: string): {
3
+ interrupts: number;
4
+ rejections: number;
5
+ };
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseNativeSteering = parseNativeSteering;
4
+ /** Native records only: quoted denial/interrupt text in conversation is not evidence. */
5
+ function parseNativeSteering(content) {
6
+ const interrupts = new Set();
7
+ const abortedTurns = new Set();
8
+ const rejections = new Set();
9
+ for (const [index, line] of content.split("\n").entries()) {
10
+ try {
11
+ const event = JSON.parse(line);
12
+ const id = String(event.id ?? event.uuid ?? event.toolCallId ?? index);
13
+ if (event.type === "agent.interruption")
14
+ interrupts.add(id);
15
+ if (event.type === "turn_ended" && event.status === "aborted")
16
+ abortedTurns.add(id);
17
+ if (event.type === "agent.rejection")
18
+ rejections.add(id);
19
+ // Cursor tool failures distinguish a user-denied permission from an
20
+ // ordinary tool failure. Don't guess from free-form error messages.
21
+ if (event.type === "tool_result" && event.error?.code === "user_denied")
22
+ rejections.add(id);
23
+ }
24
+ catch { /* incomplete transcript records are ignored */ }
25
+ }
26
+ return { interrupts: Math.max(interrupts.size, abortedTurns.size), rejections: rejections.size };
27
+ }
@@ -0,0 +1,9 @@
1
+ /** Session-start context uses each client's supported output contract. */
2
+ export declare const TEAM_SKILL_CONTEXT = "This machine is connected to Codiedev, the team's artifact and skill library.\nBefore a substantial coding task, run codiedev search \"<brief task description>\" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.\nUse codiedev push <file.md> to share an artifact when asked, codiedev ask \"<question>\" for team knowledge, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.";
3
+ export declare function teamSkillSessionOutput(source: "cursor" | "copilot", context?: string): {
4
+ additional_context: string;
5
+ additionalContext?: undefined;
6
+ } | {
7
+ additionalContext: string;
8
+ additional_context?: undefined;
9
+ };
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TEAM_SKILL_CONTEXT = void 0;
4
+ exports.teamSkillSessionOutput = teamSkillSessionOutput;
5
+ /** Session-start context uses each client's supported output contract. */
6
+ exports.TEAM_SKILL_CONTEXT = `This machine is connected to Codiedev, the team's artifact and skill library.
7
+ Before a substantial coding task, run codiedev search "<brief task description>" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.
8
+ Use codiedev push <file.md> to share an artifact when asked, codiedev ask "<question>" for team knowledge, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.`;
9
+ function teamSkillSessionOutput(source, context = exports.TEAM_SKILL_CONTEXT) {
10
+ return source === "cursor" ? { additional_context: context } : { additionalContext: context };
11
+ }
@@ -1,6 +1,8 @@
1
1
  /** Compact per-model usage, derived from captured transcript records. */
2
2
  export type TranscriptModelUsage = {
3
3
  model: string;
4
+ /** False for Copilot model/request hints before token totals arrive. */
5
+ tokensAvailable?: boolean;
4
6
  /** Assistant message IDs; nonzero cumulative usage events for Codex. */
5
7
  requests: number;
6
8
  inputTokens: number;
@@ -44,6 +44,7 @@ function* transcriptRecords(content, source) {
44
44
  function parseTranscriptModels(content, source) {
45
45
  const buckets = new Map();
46
46
  const messages = new Map();
47
+ const tokenEvidence = new Set();
47
48
  let model = "Unattributed";
48
49
  let previous = {
49
50
  inputTokens: 0,
@@ -143,9 +144,12 @@ function parseTranscriptModels(content, source) {
143
144
  model =
144
145
  typeof message.model === "string" ? message.model : "Unattributed";
145
146
  const raw = message.usage;
147
+ // Copilot's shutdown token totals carry `requests: 0`: its calls are
148
+ // already counted from the assistant records.
149
+ const requests = event.requests;
146
150
  usage = {
147
151
  ...empty(model),
148
- requests: 1,
152
+ requests: typeof requests === "number" && Number.isFinite(requests) ? Math.max(0, requests) : 1,
149
153
  inputTokens: positive(raw.input_tokens),
150
154
  outputTokens: positive(raw.output_tokens),
151
155
  cacheReadTokens: positive(raw.cache_read_input_tokens),
@@ -153,6 +157,8 @@ function parseTranscriptModels(content, source) {
153
157
  };
154
158
  messageId = message.id ?? event.uuid ?? messageId;
155
159
  }
160
+ if (source === "copilot" && event.type === "copilot.usage")
161
+ tokenEvidence.add(usage.model);
156
162
  // Client placeholders are not model executions.
157
163
  if (!usage.model.trim() || usage.model.startsWith("<"))
158
164
  continue;
@@ -197,7 +203,7 @@ function parseTranscriptModels(content, source) {
197
203
  delete buckets.get(key).costUsd;
198
204
  for (const key of incompleteReasoning)
199
205
  delete buckets.get(key).reasoningTokens;
200
- return [...buckets.values()];
206
+ return [...buckets.values()].map(row => source === "copilot" && !tokenEvidence.has(row.model) ? { ...row, tokensAvailable: false } : row);
201
207
  }
202
208
  function totalModelTokens(row) {
203
209
  return (row.inputTokens +
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import type { CaptureSignals } from "./captureSignals";
3
- import type { TranscriptModelUsage } from "./transcriptModels";
3
+ import { type TranscriptModelUsage } from "./transcriptModels";
4
4
  import { spawn as nodeSpawn } from "child_process";
5
5
  import type { CaptureLogEntry } from "./captureLog";
6
6
  import { codiedevStateDir } from "./configDir";
@@ -89,6 +89,7 @@ export interface CapturePayloadInput {
89
89
  final?: boolean;
90
90
  costUsd?: number;
91
91
  }
92
+ export declare function stripRepoUrlCredentials(url: string): string;
92
93
  export declare function buildCapturePayload(input: CapturePayloadInput): Record<string, unknown>;
93
94
  export declare function hashToken(token: string): string;
94
95
  export declare function extractHookNodePath(cmd: string | undefined): string | undefined;
@@ -96,6 +97,12 @@ export declare function resetStableNodeCacheForTests(): void;
96
97
  export declare function claudeCodeInstalled(): boolean;
97
98
  export declare function codexInstalled(): boolean;
98
99
  export declare function cursorInstalled(): boolean;
100
+ /**
101
+ * GitHub Copilot: `$COPILOT_HOME` / ~/.copilot (either surface has run once),
102
+ * the `copilot` CLI on PATH, or VS Code's Copilot Chat extension. VS Code's
103
+ * agent and the CLI read the same user hooks, so one install covers both.
104
+ */
105
+ export declare function copilotInstalled(): boolean;
99
106
  export declare function openCodeInstalled(): boolean;
100
107
  /** Install a dormant global OpenCode adapter. Installing it unconditionally
101
108
  * keeps `codiedev connect` as the only setup step even when Codie/OpenCode is
@@ -116,6 +123,8 @@ export declare function ensureAllCodiedevHooksInstalled(): void;
116
123
  export declare function ensureSessionStartHookInstalled(): void;
117
124
  export declare function installClaudeCodeInstructions(): void;
118
125
  export declare function installCodexInstructions(): void;
126
+ export declare function copilotInstructionsPath(): string;
127
+ export declare function installCopilotInstructions(): void;
119
128
  export declare function installCursorInstructions(): void;
120
129
  /**
121
130
  * Install the Codiedev MCP server into Claude Code's user-scope config.
@@ -173,6 +182,13 @@ export declare function installCodexMcp(): boolean;
173
182
  export declare function installCodexHook(): void;
174
183
  export declare function ensureCodexLimitHookInstalled(): void;
175
184
  export declare function installCursorHook(): void;
185
+ export declare function copilotHooksPath(): string;
186
+ export declare function installCopilotHook(agentSecurityEnabled?: boolean): void;
187
+ /** Self-heal the Copilot hook file after upgrades or drift (like Cursor's). */
188
+ export declare function ensureCopilotCodiedevHooksInstalled(): void;
189
+ /** Repair Cursor's complete flat hook set after upgrades or configuration
190
+ * drift. This is deliberately separate from Claude Code's nested-hook healer:
191
+ * the schemas and event names differ, and mixing them risks corrupting both. */
176
192
  export declare function ensureCursorCodiedevHooksInstalled(): void;
177
193
  export interface ParsedStats {
178
194
  messageCount: number;
@@ -184,7 +200,7 @@ export interface SteeringStats {
184
200
  }
185
201
  export declare function parseClaudeSteering(content: string): SteeringStats;
186
202
  export declare function parseCodexSteering(content: string): SteeringStats;
187
- export declare function parseSteering(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode"): SteeringStats;
203
+ export declare function parseSteering(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "copilot"): SteeringStats;
188
204
  export interface UsageStats {
189
205
  inputTokens: number;
190
206
  outputTokens: number;
@@ -196,7 +212,7 @@ export interface UsageStats {
196
212
  /** Engine-reported spend (opencode SDK `info.cost`). Desktop mode only. */
197
213
  costUsd?: number;
198
214
  }
199
- export declare function parseUsage(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "desktop"): UsageStats;
215
+ export declare function parseUsage(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "desktop" | "copilot"): UsageStats;
200
216
  export declare function parseSessionStart(content: string): number | null;
201
217
  export declare function parseClaudeCodeStats(content: string): ParsedStats;
202
218
  /** Convert OpenCode's SDK message array (`{info, parts}[]`) into the same
package/dist/utils.js CHANGED
@@ -53,6 +53,7 @@ exports.parseAuthoredBranches = parseAuthoredBranches;
53
53
  exports.parseAuthoredShas = parseAuthoredShas;
54
54
  exports.getGitBranch = getGitBranch;
55
55
  exports.getGitHeadSha = getGitHeadSha;
56
+ exports.stripRepoUrlCredentials = stripRepoUrlCredentials;
56
57
  exports.buildCapturePayload = buildCapturePayload;
57
58
  exports.hashToken = hashToken;
58
59
  exports.extractHookNodePath = extractHookNodePath;
@@ -60,6 +61,7 @@ exports.resetStableNodeCacheForTests = resetStableNodeCacheForTests;
60
61
  exports.claudeCodeInstalled = claudeCodeInstalled;
61
62
  exports.codexInstalled = codexInstalled;
62
63
  exports.cursorInstalled = cursorInstalled;
64
+ exports.copilotInstalled = copilotInstalled;
63
65
  exports.openCodeInstalled = openCodeInstalled;
64
66
  exports.installOpenCodeCapturePlugin = installOpenCodeCapturePlugin;
65
67
  exports.vscodeCopilotInstalled = vscodeCopilotInstalled;
@@ -72,6 +74,8 @@ exports.ensureAllCodiedevHooksInstalled = ensureAllCodiedevHooksInstalled;
72
74
  exports.ensureSessionStartHookInstalled = ensureSessionStartHookInstalled;
73
75
  exports.installClaudeCodeInstructions = installClaudeCodeInstructions;
74
76
  exports.installCodexInstructions = installCodexInstructions;
77
+ exports.copilotInstructionsPath = copilotInstructionsPath;
78
+ exports.installCopilotInstructions = installCopilotInstructions;
75
79
  exports.installCursorInstructions = installCursorInstructions;
76
80
  exports.installClaudeCodeMcp = installClaudeCodeMcp;
77
81
  exports.installCursorMcp = installCursorMcp;
@@ -84,6 +88,9 @@ exports.installCodexMcp = installCodexMcp;
84
88
  exports.installCodexHook = installCodexHook;
85
89
  exports.ensureCodexLimitHookInstalled = ensureCodexLimitHookInstalled;
86
90
  exports.installCursorHook = installCursorHook;
91
+ exports.copilotHooksPath = copilotHooksPath;
92
+ exports.installCopilotHook = installCopilotHook;
93
+ exports.ensureCopilotCodiedevHooksInstalled = ensureCopilotCodiedevHooksInstalled;
87
94
  exports.ensureCursorCodiedevHooksInstalled = ensureCursorCodiedevHooksInstalled;
88
95
  exports.parseClaudeSteering = parseClaudeSteering;
89
96
  exports.parseCodexSteering = parseCodexSteering;
@@ -114,6 +121,7 @@ exports.runDeferredUpload = runDeferredUpload;
114
121
  exports.transcriptReadSkipReason = transcriptReadSkipReason;
115
122
  exports.shouldReportCaptureSkip = shouldReportCaptureSkip;
116
123
  const fs = __importStar(require("fs"));
124
+ const transcriptModels_1 = require("./transcriptModels");
117
125
  const path = __importStar(require("path"));
118
126
  const os = __importStar(require("os"));
119
127
  const child_process_1 = require("child_process");
@@ -122,6 +130,9 @@ const captureLog_1 = require("./captureLog");
122
130
  const configDir_1 = require("./configDir");
123
131
  Object.defineProperty(exports, "codiedevStateDir", { enumerable: true, get: function () { return configDir_1.codiedevStateDir; } });
124
132
  const codexHookTrust_1 = require("./codexHookTrust");
133
+ const teamSkillContext_1 = require("./teamSkillContext");
134
+ const nativeSteering_1 = require("./nativeSteering");
135
+ const copilotHooks_1 = require("./copilotHooks");
125
136
  const CONFIG_DIR = (0, configDir_1.codiedevStateDir)();
126
137
  exports.CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
127
138
  function readConfig() {
@@ -824,10 +835,13 @@ function getGitHeadSha(cwd) {
824
835
  return null;
825
836
  }
826
837
  }
838
+ function stripRepoUrlCredentials(url) {
839
+ return url.replace(/^(\s*[a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^/?#]*@/, "$1");
840
+ }
827
841
  function buildCapturePayload(input) {
828
842
  return {
829
843
  sessionId: input.sessionId,
830
- repoUrl: input.remoteUrl ?? "",
844
+ repoUrl: stripRepoUrlCredentials(input.remoteUrl ?? ""),
831
845
  repoId: input.matchedRepoId,
832
846
  companyId: input.companyId,
833
847
  branch: input.branch ?? "",
@@ -836,7 +850,7 @@ function buildCapturePayload(input) {
836
850
  // see no change; newer servers index the session under every key.
837
851
  branches: input.branches && input.branches.length ? input.branches : undefined,
838
852
  headShas: input.headShas && input.headShas.length ? input.headShas : undefined,
839
- gitRefs: input.gitRefs && input.gitRefs.length ? input.gitRefs : undefined,
853
+ gitRefs: input.gitRefs && input.gitRefs.length ? input.gitRefs.map(ref => ({ ...ref, repoUrl: stripRepoUrlCredentials(ref.repoUrl) })) : undefined,
840
854
  pullRequestUrls: input.pullRequestUrls && input.pullRequestUrls.length
841
855
  ? input.pullRequestUrls
842
856
  : undefined,
@@ -1044,6 +1058,24 @@ function codexInstalled() {
1044
1058
  function cursorInstalled() {
1045
1059
  return fs.existsSync(CURSOR_DIR);
1046
1060
  }
1061
+ /**
1062
+ * GitHub Copilot: `$COPILOT_HOME` / ~/.copilot (either surface has run once),
1063
+ * the `copilot` CLI on PATH, or VS Code's Copilot Chat extension. VS Code's
1064
+ * agent and the CLI read the same user hooks, so one install covers both.
1065
+ */
1066
+ function copilotInstalled() {
1067
+ if (fs.existsSync((0, copilotHooks_1.copilotHome)()))
1068
+ return true;
1069
+ if (vscodeCopilotInstalled())
1070
+ return true;
1071
+ try {
1072
+ (0, child_process_1.execSync)(process.platform === "win32" ? "where copilot" : "command -v copilot", { stdio: ["pipe", "pipe", "pipe"] });
1073
+ return true;
1074
+ }
1075
+ catch {
1076
+ return false;
1077
+ }
1078
+ }
1047
1079
  function openCodeInstalled() {
1048
1080
  if (fs.existsSync(OPENCODE_DIR))
1049
1081
  return true;
@@ -1112,7 +1144,11 @@ function installOpenCodeCapturePlugin() {
1112
1144
  ` const prompt = Array.isArray(output?.parts) ? output.parts.filter((part) => part?.type === "text").map((part) => part.text || "").join("\\n") : "";\n` +
1113
1145
  ` const decision = invokeSync(PROMPT, { session_id: id, cwd, prompt });\n` +
1114
1146
  ` const context = decision?.hookSpecificOutput?.additionalContext;\n` +
1115
- ` if (context && Array.isArray(output?.parts)) output.parts.push({ type: "text", text: context });\n` +
1147
+ ` if (context && Array.isArray(output?.parts)) {\n` +
1148
+ ` // Preserve the native part IDs required by current engine schemas.\n` +
1149
+ ` const part = output.parts.find((part) => part?.type === "text" && typeof part.text === "string");\n` +
1150
+ ` if (part) part.text += "\\n\\n" + context;\n` +
1151
+ ` }\n` +
1116
1152
  ` },\n` +
1117
1153
  ` event: async ({ event }) => {\n` +
1118
1154
  ` const id = sessionId(event);\n` +
@@ -1475,13 +1511,13 @@ Tell the user the portal URL when the submit returns.
1475
1511
  * (no per-repo CLAUDE.md needed). Idempotent — replaces the existing block
1476
1512
  * between its begin/end markers on re-run.
1477
1513
  */
1478
- function writeInstructionsBlock(targetPath) {
1514
+ function writeInstructionsBlock(targetPath, body = CODIEDEV_INSTRUCTIONS_BODY) {
1479
1515
  const dir = path.dirname(targetPath);
1480
1516
  if (!fs.existsSync(dir)) {
1481
1517
  fs.mkdirSync(dir, { recursive: true });
1482
1518
  }
1483
1519
  const block = `${CODIEDEV_INSTRUCTIONS_BEGIN}\n` +
1484
- CODIEDEV_INSTRUCTIONS_BODY +
1520
+ body +
1485
1521
  `\n${CODIEDEV_INSTRUCTIONS_END}\n`;
1486
1522
  let existing = "";
1487
1523
  if (fs.existsSync(targetPath)) {
@@ -1512,6 +1548,12 @@ function installClaudeCodeInstructions() {
1512
1548
  function installCodexInstructions() {
1513
1549
  writeInstructionsBlock(CODEX_USER_INSTRUCTIONS_PATH);
1514
1550
  }
1551
+ function copilotInstructionsPath() {
1552
+ return path.join((0, copilotHooks_1.copilotHome)(), "copilot-instructions.md");
1553
+ }
1554
+ function installCopilotInstructions() {
1555
+ writeInstructionsBlock(copilotInstructionsPath(), `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}`);
1556
+ }
1515
1557
  // Cursor uses .mdc rule files with YAML frontmatter (description, alwaysApply,
1516
1558
  // globs). `.cursorrules` is deprecated and ignored by Agent mode as of 2026.
1517
1559
  const CURSOR_INSTRUCTIONS_FRONTMATTER = `---
@@ -1527,7 +1569,7 @@ function installCursorInstructions() {
1527
1569
  const block = CURSOR_INSTRUCTIONS_FRONTMATTER +
1528
1570
  CODIEDEV_INSTRUCTIONS_BEGIN +
1529
1571
  "\n" +
1530
- CODIEDEV_INSTRUCTIONS_BODY +
1572
+ `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}` +
1531
1573
  "\n" +
1532
1574
  CODIEDEV_INSTRUCTIONS_END +
1533
1575
  "\n";
@@ -1545,7 +1587,7 @@ function installCursorInstructions() {
1545
1587
  existing.slice(0, beginIdx) +
1546
1588
  CODIEDEV_INSTRUCTIONS_BEGIN +
1547
1589
  "\n" +
1548
- CODIEDEV_INSTRUCTIONS_BODY +
1590
+ `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}` +
1549
1591
  "\n" +
1550
1592
  CODIEDEV_INSTRUCTIONS_END +
1551
1593
  existing.slice(endIdx + CODIEDEV_INSTRUCTIONS_END.length);
@@ -1904,12 +1946,93 @@ function installCursorHook() {
1904
1946
  replaceManaged("sessionStart", ["session-start-cursor"]);
1905
1947
  replaceManaged("sessionEnd", ["capture-cursor"]);
1906
1948
  replaceManaged("stop", ["backup-cursor"]);
1949
+ for (const event of ["postToolUse", "postToolUseFailure", "subagentStart", "subagentStop"])
1950
+ replaceManaged(event, ["observe-cursor"]);
1907
1951
  // Cursor cannot inject dynamic context from beforeSubmitPrompt today.
1908
1952
  // Remove older Codiedev discovery entries so prompts are not uploaded for
1909
1953
  // a response the host will discard; preserve any third-party hooks.
1910
1954
  replaceManaged("beforeSubmitPrompt", []);
1911
1955
  fs.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
1912
1956
  }
1957
+ // GitHub Copilot reads every `$COPILOT_HOME/hooks/*.json`, so Codiedev owns
1958
+ // one file and never edits anyone else's. camelCase events only: VS Code also
1959
+ // accepts Claude-style PascalCase names from the same file, and configuring
1960
+ // both would fire every hook twice.
1961
+ function copilotHooksPath() {
1962
+ return path.join((0, copilotHooks_1.copilotHome)(), "hooks", "codiedev.json");
1963
+ }
1964
+ function copilotManagedHooks(agentSecurityEnabled) {
1965
+ return [
1966
+ ["sessionStart", "session-start-copilot", 10],
1967
+ ["userPromptSubmitted", "discover-copilot", 10],
1968
+ // agentStop = end of a turn: a non-final snapshot. VS Code sessions live
1969
+ // until the chat is closed, so this is what keeps them captured.
1970
+ ["agentStop", "backup-copilot", 30],
1971
+ ["sessionEnd", "capture-copilot", 30],
1972
+ ["errorOccurred", "limit-copilot", 10],
1973
+ ];
1974
+ }
1975
+ const COPILOT_EVENTS = ["sessionStart", "userPromptSubmitted", "agentStop", "sessionEnd", "preToolUse", "postToolUse", "errorOccurred"];
1976
+ function installCopilotHook(agentSecurityEnabled = false) {
1977
+ const hooksPath = copilotHooksPath();
1978
+ let hooksFile = {};
1979
+ try {
1980
+ if (fs.existsSync(hooksPath)) {
1981
+ const raw = fs.readFileSync(hooksPath, "utf8");
1982
+ hooksFile = raw.trim() ? JSON.parse(raw) : {};
1983
+ }
1984
+ }
1985
+ catch {
1986
+ hooksFile = {};
1987
+ }
1988
+ hooksFile.version = 1;
1989
+ const hooks = (hooksFile.hooks && typeof hooksFile.hooks === "object" ? hooksFile.hooks : {});
1990
+ const managed = copilotManagedHooks(agentSecurityEnabled);
1991
+ for (const eventName of COPILOT_EVENTS) {
1992
+ const existing = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
1993
+ // Anything a user added to this file by hand stays.
1994
+ const preserved = existing.filter((hook) => !(0, detection_1.isCodiedevHookCommand)((hook.bash ?? hook.powershell ?? hook.command)));
1995
+ const ours = managed
1996
+ .filter(([event]) => event === eventName)
1997
+ .map(([, subcommand, timeoutSec]) => {
1998
+ const command = resolveHookCommand(subcommand);
1999
+ return { type: "command", bash: command, powershell: command, timeoutSec };
2000
+ });
2001
+ if (preserved.length + ours.length === 0)
2002
+ delete hooks[eventName];
2003
+ else
2004
+ hooks[eventName] = [...preserved, ...ours];
2005
+ }
2006
+ hooksFile.hooks = hooks;
2007
+ fs.mkdirSync(path.dirname(hooksPath), { recursive: true });
2008
+ fs.writeFileSync(hooksPath, JSON.stringify(hooksFile, null, 2), "utf8");
2009
+ }
2010
+ /** Self-heal the Copilot hook file after upgrades or drift (like Cursor's). */
2011
+ function ensureCopilotCodiedevHooksInstalled() {
2012
+ const agentSecurityEnabled = false; // production line: no security hooks
2013
+ try {
2014
+ const hooksPath = copilotHooksPath();
2015
+ if (!fs.existsSync(hooksPath))
2016
+ return;
2017
+ const parsed = JSON.parse(fs.readFileSync(hooksPath, "utf8"));
2018
+ const hooks = parsed.hooks ?? {};
2019
+ const commandsFor = (eventName) => (Array.isArray(hooks[eventName]) ? hooks[eventName] : [])
2020
+ .map((entry) => (entry.bash ?? entry.powershell ?? entry.command))
2021
+ .filter((command) => typeof command === "string" && (0, detection_1.isCodiedevHookCommand)(command));
2022
+ const required = copilotManagedHooks(agentSecurityEnabled);
2023
+ const complete = required.every(([eventName, subcommand]) => commandsFor(eventName).filter((command) => command.trim().endsWith(subcommand)).length === 1);
2024
+ const stale = COPILOT_EVENTS.some((eventName) => commandsFor(eventName).length !== required.filter(([event]) => event === eventName).length);
2025
+ if (complete && !stale)
2026
+ return;
2027
+ installCopilotHook(agentSecurityEnabled);
2028
+ }
2029
+ catch {
2030
+ // Best-effort — a malformed hook file must never block capture.
2031
+ }
2032
+ }
2033
+ /** Repair Cursor's complete flat hook set after upgrades or configuration
2034
+ * drift. This is deliberately separate from Claude Code's nested-hook healer:
2035
+ * the schemas and event names differ, and mixing them risks corrupting both. */
1913
2036
  function ensureCursorCodiedevHooksInstalled() {
1914
2037
  try {
1915
2038
  if (!fs.existsSync(CURSOR_HOOKS_PATH))
@@ -1920,6 +2043,10 @@ function ensureCursorCodiedevHooksInstalled() {
1920
2043
  ["sessionStart", "session-start-cursor"],
1921
2044
  ["sessionEnd", "capture-cursor"],
1922
2045
  ["stop", "backup-cursor"],
2046
+ ["postToolUse", "observe-cursor"],
2047
+ ["postToolUseFailure", "observe-cursor"],
2048
+ ["subagentStart", "observe-cursor"],
2049
+ ["subagentStop", "observe-cursor"],
1923
2050
  ];
1924
2051
  const has = (eventName, subcommand) => {
1925
2052
  const entries = hooks[eventName];
@@ -1981,46 +2108,27 @@ function parseCodexSteering(content) {
1981
2108
  return { interrupts, rejections };
1982
2109
  }
1983
2110
  function parseSteering(content, mode) {
2111
+ if (mode === "cursor" || mode === "copilot")
2112
+ return (0, nativeSteering_1.parseNativeSteering)(content);
1984
2113
  return mode === "codex"
1985
2114
  ? parseCodexSteering(content)
1986
2115
  : parseClaudeSteering(content);
1987
2116
  }
1988
2117
  function parseClaudeUsage(content) {
1989
- // Claude Code logs per-turn usage; sum across the session. `input_tokens` is
1990
- // already fresh (cache read/creation are reported separately).
1991
- let inputTokens = 0;
1992
- let outputTokens = 0;
1993
- let cacheReadTokens = 0;
1994
- let cacheCreationTokens = 0;
2118
+ const models = (0, transcriptModels_1.parseTranscriptModels)(content, "claude-code");
1995
2119
  let model = null;
1996
2120
  for (const line of content.split("\n")) {
1997
- if (!line.trim())
1998
- continue;
1999
2121
  try {
2000
- const e = JSON.parse(line);
2001
- const m = e?.message;
2002
- const u = m?.usage;
2003
- if (u) {
2004
- inputTokens += u.input_tokens ?? 0;
2005
- outputTokens += u.output_tokens ?? 0;
2006
- cacheReadTokens += u.cache_read_input_tokens ?? 0;
2007
- cacheCreationTokens += u.cache_creation_input_tokens ?? 0;
2008
- }
2009
- if (typeof m?.model === "string" && m.model)
2010
- model = m.model;
2011
- }
2012
- catch {
2013
- // skip malformed
2122
+ const value = JSON.parse(line)?.message?.model;
2123
+ if (typeof value === "string" && value.trim() && !value.startsWith("<"))
2124
+ model = value;
2014
2125
  }
2126
+ catch { /* partial records */ }
2015
2127
  }
2016
- return {
2017
- inputTokens,
2018
- outputTokens,
2019
- cacheTokens: cacheReadTokens + cacheCreationTokens,
2020
- cacheReadTokens,
2021
- cacheCreationTokens,
2022
- model,
2023
- };
2128
+ const sum = (field) => models.reduce((total, usage) => total + usage[field], 0);
2129
+ const cacheReadTokens = sum("cacheReadTokens"), cacheCreationTokens = sum("cacheCreationTokens");
2130
+ return { inputTokens: sum("inputTokens"), outputTokens: sum("outputTokens"),
2131
+ cacheTokens: cacheReadTokens + cacheCreationTokens, cacheReadTokens, cacheCreationTokens, model };
2024
2132
  }
2025
2133
  function parseCodexUsage(content) {
2026
2134
  // Codex reports a CUMULATIVE token_count per turn — the last one is the
@@ -2154,20 +2262,30 @@ function parseSessionStart(content) {
2154
2262
  function parseClaudeCodeStats(content) {
2155
2263
  let messageCount = 0;
2156
2264
  let toolCallCount = 0;
2265
+ const messages = new Set();
2266
+ const tools = new Set();
2157
2267
  const lines = content.split("\n").filter((l) => l.trim());
2158
2268
  for (const line of lines) {
2159
2269
  try {
2160
2270
  const entry = JSON.parse(line);
2161
2271
  if (!entry || typeof entry !== "object")
2162
2272
  continue;
2163
- if (entry.type === "user" || entry.type === "assistant") {
2273
+ const id = entry.message?.id ?? entry.uuid;
2274
+ const messageKey = `${entry.type}:${id ?? messages.size}`;
2275
+ if ((entry.type === "user" || entry.type === "assistant") && !messages.has(messageKey)) {
2276
+ messages.add(messageKey);
2164
2277
  messageCount++;
2165
2278
  }
2166
2279
  const c = entry.message?.content;
2167
2280
  if (Array.isArray(c)) {
2168
- for (const block of c) {
2169
- if (block && block.type === "tool_use")
2281
+ for (const [index, block] of c.entries()) {
2282
+ if (!block || block.type !== "tool_use")
2283
+ continue;
2284
+ const toolKey = block.id ?? `${messageKey}:${index}`;
2285
+ if (!tools.has(toolKey)) {
2286
+ tools.add(toolKey);
2170
2287
  toolCallCount++;
2288
+ }
2171
2289
  }
2172
2290
  }
2173
2291
  }
@@ -2215,14 +2333,14 @@ function normalizeOpenCodeTranscript(content) {
2215
2333
  else if (part.type === "tool") {
2216
2334
  normalizedParts.push({
2217
2335
  type: "tool_use",
2218
- id: part.callID ?? part.id ?? "tool",
2336
+ id: part.callID ?? part.id,
2219
2337
  name: part.tool ?? "tool",
2220
2338
  input: part.state?.input ?? {},
2221
2339
  });
2222
2340
  if (part.state?.output != null || part.state?.error != null) {
2223
2341
  normalizedParts.push({
2224
2342
  type: "tool_result",
2225
- tool_use_id: part.callID ?? part.id ?? "tool",
2343
+ tool_use_id: part.callID ?? part.id,
2226
2344
  content: part.state?.output ?? part.state?.error,
2227
2345
  is_error: part.state?.status === "error" || part.state?.error != null,
2228
2346
  });
@@ -2238,6 +2356,7 @@ function normalizeOpenCodeTranscript(content) {
2238
2356
  type: role,
2239
2357
  timestamp,
2240
2358
  message: {
2359
+ id: typeof info.id === "string" ? info.id : undefined,
2241
2360
  role,
2242
2361
  model: typeof info.modelID === "string" ? info.modelID : undefined,
2243
2362
  usage: role === "assistant" ? {
@@ -2568,7 +2687,7 @@ function resolveCaptureFinal(argv2) {
2568
2687
  return false;
2569
2688
  if (argv2 === "capture-opencode-final" || argv2 === "capture-desktop-final")
2570
2689
  return true;
2571
- return argv2 === "capture" || argv2 === "capture-cursor" || argv2 === undefined
2690
+ return argv2 === "capture" || argv2 === "capture-cursor" || argv2 === "capture-copilot" || argv2 === undefined
2572
2691
  ? true
2573
2692
  : undefined;
2574
2693
  }