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.
- package/dist/captureSignals.d.ts +4 -0
- package/dist/captureSignals.js +18 -0
- package/dist/cli.js +3 -1
- package/dist/commands/doctor.js +11 -0
- package/dist/connect.js +9 -1
- package/dist/copilotHooks.d.ts +85 -0
- package/dist/copilotHooks.js +458 -0
- package/dist/copilotInstallation.d.ts +10 -0
- package/dist/copilotInstallation.js +65 -0
- package/dist/cursorHooks.d.ts +5 -1
- package/dist/cursorHooks.js +50 -7
- package/dist/cursorLifecycle.d.ts +2 -0
- package/dist/cursorLifecycle.js +115 -0
- package/dist/hook.js +133 -36
- package/dist/launchContext.d.ts +20 -6
- package/dist/launchContext.js +65 -10
- package/dist/limitEvent.d.ts +3 -0
- package/dist/limitEvent.js +42 -0
- package/dist/limitEventReporter.d.ts +1 -1
- package/dist/limitEventReporter.js +1 -1
- package/dist/nativeSteering.d.ts +5 -0
- package/dist/nativeSteering.js +27 -0
- package/dist/teamSkillContext.d.ts +9 -0
- package/dist/teamSkillContext.js +11 -0
- package/dist/transcriptModels.d.ts +2 -0
- package/dist/transcriptModels.js +8 -2
- package/dist/utils.d.ts +19 -3
- package/dist/utils.js +162 -43
- package/package.json +7 -4
package/dist/limitEvent.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.classifyLimitKind = classifyLimitKind;
|
|
4
4
|
exports.parseLimitEventFromTranscript = parseLimitEventFromTranscript;
|
|
5
5
|
exports.parseCodexLimitEventFromTranscript = parseCodexLimitEventFromTranscript;
|
|
6
|
+
exports.parseNativeLimitError = parseNativeLimitError;
|
|
7
|
+
exports.parseNativeLimitEventFromTranscript = parseNativeLimitEventFromTranscript;
|
|
6
8
|
exports.tailForLimitEvent = tailForLimitEvent;
|
|
7
9
|
function classifyLimitKind(text) {
|
|
8
10
|
const t = text.toLowerCase();
|
|
@@ -129,6 +131,46 @@ function parseCodexLimitEventFromTranscript(transcript) {
|
|
|
129
131
|
}
|
|
130
132
|
return found;
|
|
131
133
|
}
|
|
134
|
+
/** Only explicit provider errors qualify; never scan user/assistant prose. */
|
|
135
|
+
function parseNativeLimitError(input, now = Date.now()) {
|
|
136
|
+
const context = input.errorContext ?? input.error_context;
|
|
137
|
+
if (context !== undefined && context !== "model_call")
|
|
138
|
+
return null;
|
|
139
|
+
const error = input.error;
|
|
140
|
+
const fields = error && typeof error === "object" ? error : {};
|
|
141
|
+
const code = String(fields.code ?? fields.name ?? input.error_code ?? "");
|
|
142
|
+
const text = typeof error === "string" ? error
|
|
143
|
+
: typeof fields.message === "string" ? fields.message
|
|
144
|
+
: typeof input.error_message === "string" ? input.error_message : "";
|
|
145
|
+
if (!/^(?:429|rate[_-]?limit(?:ed|_exceeded|_error)?|RateLimitError|quota_exceeded)$/i.test(code)
|
|
146
|
+
&& !/\b(?:rate limit (?:exceeded|reached)|too many requests|usage limit (?:exceeded|reached)|premium requests? (?:exhausted|limit reached))\b/i.test(text))
|
|
147
|
+
return null;
|
|
148
|
+
const rawAt = input.timestamp;
|
|
149
|
+
const parsedAt = typeof rawAt === "number" ? rawAt : typeof rawAt === "string" ? Date.parse(rawAt) : NaN;
|
|
150
|
+
return { kind: classifyLimitKind(text), at: Number.isFinite(parsedAt) && parsedAt > 0 ? parsedAt : now,
|
|
151
|
+
resetText: parseResetText(text), reason: code || "rate_limit" };
|
|
152
|
+
}
|
|
153
|
+
function parseNativeLimitEventFromTranscript(transcript) {
|
|
154
|
+
let found = null;
|
|
155
|
+
for (const line of transcript.split("\n")) {
|
|
156
|
+
try {
|
|
157
|
+
const entry = JSON.parse(line);
|
|
158
|
+
if (entry.type === "agent.limit" && Number.isFinite(entry.at) && entry.at > 0
|
|
159
|
+
&& ["session", "user_spend", "org_spend", "other"].includes(entry.kind)) {
|
|
160
|
+
found = { kind: entry.kind, at: entry.at, resetText: typeof entry.resetText === "string" ? entry.resetText : null,
|
|
161
|
+
...(typeof entry.reason === "string" ? { reason: entry.reason } : {}) };
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!entry.timestamp)
|
|
165
|
+
continue; // no stable timestamp means snapshots cannot deduplicate
|
|
166
|
+
if (entry.type === "agent.error" || entry.type === "session.error") {
|
|
167
|
+
found = parseNativeLimitError({ ...(entry.data ?? entry), timestamp: entry.timestamp }) ?? found;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch { /* tolerant of incomplete transcript tails */ }
|
|
171
|
+
}
|
|
172
|
+
return found;
|
|
173
|
+
}
|
|
132
174
|
// Tail of the raw transcript to ship with the limit event so the server can
|
|
133
175
|
// summarize "what they were working on" immediately — without waiting for the
|
|
134
176
|
// SessionEnd capture (which can lag hours, or drop entirely on Windows).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type LimitEvent } from "./limitEvent";
|
|
2
|
-
export type LimitEventSource = "claude-code" | "codex";
|
|
2
|
+
export type LimitEventSource = "claude-code" | "codex" | "cursor" | "copilot";
|
|
3
3
|
export type LimitEventPayload = LimitEvent & {
|
|
4
4
|
source: LimitEventSource;
|
|
5
5
|
sessionId?: string;
|
|
@@ -76,7 +76,7 @@ function postLimitEvent(config, payload, timeoutMs = 5_000) {
|
|
|
76
76
|
async function reportTranscriptLimitEvent(args) {
|
|
77
77
|
const parse = args.source === "codex"
|
|
78
78
|
? limitEvent_1.parseCodexLimitEventFromTranscript
|
|
79
|
-
: limitEvent_1.parseLimitEventFromTranscript;
|
|
79
|
+
: args.source === "cursor" || args.source === "copilot" ? limitEvent_1.parseNativeLimitEventFromTranscript : limitEvent_1.parseLimitEventFromTranscript;
|
|
80
80
|
const parsed = parse(args.transcript);
|
|
81
81
|
if (!parsed && !args.emitMinimalOnMiss)
|
|
82
82
|
return false;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseNativeSteering = parseNativeSteering;
|
|
4
|
+
/** Native records only: quoted denial/interrupt text in conversation is not evidence. */
|
|
5
|
+
function parseNativeSteering(content) {
|
|
6
|
+
const interrupts = new Set();
|
|
7
|
+
const abortedTurns = new Set();
|
|
8
|
+
const rejections = new Set();
|
|
9
|
+
for (const [index, line] of content.split("\n").entries()) {
|
|
10
|
+
try {
|
|
11
|
+
const event = JSON.parse(line);
|
|
12
|
+
const id = String(event.id ?? event.uuid ?? event.toolCallId ?? index);
|
|
13
|
+
if (event.type === "agent.interruption")
|
|
14
|
+
interrupts.add(id);
|
|
15
|
+
if (event.type === "turn_ended" && event.status === "aborted")
|
|
16
|
+
abortedTurns.add(id);
|
|
17
|
+
if (event.type === "agent.rejection")
|
|
18
|
+
rejections.add(id);
|
|
19
|
+
// Cursor tool failures distinguish a user-denied permission from an
|
|
20
|
+
// ordinary tool failure. Don't guess from free-form error messages.
|
|
21
|
+
if (event.type === "tool_result" && event.error?.code === "user_denied")
|
|
22
|
+
rejections.add(id);
|
|
23
|
+
}
|
|
24
|
+
catch { /* incomplete transcript records are ignored */ }
|
|
25
|
+
}
|
|
26
|
+
return { interrupts: Math.max(interrupts.size, abortedTurns.size), rejections: rejections.size };
|
|
27
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Session-start context uses each client's supported output contract. */
|
|
2
|
+
export declare const TEAM_SKILL_CONTEXT = "This machine is connected to Codiedev, the team's artifact and skill library.\nBefore a substantial coding task, run codiedev search \"<brief task description>\" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.\nUse codiedev push <file.md> to share an artifact when asked, codiedev ask \"<question>\" for team knowledge, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.";
|
|
3
|
+
export declare function teamSkillSessionOutput(source: "cursor" | "copilot", context?: string): {
|
|
4
|
+
additional_context: string;
|
|
5
|
+
additionalContext?: undefined;
|
|
6
|
+
} | {
|
|
7
|
+
additionalContext: string;
|
|
8
|
+
additional_context?: undefined;
|
|
9
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TEAM_SKILL_CONTEXT = void 0;
|
|
4
|
+
exports.teamSkillSessionOutput = teamSkillSessionOutput;
|
|
5
|
+
/** Session-start context uses each client's supported output contract. */
|
|
6
|
+
exports.TEAM_SKILL_CONTEXT = `This machine is connected to Codiedev, the team's artifact and skill library.
|
|
7
|
+
Before a substantial coding task, run codiedev search "<brief task description>" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.
|
|
8
|
+
Use codiedev push <file.md> to share an artifact when asked, codiedev ask "<question>" for team knowledge, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.`;
|
|
9
|
+
function teamSkillSessionOutput(source, context = exports.TEAM_SKILL_CONTEXT) {
|
|
10
|
+
return source === "cursor" ? { additional_context: context } : { additionalContext: context };
|
|
11
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Compact per-model usage, derived from captured transcript records. */
|
|
2
2
|
export type TranscriptModelUsage = {
|
|
3
3
|
model: string;
|
|
4
|
+
/** False for Copilot model/request hints before token totals arrive. */
|
|
5
|
+
tokensAvailable?: boolean;
|
|
4
6
|
/** Assistant message IDs; nonzero cumulative usage events for Codex. */
|
|
5
7
|
requests: number;
|
|
6
8
|
inputTokens: number;
|
package/dist/transcriptModels.js
CHANGED
|
@@ -44,6 +44,7 @@ function* transcriptRecords(content, source) {
|
|
|
44
44
|
function parseTranscriptModels(content, source) {
|
|
45
45
|
const buckets = new Map();
|
|
46
46
|
const messages = new Map();
|
|
47
|
+
const tokenEvidence = new Set();
|
|
47
48
|
let model = "Unattributed";
|
|
48
49
|
let previous = {
|
|
49
50
|
inputTokens: 0,
|
|
@@ -143,9 +144,12 @@ function parseTranscriptModels(content, source) {
|
|
|
143
144
|
model =
|
|
144
145
|
typeof message.model === "string" ? message.model : "Unattributed";
|
|
145
146
|
const raw = message.usage;
|
|
147
|
+
// Copilot's shutdown token totals carry `requests: 0`: its calls are
|
|
148
|
+
// already counted from the assistant records.
|
|
149
|
+
const requests = event.requests;
|
|
146
150
|
usage = {
|
|
147
151
|
...empty(model),
|
|
148
|
-
requests: 1,
|
|
152
|
+
requests: typeof requests === "number" && Number.isFinite(requests) ? Math.max(0, requests) : 1,
|
|
149
153
|
inputTokens: positive(raw.input_tokens),
|
|
150
154
|
outputTokens: positive(raw.output_tokens),
|
|
151
155
|
cacheReadTokens: positive(raw.cache_read_input_tokens),
|
|
@@ -153,6 +157,8 @@ function parseTranscriptModels(content, source) {
|
|
|
153
157
|
};
|
|
154
158
|
messageId = message.id ?? event.uuid ?? messageId;
|
|
155
159
|
}
|
|
160
|
+
if (source === "copilot" && event.type === "copilot.usage")
|
|
161
|
+
tokenEvidence.add(usage.model);
|
|
156
162
|
// Client placeholders are not model executions.
|
|
157
163
|
if (!usage.model.trim() || usage.model.startsWith("<"))
|
|
158
164
|
continue;
|
|
@@ -197,7 +203,7 @@ function parseTranscriptModels(content, source) {
|
|
|
197
203
|
delete buckets.get(key).costUsd;
|
|
198
204
|
for (const key of incompleteReasoning)
|
|
199
205
|
delete buckets.get(key).reasoningTokens;
|
|
200
|
-
return [...buckets.values()];
|
|
206
|
+
return [...buckets.values()].map(row => source === "copilot" && !tokenEvidence.has(row.model) ? { ...row, tokensAvailable: false } : row);
|
|
201
207
|
}
|
|
202
208
|
function totalModelTokens(row) {
|
|
203
209
|
return (row.inputTokens +
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import type { CaptureSignals } from "./captureSignals";
|
|
3
|
-
import type
|
|
3
|
+
import { type TranscriptModelUsage } from "./transcriptModels";
|
|
4
4
|
import { spawn as nodeSpawn } from "child_process";
|
|
5
5
|
import type { CaptureLogEntry } from "./captureLog";
|
|
6
6
|
import { codiedevStateDir } from "./configDir";
|
|
@@ -89,6 +89,7 @@ export interface CapturePayloadInput {
|
|
|
89
89
|
final?: boolean;
|
|
90
90
|
costUsd?: number;
|
|
91
91
|
}
|
|
92
|
+
export declare function stripRepoUrlCredentials(url: string): string;
|
|
92
93
|
export declare function buildCapturePayload(input: CapturePayloadInput): Record<string, unknown>;
|
|
93
94
|
export declare function hashToken(token: string): string;
|
|
94
95
|
export declare function extractHookNodePath(cmd: string | undefined): string | undefined;
|
|
@@ -96,6 +97,12 @@ export declare function resetStableNodeCacheForTests(): void;
|
|
|
96
97
|
export declare function claudeCodeInstalled(): boolean;
|
|
97
98
|
export declare function codexInstalled(): boolean;
|
|
98
99
|
export declare function cursorInstalled(): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* GitHub Copilot: `$COPILOT_HOME` / ~/.copilot (either surface has run once),
|
|
102
|
+
* the `copilot` CLI on PATH, or VS Code's Copilot Chat extension. VS Code's
|
|
103
|
+
* agent and the CLI read the same user hooks, so one install covers both.
|
|
104
|
+
*/
|
|
105
|
+
export declare function copilotInstalled(): boolean;
|
|
99
106
|
export declare function openCodeInstalled(): boolean;
|
|
100
107
|
/** Install a dormant global OpenCode adapter. Installing it unconditionally
|
|
101
108
|
* keeps `codiedev connect` as the only setup step even when Codie/OpenCode is
|
|
@@ -116,6 +123,8 @@ export declare function ensureAllCodiedevHooksInstalled(): void;
|
|
|
116
123
|
export declare function ensureSessionStartHookInstalled(): void;
|
|
117
124
|
export declare function installClaudeCodeInstructions(): void;
|
|
118
125
|
export declare function installCodexInstructions(): void;
|
|
126
|
+
export declare function copilotInstructionsPath(): string;
|
|
127
|
+
export declare function installCopilotInstructions(): void;
|
|
119
128
|
export declare function installCursorInstructions(): void;
|
|
120
129
|
/**
|
|
121
130
|
* Install the Codiedev MCP server into Claude Code's user-scope config.
|
|
@@ -173,6 +182,13 @@ export declare function installCodexMcp(): boolean;
|
|
|
173
182
|
export declare function installCodexHook(): void;
|
|
174
183
|
export declare function ensureCodexLimitHookInstalled(): void;
|
|
175
184
|
export declare function installCursorHook(): void;
|
|
185
|
+
export declare function copilotHooksPath(): string;
|
|
186
|
+
export declare function installCopilotHook(agentSecurityEnabled?: boolean): void;
|
|
187
|
+
/** Self-heal the Copilot hook file after upgrades or drift (like Cursor's). */
|
|
188
|
+
export declare function ensureCopilotCodiedevHooksInstalled(): void;
|
|
189
|
+
/** Repair Cursor's complete flat hook set after upgrades or configuration
|
|
190
|
+
* drift. This is deliberately separate from Claude Code's nested-hook healer:
|
|
191
|
+
* the schemas and event names differ, and mixing them risks corrupting both. */
|
|
176
192
|
export declare function ensureCursorCodiedevHooksInstalled(): void;
|
|
177
193
|
export interface ParsedStats {
|
|
178
194
|
messageCount: number;
|
|
@@ -184,7 +200,7 @@ export interface SteeringStats {
|
|
|
184
200
|
}
|
|
185
201
|
export declare function parseClaudeSteering(content: string): SteeringStats;
|
|
186
202
|
export declare function parseCodexSteering(content: string): SteeringStats;
|
|
187
|
-
export declare function parseSteering(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode"): SteeringStats;
|
|
203
|
+
export declare function parseSteering(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "copilot"): SteeringStats;
|
|
188
204
|
export interface UsageStats {
|
|
189
205
|
inputTokens: number;
|
|
190
206
|
outputTokens: number;
|
|
@@ -196,7 +212,7 @@ export interface UsageStats {
|
|
|
196
212
|
/** Engine-reported spend (opencode SDK `info.cost`). Desktop mode only. */
|
|
197
213
|
costUsd?: number;
|
|
198
214
|
}
|
|
199
|
-
export declare function parseUsage(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "desktop"): UsageStats;
|
|
215
|
+
export declare function parseUsage(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "desktop" | "copilot"): UsageStats;
|
|
200
216
|
export declare function parseSessionStart(content: string): number | null;
|
|
201
217
|
export declare function parseClaudeCodeStats(content: string): ParsedStats;
|
|
202
218
|
/** Convert OpenCode's SDK message array (`{info, parts}[]`) into the same
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2001
|
-
|
|
2002
|
-
|
|
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
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|