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.
@@ -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;
@@ -1475,13 +1507,13 @@ Tell the user the portal URL when the submit returns.
1475
1507
  * (no per-repo CLAUDE.md needed). Idempotent — replaces the existing block
1476
1508
  * between its begin/end markers on re-run.
1477
1509
  */
1478
- function writeInstructionsBlock(targetPath) {
1510
+ function writeInstructionsBlock(targetPath, body = CODIEDEV_INSTRUCTIONS_BODY) {
1479
1511
  const dir = path.dirname(targetPath);
1480
1512
  if (!fs.existsSync(dir)) {
1481
1513
  fs.mkdirSync(dir, { recursive: true });
1482
1514
  }
1483
1515
  const block = `${CODIEDEV_INSTRUCTIONS_BEGIN}\n` +
1484
- CODIEDEV_INSTRUCTIONS_BODY +
1516
+ body +
1485
1517
  `\n${CODIEDEV_INSTRUCTIONS_END}\n`;
1486
1518
  let existing = "";
1487
1519
  if (fs.existsSync(targetPath)) {
@@ -1512,6 +1544,12 @@ function installClaudeCodeInstructions() {
1512
1544
  function installCodexInstructions() {
1513
1545
  writeInstructionsBlock(CODEX_USER_INSTRUCTIONS_PATH);
1514
1546
  }
1547
+ function copilotInstructionsPath() {
1548
+ return path.join((0, copilotHooks_1.copilotHome)(), "copilot-instructions.md");
1549
+ }
1550
+ function installCopilotInstructions() {
1551
+ writeInstructionsBlock(copilotInstructionsPath(), `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}`);
1552
+ }
1515
1553
  // Cursor uses .mdc rule files with YAML frontmatter (description, alwaysApply,
1516
1554
  // globs). `.cursorrules` is deprecated and ignored by Agent mode as of 2026.
1517
1555
  const CURSOR_INSTRUCTIONS_FRONTMATTER = `---
@@ -1527,7 +1565,7 @@ function installCursorInstructions() {
1527
1565
  const block = CURSOR_INSTRUCTIONS_FRONTMATTER +
1528
1566
  CODIEDEV_INSTRUCTIONS_BEGIN +
1529
1567
  "\n" +
1530
- CODIEDEV_INSTRUCTIONS_BODY +
1568
+ `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}` +
1531
1569
  "\n" +
1532
1570
  CODIEDEV_INSTRUCTIONS_END +
1533
1571
  "\n";
@@ -1545,7 +1583,7 @@ function installCursorInstructions() {
1545
1583
  existing.slice(0, beginIdx) +
1546
1584
  CODIEDEV_INSTRUCTIONS_BEGIN +
1547
1585
  "\n" +
1548
- CODIEDEV_INSTRUCTIONS_BODY +
1586
+ `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}` +
1549
1587
  "\n" +
1550
1588
  CODIEDEV_INSTRUCTIONS_END +
1551
1589
  existing.slice(endIdx + CODIEDEV_INSTRUCTIONS_END.length);
@@ -1904,12 +1942,93 @@ function installCursorHook() {
1904
1942
  replaceManaged("sessionStart", ["session-start-cursor"]);
1905
1943
  replaceManaged("sessionEnd", ["capture-cursor"]);
1906
1944
  replaceManaged("stop", ["backup-cursor"]);
1945
+ for (const event of ["postToolUse", "postToolUseFailure", "subagentStart", "subagentStop"])
1946
+ replaceManaged(event, ["observe-cursor"]);
1907
1947
  // Cursor cannot inject dynamic context from beforeSubmitPrompt today.
1908
1948
  // Remove older Codiedev discovery entries so prompts are not uploaded for
1909
1949
  // a response the host will discard; preserve any third-party hooks.
1910
1950
  replaceManaged("beforeSubmitPrompt", []);
1911
1951
  fs.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
1912
1952
  }
1953
+ // GitHub Copilot reads every `$COPILOT_HOME/hooks/*.json`, so Codiedev owns
1954
+ // one file and never edits anyone else's. camelCase events only: VS Code also
1955
+ // accepts Claude-style PascalCase names from the same file, and configuring
1956
+ // both would fire every hook twice.
1957
+ function copilotHooksPath() {
1958
+ return path.join((0, copilotHooks_1.copilotHome)(), "hooks", "codiedev.json");
1959
+ }
1960
+ function copilotManagedHooks(agentSecurityEnabled) {
1961
+ return [
1962
+ ["sessionStart", "session-start-copilot", 10],
1963
+ ["userPromptSubmitted", "discover-copilot", 10],
1964
+ // agentStop = end of a turn: a non-final snapshot. VS Code sessions live
1965
+ // until the chat is closed, so this is what keeps them captured.
1966
+ ["agentStop", "backup-copilot", 30],
1967
+ ["sessionEnd", "capture-copilot", 30],
1968
+ ["errorOccurred", "limit-copilot", 10],
1969
+ ];
1970
+ }
1971
+ const COPILOT_EVENTS = ["sessionStart", "userPromptSubmitted", "agentStop", "sessionEnd", "preToolUse", "postToolUse", "errorOccurred"];
1972
+ function installCopilotHook(agentSecurityEnabled = false) {
1973
+ const hooksPath = copilotHooksPath();
1974
+ let hooksFile = {};
1975
+ try {
1976
+ if (fs.existsSync(hooksPath)) {
1977
+ const raw = fs.readFileSync(hooksPath, "utf8");
1978
+ hooksFile = raw.trim() ? JSON.parse(raw) : {};
1979
+ }
1980
+ }
1981
+ catch {
1982
+ hooksFile = {};
1983
+ }
1984
+ hooksFile.version = 1;
1985
+ const hooks = (hooksFile.hooks && typeof hooksFile.hooks === "object" ? hooksFile.hooks : {});
1986
+ const managed = copilotManagedHooks(agentSecurityEnabled);
1987
+ for (const eventName of COPILOT_EVENTS) {
1988
+ const existing = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
1989
+ // Anything a user added to this file by hand stays.
1990
+ const preserved = existing.filter((hook) => !(0, detection_1.isCodiedevHookCommand)((hook.bash ?? hook.powershell ?? hook.command)));
1991
+ const ours = managed
1992
+ .filter(([event]) => event === eventName)
1993
+ .map(([, subcommand, timeoutSec]) => {
1994
+ const command = resolveHookCommand(subcommand);
1995
+ return { type: "command", bash: command, powershell: command, timeoutSec };
1996
+ });
1997
+ if (preserved.length + ours.length === 0)
1998
+ delete hooks[eventName];
1999
+ else
2000
+ hooks[eventName] = [...preserved, ...ours];
2001
+ }
2002
+ hooksFile.hooks = hooks;
2003
+ fs.mkdirSync(path.dirname(hooksPath), { recursive: true });
2004
+ fs.writeFileSync(hooksPath, JSON.stringify(hooksFile, null, 2), "utf8");
2005
+ }
2006
+ /** Self-heal the Copilot hook file after upgrades or drift (like Cursor's). */
2007
+ function ensureCopilotCodiedevHooksInstalled() {
2008
+ const agentSecurityEnabled = false; // production line: no security hooks
2009
+ try {
2010
+ const hooksPath = copilotHooksPath();
2011
+ if (!fs.existsSync(hooksPath))
2012
+ return;
2013
+ const parsed = JSON.parse(fs.readFileSync(hooksPath, "utf8"));
2014
+ const hooks = parsed.hooks ?? {};
2015
+ const commandsFor = (eventName) => (Array.isArray(hooks[eventName]) ? hooks[eventName] : [])
2016
+ .map((entry) => (entry.bash ?? entry.powershell ?? entry.command))
2017
+ .filter((command) => typeof command === "string" && (0, detection_1.isCodiedevHookCommand)(command));
2018
+ const required = copilotManagedHooks(agentSecurityEnabled);
2019
+ const complete = required.every(([eventName, subcommand]) => commandsFor(eventName).filter((command) => command.trim().endsWith(subcommand)).length === 1);
2020
+ const stale = COPILOT_EVENTS.some((eventName) => commandsFor(eventName).length !== required.filter(([event]) => event === eventName).length);
2021
+ if (complete && !stale)
2022
+ return;
2023
+ installCopilotHook(agentSecurityEnabled);
2024
+ }
2025
+ catch {
2026
+ // Best-effort — a malformed hook file must never block capture.
2027
+ }
2028
+ }
2029
+ /** Repair Cursor's complete flat hook set after upgrades or configuration
2030
+ * drift. This is deliberately separate from Claude Code's nested-hook healer:
2031
+ * the schemas and event names differ, and mixing them risks corrupting both. */
1913
2032
  function ensureCursorCodiedevHooksInstalled() {
1914
2033
  try {
1915
2034
  if (!fs.existsSync(CURSOR_HOOKS_PATH))
@@ -1920,6 +2039,10 @@ function ensureCursorCodiedevHooksInstalled() {
1920
2039
  ["sessionStart", "session-start-cursor"],
1921
2040
  ["sessionEnd", "capture-cursor"],
1922
2041
  ["stop", "backup-cursor"],
2042
+ ["postToolUse", "observe-cursor"],
2043
+ ["postToolUseFailure", "observe-cursor"],
2044
+ ["subagentStart", "observe-cursor"],
2045
+ ["subagentStop", "observe-cursor"],
1923
2046
  ];
1924
2047
  const has = (eventName, subcommand) => {
1925
2048
  const entries = hooks[eventName];
@@ -1981,46 +2104,27 @@ function parseCodexSteering(content) {
1981
2104
  return { interrupts, rejections };
1982
2105
  }
1983
2106
  function parseSteering(content, mode) {
2107
+ if (mode === "cursor" || mode === "copilot")
2108
+ return (0, nativeSteering_1.parseNativeSteering)(content);
1984
2109
  return mode === "codex"
1985
2110
  ? parseCodexSteering(content)
1986
2111
  : parseClaudeSteering(content);
1987
2112
  }
1988
2113
  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;
2114
+ const models = (0, transcriptModels_1.parseTranscriptModels)(content, "claude-code");
1995
2115
  let model = null;
1996
2116
  for (const line of content.split("\n")) {
1997
- if (!line.trim())
1998
- continue;
1999
2117
  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
2118
+ const value = JSON.parse(line)?.message?.model;
2119
+ if (typeof value === "string" && value.trim() && !value.startsWith("<"))
2120
+ model = value;
2014
2121
  }
2122
+ catch { /* partial records */ }
2015
2123
  }
2016
- return {
2017
- inputTokens,
2018
- outputTokens,
2019
- cacheTokens: cacheReadTokens + cacheCreationTokens,
2020
- cacheReadTokens,
2021
- cacheCreationTokens,
2022
- model,
2023
- };
2124
+ const sum = (field) => models.reduce((total, usage) => total + usage[field], 0);
2125
+ const cacheReadTokens = sum("cacheReadTokens"), cacheCreationTokens = sum("cacheCreationTokens");
2126
+ return { inputTokens: sum("inputTokens"), outputTokens: sum("outputTokens"),
2127
+ cacheTokens: cacheReadTokens + cacheCreationTokens, cacheReadTokens, cacheCreationTokens, model };
2024
2128
  }
2025
2129
  function parseCodexUsage(content) {
2026
2130
  // Codex reports a CUMULATIVE token_count per turn — the last one is the
@@ -2154,20 +2258,30 @@ function parseSessionStart(content) {
2154
2258
  function parseClaudeCodeStats(content) {
2155
2259
  let messageCount = 0;
2156
2260
  let toolCallCount = 0;
2261
+ const messages = new Set();
2262
+ const tools = new Set();
2157
2263
  const lines = content.split("\n").filter((l) => l.trim());
2158
2264
  for (const line of lines) {
2159
2265
  try {
2160
2266
  const entry = JSON.parse(line);
2161
2267
  if (!entry || typeof entry !== "object")
2162
2268
  continue;
2163
- if (entry.type === "user" || entry.type === "assistant") {
2269
+ const id = entry.message?.id ?? entry.uuid;
2270
+ const messageKey = `${entry.type}:${id ?? messages.size}`;
2271
+ if ((entry.type === "user" || entry.type === "assistant") && !messages.has(messageKey)) {
2272
+ messages.add(messageKey);
2164
2273
  messageCount++;
2165
2274
  }
2166
2275
  const c = entry.message?.content;
2167
2276
  if (Array.isArray(c)) {
2168
- for (const block of c) {
2169
- if (block && block.type === "tool_use")
2277
+ for (const [index, block] of c.entries()) {
2278
+ if (!block || block.type !== "tool_use")
2279
+ continue;
2280
+ const toolKey = block.id ?? `${messageKey}:${index}`;
2281
+ if (!tools.has(toolKey)) {
2282
+ tools.add(toolKey);
2170
2283
  toolCallCount++;
2284
+ }
2171
2285
  }
2172
2286
  }
2173
2287
  }
@@ -2215,14 +2329,14 @@ function normalizeOpenCodeTranscript(content) {
2215
2329
  else if (part.type === "tool") {
2216
2330
  normalizedParts.push({
2217
2331
  type: "tool_use",
2218
- id: part.callID ?? part.id ?? "tool",
2332
+ id: part.callID ?? part.id,
2219
2333
  name: part.tool ?? "tool",
2220
2334
  input: part.state?.input ?? {},
2221
2335
  });
2222
2336
  if (part.state?.output != null || part.state?.error != null) {
2223
2337
  normalizedParts.push({
2224
2338
  type: "tool_result",
2225
- tool_use_id: part.callID ?? part.id ?? "tool",
2339
+ tool_use_id: part.callID ?? part.id,
2226
2340
  content: part.state?.output ?? part.state?.error,
2227
2341
  is_error: part.state?.status === "error" || part.state?.error != null,
2228
2342
  });
@@ -2238,6 +2352,7 @@ function normalizeOpenCodeTranscript(content) {
2238
2352
  type: role,
2239
2353
  timestamp,
2240
2354
  message: {
2355
+ id: typeof info.id === "string" ? info.id : undefined,
2241
2356
  role,
2242
2357
  model: typeof info.modelID === "string" ? info.modelID : undefined,
2243
2358
  usage: role === "assistant" ? {
@@ -2568,7 +2683,7 @@ function resolveCaptureFinal(argv2) {
2568
2683
  return false;
2569
2684
  if (argv2 === "capture-opencode-final" || argv2 === "capture-desktop-final")
2570
2685
  return true;
2571
- return argv2 === "capture" || argv2 === "capture-cursor" || argv2 === undefined
2686
+ return argv2 === "capture" || argv2 === "capture-cursor" || argv2 === "capture-copilot" || argv2 === undefined
2572
2687
  ? true
2573
2688
  : undefined;
2574
2689
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.60",
4
- "description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
3
+ "version": "0.9.61",
4
+ "description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration \u2014 and run the governed Codie harness",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",
7
7
  "codiedev-hook": "dist/hook.js",
@@ -12,10 +12,13 @@
12
12
  "dev": "tsc --watch",
13
13
  "prepublishOnly": "tsc",
14
14
  "test": "vitest run",
15
- "test:functional": "vitest run src/__tests__/limitEvent.functional.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts",
16
- "test:harness-parity": "vitest run src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts",
15
+ "test:functional": "vitest run src/__tests__/limitEvent.functional.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts",
16
+ "test:harness-parity": "vitest run src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts",
17
17
  "test:harness-binaries": "node scripts/checkHarnessBinaries.mjs",
18
- "test:watch": "vitest"
18
+ "test:watch": "vitest",
19
+ "test:live-sources": "npm run build && node scripts/liveSourceParity.mjs",
20
+ "test:ide-sources": "npm run build && node scripts/liveSourceParity.mjs --ide",
21
+ "test:metadata-parity": "vitest run src/__tests__/sourceMetadataMatrix.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/copilotCapture.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/nativeSourceParity.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/subagents.test.ts src/__tests__/transcriptModels.test.ts src/__tests__/captureRetry.test.ts src/__tests__/limitEvent.functional.test.ts src/__tests__/captureExclusions.test.ts"
19
22
  },
20
23
  "files": [
21
24
  "dist",