codiedev 0.9.51 → 0.10.0-beta.1
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/README.md +9 -1
- package/dist/cli.js +6 -11
- package/dist/codieStartup.d.ts +2 -0
- package/dist/codieStartup.js +1 -0
- package/dist/connect.js +5 -2
- package/dist/cursorHooks.d.ts +23 -0
- package/dist/cursorHooks.js +81 -0
- package/dist/hook.js +127 -52
- package/dist/limitEvent.d.ts +8 -1
- package/dist/limitEvent.js +58 -0
- package/dist/limitEventReporter.d.ts +21 -0
- package/dist/limitEventReporter.js +91 -0
- package/dist/mcp.js +1 -93
- package/dist/repoResolver.js +1 -1
- package/dist/security.d.ts +6 -1
- package/dist/security.js +15 -0
- package/dist/securityInstall.d.ts +2 -1
- package/dist/securityInstall.js +23 -11
- package/dist/securityPilot.js +1 -1
- package/dist/securityPolicy.d.ts +2 -1
- package/dist/securityPolicy.js +9 -0
- package/dist/utils.d.ts +10 -2
- package/dist/utils.js +202 -66
- package/package.json +5 -2
- package/dist/commands/post.d.ts +0 -2
- package/dist/commands/post.js +0 -96
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ The CLI will prompt you for an API token (get one at https://codiedev.com/portal
|
|
|
12
12
|
|
|
13
13
|
- **Claude Code** — `SessionEnd` hook in `~/.claude/settings.json`
|
|
14
14
|
- **Codex** — `Stop` hook in `~/.codex/hooks.json`
|
|
15
|
-
- **Cursor** —
|
|
15
|
+
- **Cursor** — native session, prompt-security, tool-security, backup, and capture hooks in `~/.cursor/hooks.json`
|
|
16
16
|
- **OpenCode / Codie** — plugin in `~/.config/opencode/plugins/codiedev.js`
|
|
17
17
|
|
|
18
18
|
Installed clients are wired automatically. The OpenCode adapter is staged even
|
|
@@ -49,6 +49,14 @@ container needs its own rule unless `~/.codiedev/config.json` is persisted.
|
|
|
49
49
|
|
|
50
50
|
Codex doesn't currently expose a `SessionEnd` event, so the hook runs on `Stop` (fires after each assistant turn). The backend dedupes by `(company, session_id)` and replaces the stored transcript on re-upload so the latest turn's snapshot wins. Summarization happens once per session; subsequent turn uploads keep the raw transcript fresh but don't re-summarize (to avoid running up LLM cost).
|
|
51
51
|
|
|
52
|
+
## Cursor note
|
|
53
|
+
|
|
54
|
+
Cursor uses its native hook events for session start/end, prompt blocking,
|
|
55
|
+
pre/post-tool security, and stop-time backup. Cursor's `beforeSubmitPrompt`
|
|
56
|
+
cannot inject dynamic context today, so task-specific discovery is not claimed.
|
|
57
|
+
Cursor sessions keep their source
|
|
58
|
+
label and real transcript statistics through the shared capture pipeline.
|
|
59
|
+
|
|
52
60
|
## OpenCode note
|
|
53
61
|
|
|
54
62
|
OpenCode exposes session lifecycle events through plugins. Codiedev snapshots
|
package/dist/cli.js
CHANGED
|
@@ -29,7 +29,6 @@ const createTicket_1 = require("./commands/createTicket");
|
|
|
29
29
|
const doctor_1 = require("./commands/doctor");
|
|
30
30
|
const search_1 = require("./commands/search");
|
|
31
31
|
const library_1 = require("./commands/library");
|
|
32
|
-
const post_1 = require("./commands/post");
|
|
33
32
|
const share_1 = require("./commands/share");
|
|
34
33
|
const send_1 = require("./commands/send");
|
|
35
34
|
const react_1 = require("./commands/react");
|
|
@@ -44,7 +43,7 @@ Codiedev CLI
|
|
|
44
43
|
someone left off across sessions and providers.
|
|
45
44
|
|
|
46
45
|
Connect:
|
|
47
|
-
codiedev connect Link Claude Code / Codex to your org
|
|
46
|
+
codiedev connect Link Claude Code / Codex / Cursor to your org
|
|
48
47
|
codiedev doctor Verify setup, capture, and Agent Security
|
|
49
48
|
codiedev security pilot Run safe synthetic policy checks
|
|
50
49
|
codiedev security inventory Inventory MCPs, skills, hooks, plugins,
|
|
@@ -105,9 +104,6 @@ Discovery:
|
|
|
105
104
|
List artifacts in your workspace
|
|
106
105
|
|
|
107
106
|
Feed:
|
|
108
|
-
codiedev post --title "<t>" --body "<b>" [--intent share|request_review|
|
|
109
|
-
request_expertise|link_share|rfc] [--filename <key>]
|
|
110
|
-
[--mention <name>]... Publish to the team feed
|
|
111
107
|
codiedev react <postId> <emoji> Toggle a reaction on a feed post
|
|
112
108
|
|
|
113
109
|
Privacy:
|
|
@@ -135,7 +131,7 @@ Other artifact types (rare — use the matching prefix):
|
|
|
135
131
|
|
|
136
132
|
Skills (slash commands):
|
|
137
133
|
Add 'name:' + 'description:' YAML frontmatter to any pushed file
|
|
138
|
-
and it shows up as /<name> in Claude Code / Codex. Filename
|
|
134
|
+
and it shows up as /<name> in Claude Code / Codex / Cursor. Filename
|
|
139
135
|
prefix doesn't matter — frontmatter is the signal.
|
|
140
136
|
|
|
141
137
|
Examples:
|
|
@@ -172,6 +168,8 @@ codiedev connect — link your agent CLI to Codiedev
|
|
|
172
168
|
- ~/.claude/CLAUDE.md instructions (teaches Claude the commands)
|
|
173
169
|
- ~/.codex/hooks.json hook (same, for Codex)
|
|
174
170
|
- ~/.codex/AGENTS.md instructions (same, for Codex)
|
|
171
|
+
- ~/.cursor/hooks.json hooks (session, prompt, security, capture)
|
|
172
|
+
- ~/.cursor/rules/codiedev.mdc (teaches Cursor the commands)
|
|
175
173
|
|
|
176
174
|
Re-run anytime to swap accounts or refresh the instruction block.
|
|
177
175
|
|
|
@@ -374,7 +372,7 @@ Default path:
|
|
|
374
372
|
# body...
|
|
375
373
|
3. codiedev push portal-design.md
|
|
376
374
|
|
|
377
|
-
It's now /portal-design in Claude Code / Codex.
|
|
375
|
+
It's now /portal-design in Claude Code / Codex / Cursor.
|
|
378
376
|
|
|
379
377
|
Filename prefix is irrelevant for skills — frontmatter is the only
|
|
380
378
|
signal. The stored artifact type still follows the filename rule
|
|
@@ -392,7 +390,7 @@ email notifications when pinged.
|
|
|
392
390
|
|
|
393
391
|
## Agent-native
|
|
394
392
|
|
|
395
|
-
If you use Claude Code or
|
|
393
|
+
If you use Claude Code, Codex, or Cursor, the agent already knows the commands —
|
|
396
394
|
'codiedev connect' installs instructions into your user-level config.
|
|
397
395
|
Just say things like:
|
|
398
396
|
|
|
@@ -513,9 +511,6 @@ async function main() {
|
|
|
513
511
|
case "ls":
|
|
514
512
|
await (0, library_1.runLibrary)(rest);
|
|
515
513
|
return;
|
|
516
|
-
case "post":
|
|
517
|
-
await (0, post_1.runPost)(rest);
|
|
518
|
-
return;
|
|
519
514
|
case "share":
|
|
520
515
|
await (0, share_1.runShare)(rest);
|
|
521
516
|
return;
|
package/dist/codieStartup.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface CodieStartupConfig extends AgentSecurityEntitlement {
|
|
|
4
4
|
backendUrl?: string;
|
|
5
5
|
token?: string;
|
|
6
6
|
companyId?: string;
|
|
7
|
+
deviceId?: string;
|
|
7
8
|
}
|
|
8
9
|
export type CodiePolicyRefreshResult = {
|
|
9
10
|
status: "disabled";
|
|
@@ -21,4 +22,5 @@ export declare function refreshCodieSecurityPolicy(config: CodieStartupConfig, s
|
|
|
21
22
|
backendUrl: string;
|
|
22
23
|
token: string;
|
|
23
24
|
companyId: string;
|
|
25
|
+
deviceId?: string;
|
|
24
26
|
}, source?: AgentSurface) => Promise<EndpointSecurityPolicy>): Promise<CodiePolicyRefreshResult>;
|
package/dist/codieStartup.js
CHANGED
|
@@ -17,6 +17,7 @@ async function refreshCodieSecurityPolicy(config, syncPolicy = securityPolicy_1.
|
|
|
17
17
|
backendUrl: config.backendUrl,
|
|
18
18
|
token: config.token,
|
|
19
19
|
companyId: config.companyId,
|
|
20
|
+
...(config.deviceId ? { deviceId: config.deviceId } : {}),
|
|
20
21
|
}, "codie-harness");
|
|
21
22
|
return { status: "synced", policy };
|
|
22
23
|
}
|
package/dist/connect.js
CHANGED
|
@@ -291,6 +291,7 @@ async function runConnect(args = []) {
|
|
|
291
291
|
const securityConfigPaths = [
|
|
292
292
|
path.join(home, ".claude", "settings.json"),
|
|
293
293
|
path.join(home, ".codex", "hooks.json"),
|
|
294
|
+
path.join(home, ".cursor", "hooks.json"),
|
|
294
295
|
path.join(home, ".config", "opencode", "plugins", "codiedev.js"),
|
|
295
296
|
path.join(home, ".codex", "config.toml"),
|
|
296
297
|
];
|
|
@@ -327,7 +328,7 @@ async function runConnect(args = []) {
|
|
|
327
328
|
runInstall("codex-instructions", utils_1.installCodexInstructions, "Codex agent instructions (~/.codex/AGENTS.md)", "codex-instructions");
|
|
328
329
|
}
|
|
329
330
|
if (hasCursor) {
|
|
330
|
-
runInstall("cursor-hook", utils_1.installCursorHook, "Cursor
|
|
331
|
+
runInstall("cursor-hook", () => (0, utils_1.installCursorHook)(agentSecurityEnabled), "Cursor sessionStart + prompt security + stop backup + sessionEnd capture hooks (~/.cursor/hooks.json)", "cursor-hook");
|
|
331
332
|
runInstall("cursor-instructions", utils_1.installCursorInstructions, "Cursor agent instructions (~/.cursor/rules/codiedev.mdc)", "cursor-instructions");
|
|
332
333
|
}
|
|
333
334
|
// Always stage the adapter. It is dormant until OpenCode starts, which lets
|
|
@@ -345,8 +346,10 @@ async function runConnect(args = []) {
|
|
|
345
346
|
targets.push({ surface: "claude-code", configPath: securityConfigPaths[0] });
|
|
346
347
|
if (installedTargets.includes("codex-hook"))
|
|
347
348
|
targets.push({ surface: "codex", configPath: securityConfigPaths[1] });
|
|
349
|
+
if (installedTargets.includes("cursor-hook"))
|
|
350
|
+
targets.push({ surface: "cursor", configPath: securityConfigPaths[2] });
|
|
348
351
|
if (installedTargets.includes("opencode-capture"))
|
|
349
|
-
targets.push({ surface: "codie-harness", configPath: securityConfigPaths[
|
|
352
|
+
targets.push({ surface: "codie-harness", configPath: securityConfigPaths[3] });
|
|
350
353
|
(0, securityInstall_1.writeSecurityInstallManifest)({ hookScriptPath, snapshot: securitySnapshot, targets });
|
|
351
354
|
}
|
|
352
355
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Cursor's native hook payload is close to Claude Code's, but its stable
|
|
2
|
+
* conversation identifier and several tool fields use different names. Keep
|
|
3
|
+
* the translation at the process boundary so the shared capture/security
|
|
4
|
+
* pipeline never has to guess which surface produced an event. */
|
|
5
|
+
export declare function normalizeCursorHookInput<T extends Record<string, unknown>>(input: T): T & Record<string, unknown>;
|
|
6
|
+
/** Cursor stores JSONL message roles in `role`; the shared capture pipeline
|
|
7
|
+
* uses Claude's top-level `type` discriminator. Preserve all original fields
|
|
8
|
+
* and add only that discriminator so the existing parsers see real messages. */
|
|
9
|
+
export declare function normalizeCursorTranscript(content: string): string;
|
|
10
|
+
/** Cursor may change its private JSONL record shape independently of the hook
|
|
11
|
+
* contract. Only a byte-empty snapshot is safe to discard; a non-empty file
|
|
12
|
+
* that the current normalizer cannot parse must still be uploaded for backend
|
|
13
|
+
* recovery and diagnosis. */
|
|
14
|
+
export declare function isCursorTranscriptEmpty(content: string): boolean;
|
|
15
|
+
export declare function cursorPreToolBlockResponse(reason: string): {
|
|
16
|
+
permission: "deny";
|
|
17
|
+
user_message: string;
|
|
18
|
+
agent_message: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function cursorPromptBlockResponse(reason: string): {
|
|
21
|
+
continue: false;
|
|
22
|
+
user_message: string;
|
|
23
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeCursorHookInput = normalizeCursorHookInput;
|
|
4
|
+
exports.normalizeCursorTranscript = normalizeCursorTranscript;
|
|
5
|
+
exports.isCursorTranscriptEmpty = isCursorTranscriptEmpty;
|
|
6
|
+
exports.cursorPreToolBlockResponse = cursorPreToolBlockResponse;
|
|
7
|
+
exports.cursorPromptBlockResponse = cursorPromptBlockResponse;
|
|
8
|
+
/** Cursor's native hook payload is close to Claude Code's, but its stable
|
|
9
|
+
* conversation identifier and several tool fields use different names. Keep
|
|
10
|
+
* the translation at the process boundary so the shared capture/security
|
|
11
|
+
* pipeline never has to guess which surface produced an event. */
|
|
12
|
+
function normalizeCursorHookInput(input) {
|
|
13
|
+
const sessionId = firstString(input.session_id, input.sessionId, input.conversation_id);
|
|
14
|
+
const roots = Array.isArray(input.workspace_roots)
|
|
15
|
+
? input.workspace_roots.filter((root) => typeof root === "string")
|
|
16
|
+
: [];
|
|
17
|
+
const cwd = firstString(input.cwd, roots[0]);
|
|
18
|
+
const duration = firstNumber(input.duration_ms, input.durationMs, input.duration);
|
|
19
|
+
return {
|
|
20
|
+
...input,
|
|
21
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
22
|
+
...(cwd ? { cwd } : {}),
|
|
23
|
+
...(input.tool_response === undefined && input.toolResponse === undefined && input.tool_output !== undefined
|
|
24
|
+
? { tool_response: input.tool_output }
|
|
25
|
+
: {}),
|
|
26
|
+
...(input.error === undefined && input.error_message !== undefined
|
|
27
|
+
? { error: input.error_message }
|
|
28
|
+
: {}),
|
|
29
|
+
...(duration !== undefined ? { duration_ms: duration } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Cursor stores JSONL message roles in `role`; the shared capture pipeline
|
|
33
|
+
* uses Claude's top-level `type` discriminator. Preserve all original fields
|
|
34
|
+
* and add only that discriminator so the existing parsers see real messages. */
|
|
35
|
+
function normalizeCursorTranscript(content) {
|
|
36
|
+
return content
|
|
37
|
+
.split("\n")
|
|
38
|
+
.map((line) => {
|
|
39
|
+
if (!line.trim())
|
|
40
|
+
return line;
|
|
41
|
+
try {
|
|
42
|
+
const entry = JSON.parse(line);
|
|
43
|
+
if ((entry.role === "user" || entry.role === "assistant") &&
|
|
44
|
+
entry.type === undefined) {
|
|
45
|
+
return JSON.stringify({ ...entry, type: entry.role });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Preserve malformed/non-JSON lines; tolerant downstream parsers skip
|
|
50
|
+
// anything they cannot consume.
|
|
51
|
+
}
|
|
52
|
+
return line;
|
|
53
|
+
})
|
|
54
|
+
.join("\n");
|
|
55
|
+
}
|
|
56
|
+
/** Cursor may change its private JSONL record shape independently of the hook
|
|
57
|
+
* contract. Only a byte-empty snapshot is safe to discard; a non-empty file
|
|
58
|
+
* that the current normalizer cannot parse must still be uploaded for backend
|
|
59
|
+
* recovery and diagnosis. */
|
|
60
|
+
function isCursorTranscriptEmpty(content) {
|
|
61
|
+
return content.trim().length === 0;
|
|
62
|
+
}
|
|
63
|
+
function firstString(...values) {
|
|
64
|
+
return values.find((value) => typeof value === "string" && value.trim().length > 0);
|
|
65
|
+
}
|
|
66
|
+
function firstNumber(...values) {
|
|
67
|
+
return values.find((value) => typeof value === "number" && Number.isFinite(value));
|
|
68
|
+
}
|
|
69
|
+
function cursorPreToolBlockResponse(reason) {
|
|
70
|
+
return {
|
|
71
|
+
permission: "deny",
|
|
72
|
+
user_message: reason,
|
|
73
|
+
agent_message: reason,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function cursorPromptBlockResponse(reason) {
|
|
77
|
+
return {
|
|
78
|
+
continue: false,
|
|
79
|
+
user_message: reason,
|
|
80
|
+
};
|
|
81
|
+
}
|
package/dist/hook.js
CHANGED
|
@@ -37,12 +37,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
37
37
|
exports.buildDiscoveryContext = buildDiscoveryContext;
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const os = __importStar(require("os"));
|
|
40
|
-
const path = __importStar(require("path"));
|
|
41
40
|
const https = __importStar(require("https"));
|
|
42
41
|
const http = __importStar(require("http"));
|
|
43
42
|
const child_process_1 = require("child_process");
|
|
44
43
|
const utils_1 = require("./utils");
|
|
45
|
-
const
|
|
44
|
+
const limitEventReporter_1 = require("./limitEventReporter");
|
|
46
45
|
const version_1 = require("./version");
|
|
47
46
|
const captureLog_1 = require("./captureLog");
|
|
48
47
|
const security_1 = require("./security");
|
|
@@ -51,6 +50,7 @@ const securityPolicy_1 = require("./securityPolicy");
|
|
|
51
50
|
const securityFeature_1 = require("./securityFeature");
|
|
52
51
|
const securityInventory_1 = require("./securityInventory");
|
|
53
52
|
const captureExclusions_1 = require("./captureExclusions");
|
|
53
|
+
const cursorHooks_1 = require("./cursorHooks");
|
|
54
54
|
// Pure builder for the discovery additionalContext. Returns null when there are
|
|
55
55
|
// no matches. The suggested `codiedev pull` command carries the EXACT top key
|
|
56
56
|
// (e.g. "pr-push.md") rather than a "<key>" placeholder — otherwise agents
|
|
@@ -98,43 +98,58 @@ async function runDiscover(prompt) {
|
|
|
98
98
|
}
|
|
99
99
|
process.exit(0);
|
|
100
100
|
}
|
|
101
|
+
function readTranscriptTailFile(transcriptPath, maxBytes = 512 * 1024) {
|
|
102
|
+
const size = fs.statSync(transcriptPath).size;
|
|
103
|
+
if (size <= maxBytes)
|
|
104
|
+
return fs.readFileSync(transcriptPath, "utf8");
|
|
105
|
+
const handle = fs.openSync(transcriptPath, "r");
|
|
106
|
+
try {
|
|
107
|
+
const buffer = Buffer.allocUnsafe(maxBytes);
|
|
108
|
+
const bytesRead = fs.readSync(handle, buffer, 0, maxBytes, size - maxBytes);
|
|
109
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
fs.closeSync(handle);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
101
115
|
// StopFailure (matcher rate_limit): the dev just got blocked. Read the local
|
|
102
116
|
// transcript tail for the ground-truth rate_limit line and POST a tiny event.
|
|
103
117
|
// Best-effort + silent — StopFailure output is ignored by Claude Code anyway.
|
|
104
|
-
async function
|
|
118
|
+
async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
|
|
105
119
|
const config = (0, utils_1.readConfig)();
|
|
106
120
|
if (!config)
|
|
107
121
|
process.exit(0);
|
|
108
|
-
let
|
|
109
|
-
let transcriptTail;
|
|
122
|
+
let transcript = "";
|
|
110
123
|
const tp = hookInput.transcript_path;
|
|
111
124
|
if (tp) {
|
|
112
125
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// were working on" right now — no waiting for the SessionEnd capture.
|
|
117
|
-
transcriptTail = (0, limitEvent_1.tailForLimitEvent)(transcript) || undefined;
|
|
126
|
+
// Codex invokes Stop after every turn. Bound this read so long-running
|
|
127
|
+
// sessions do not repeatedly load their entire rollout into memory.
|
|
128
|
+
transcript = readTranscriptTailFile(tp);
|
|
118
129
|
}
|
|
119
130
|
catch {
|
|
120
131
|
/* unreadable transcript — fall through to minimal event */
|
|
121
132
|
}
|
|
122
133
|
}
|
|
123
|
-
// The hook is registered with matcher "rate_limit", so a miss still means a
|
|
124
|
-
// real limit — emit a minimal event rather than dropping it.
|
|
125
|
-
const payload = event ?? { kind: "other", at: Date.now(), resetText: null };
|
|
126
134
|
try {
|
|
127
|
-
await
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
resetText: payload.resetText,
|
|
135
|
+
await (0, limitEventReporter_1.reportTranscriptLimitEvent)({
|
|
136
|
+
config,
|
|
137
|
+
source,
|
|
131
138
|
sessionId: hookInput.session_id,
|
|
132
|
-
|
|
133
|
-
|
|
139
|
+
transcript,
|
|
140
|
+
emitMinimalOnMiss,
|
|
141
|
+
});
|
|
134
142
|
}
|
|
135
143
|
catch {
|
|
136
144
|
/* best-effort */
|
|
137
145
|
}
|
|
146
|
+
}
|
|
147
|
+
async function runStopFailure(hookInput) {
|
|
148
|
+
await readAndPostLimitEvent(hookInput, "claude-code", true);
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
async function runCodexLimit(hookInput) {
|
|
152
|
+
await readAndPostLimitEvent(hookInput, "codex", false);
|
|
138
153
|
process.exit(0);
|
|
139
154
|
}
|
|
140
155
|
// Skip-reason ping: this session is ending WITHOUT a capture, and here's why.
|
|
@@ -294,13 +309,16 @@ async function runSecurityHook(hookInput, phase, source, uploadTelemetry = true)
|
|
|
294
309
|
});
|
|
295
310
|
if (uploadTelemetry)
|
|
296
311
|
dispatchSecurityEventUpload(event);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
312
|
+
const response = source === "cursor"
|
|
313
|
+
? (0, cursorHooks_1.cursorPreToolBlockResponse)(localDecision.reason ?? "Codie blocked this action")
|
|
314
|
+
: {
|
|
315
|
+
hookSpecificOutput: {
|
|
316
|
+
hookEventName: "PreToolUse",
|
|
317
|
+
permissionDecision: "deny",
|
|
318
|
+
permissionDecisionReason: localDecision.reason,
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
process.stdout.write(JSON.stringify(response));
|
|
304
322
|
return;
|
|
305
323
|
}
|
|
306
324
|
if (!event)
|
|
@@ -324,10 +342,10 @@ function runPromptSecurityHook(hookInput, source, uploadTelemetry = true) {
|
|
|
324
342
|
if (uploadTelemetry) {
|
|
325
343
|
dispatchSecurityEventUpload((0, security_1.buildBlockedSecurityPromptEvent)(hookInput, source, decision.category ?? "secret_in_prompt"));
|
|
326
344
|
}
|
|
327
|
-
|
|
328
|
-
decision
|
|
329
|
-
reason: decision.reason
|
|
330
|
-
|
|
345
|
+
const response = source === "cursor"
|
|
346
|
+
? (0, cursorHooks_1.cursorPromptBlockResponse)(decision.reason ?? "Codie blocked this prompt")
|
|
347
|
+
: { decision: "block", reason: decision.reason };
|
|
348
|
+
process.stdout.write(JSON.stringify(response));
|
|
331
349
|
return true;
|
|
332
350
|
}
|
|
333
351
|
function dispatchAllowedPromptSecurityEvent(hookInput, source) {
|
|
@@ -361,10 +379,13 @@ async function runSecurityEventUpload() {
|
|
|
361
379
|
try {
|
|
362
380
|
const raw = await readStdin();
|
|
363
381
|
const event = JSON.parse(raw);
|
|
382
|
+
const cachedPolicy = (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId);
|
|
364
383
|
const eventWithEndpoint = (0, security_1.withEndpointIdentity)(event, {
|
|
365
384
|
endpointId: config.deviceId,
|
|
366
385
|
endpointName: config.deviceName,
|
|
367
386
|
cliVersion: version_1.CLI_VERSION,
|
|
387
|
+
securityRuntimeVersion: security_1.SECURITY_RUNTIME_VERSION,
|
|
388
|
+
securityPolicyVersion: cachedPolicy?.policyVersion,
|
|
368
389
|
});
|
|
369
390
|
const result = await postJsonForResponse(`${config.backendUrl}/api/cli/securityEvent`, { ...eventWithEndpoint }, config.token,
|
|
370
391
|
// Leave room for a cold backend and durable scheduling. This uploader is
|
|
@@ -445,7 +466,7 @@ async function readStdin() {
|
|
|
445
466
|
function resolveMode(arg) {
|
|
446
467
|
if (arg === "capture-codex")
|
|
447
468
|
return "codex";
|
|
448
|
-
if (arg === "capture-cursor")
|
|
469
|
+
if (arg === "capture-cursor" || arg === "backup-cursor")
|
|
449
470
|
return "cursor";
|
|
450
471
|
if (arg === "capture-opencode" || arg === "capture-opencode-final")
|
|
451
472
|
return "opencode";
|
|
@@ -574,7 +595,10 @@ async function main() {
|
|
|
574
595
|
try {
|
|
575
596
|
const raw = await readStdin();
|
|
576
597
|
if (raw.trim()) {
|
|
577
|
-
|
|
598
|
+
const parsed = JSON.parse(raw);
|
|
599
|
+
hookInput = (process.argv[2]?.endsWith("-cursor")
|
|
600
|
+
? (0, cursorHooks_1.normalizeCursorHookInput)(parsed)
|
|
601
|
+
: parsed);
|
|
578
602
|
}
|
|
579
603
|
}
|
|
580
604
|
catch {
|
|
@@ -585,18 +609,28 @@ async function main() {
|
|
|
585
609
|
// disabled; all other hook modes still exit before making a network request.
|
|
586
610
|
const captureIgnored = (0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput);
|
|
587
611
|
const securityCommand = process.argv[2] ?? "";
|
|
588
|
-
const securityMatch = securityCommand.match(/^security-(pre|post|failure)(?:-(codex|codie))?$/);
|
|
612
|
+
const securityMatch = securityCommand.match(/^security-(pre|post|failure)(?:-(codex|cursor|codie))?$/);
|
|
589
613
|
if (securityMatch) {
|
|
590
614
|
const source = securityMatch[2] === "codex"
|
|
591
615
|
? "codex"
|
|
592
|
-
: securityMatch[2] === "
|
|
593
|
-
? "
|
|
594
|
-
: "
|
|
616
|
+
: securityMatch[2] === "cursor"
|
|
617
|
+
? "cursor"
|
|
618
|
+
: securityMatch[2] === "codie"
|
|
619
|
+
? "codie-harness"
|
|
620
|
+
: "claude-code";
|
|
595
621
|
await runSecurityHook(hookInput, securityMatch[1], source, !captureIgnored);
|
|
596
622
|
process.exit(0);
|
|
597
623
|
}
|
|
598
624
|
// UserPromptSubmit discovery path — separate from session capture.
|
|
599
625
|
const promptCommand = process.argv[2] ?? "";
|
|
626
|
+
if (promptCommand === "security-prompt-cursor") {
|
|
627
|
+
if (runPromptSecurityHook(hookInput, "cursor", !captureIgnored))
|
|
628
|
+
return;
|
|
629
|
+
if (captureIgnored)
|
|
630
|
+
return;
|
|
631
|
+
dispatchAllowedPromptSecurityEvent(hookInput, "cursor");
|
|
632
|
+
process.exit(0);
|
|
633
|
+
}
|
|
600
634
|
if (process.argv[2] === "discover" ||
|
|
601
635
|
process.argv[2] === "discover-codex" ||
|
|
602
636
|
process.argv[2] === "discover-codie") {
|
|
@@ -622,8 +656,14 @@ async function main() {
|
|
|
622
656
|
await runStopFailure(hookInput);
|
|
623
657
|
return;
|
|
624
658
|
}
|
|
659
|
+
if (process.argv[2] === "limit-codex") {
|
|
660
|
+
await runCodexLimit(hookInput);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
625
663
|
// SessionStart: post the capture-coverage denominator ping. Tiny + fast.
|
|
626
|
-
if (process.argv[2] === "session-start" ||
|
|
664
|
+
if (process.argv[2] === "session-start" ||
|
|
665
|
+
process.argv[2] === "session-start-cursor" ||
|
|
666
|
+
process.argv[2] === "session-start-opencode") {
|
|
627
667
|
if ((0, securityFeature_1.isAgentSecurityEnabled)((0, utils_1.readConfig)()) && (0, securityInventory_1.securityInventoryDue)()) {
|
|
628
668
|
try {
|
|
629
669
|
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], "security-inventory-upload", hookInput.cwd || process.cwd()], {
|
|
@@ -634,7 +674,11 @@ async function main() {
|
|
|
634
674
|
}
|
|
635
675
|
catch { /* daily inventory is best-effort */ }
|
|
636
676
|
}
|
|
637
|
-
await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
|
|
677
|
+
await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
|
|
678
|
+
? "opencode"
|
|
679
|
+
: process.argv[2] === "session-start-cursor"
|
|
680
|
+
? "cursor"
|
|
681
|
+
: "claude-code");
|
|
638
682
|
return;
|
|
639
683
|
}
|
|
640
684
|
// Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets
|
|
@@ -642,7 +686,14 @@ async function main() {
|
|
|
642
686
|
// no self-heal and could stay stripped forever while SessionStart kept
|
|
643
687
|
// pinging (the Sonifi "starts arrive, captures never" loss) — re-wired on
|
|
644
688
|
// their next Stop/SessionEnd. One read + one write, only when missing.
|
|
645
|
-
(
|
|
689
|
+
if ((process.argv[2] ?? "").endsWith("-cursor")) {
|
|
690
|
+
(0, utils_1.ensureCursorCodiedevHooksInstalled)();
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
(0, utils_1.ensureAllCodiedevHooksInstalled)();
|
|
694
|
+
}
|
|
695
|
+
if (process.argv[2] === "capture-codex")
|
|
696
|
+
(0, utils_1.ensureCodexLimitHookInstalled)();
|
|
646
697
|
const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
|
|
647
698
|
const workspaceRoot = workspace_roots && workspace_roots.length > 0
|
|
648
699
|
? workspace_roots[0]
|
|
@@ -655,13 +706,11 @@ async function main() {
|
|
|
655
706
|
// extension) where SessionEnd may never come — the silent zero-capture bug.
|
|
656
707
|
// Same detached capture-finish child as SessionEnd, marked non-final so the
|
|
657
708
|
// server renews the session lease instead of summarizing immediately.
|
|
658
|
-
if (process.argv[2] === "backup") {
|
|
709
|
+
if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor") {
|
|
659
710
|
if (transcript_path) {
|
|
660
711
|
try {
|
|
661
712
|
(0, utils_1.dispatchCaptureFinish)({
|
|
662
|
-
|
|
663
|
-
// SessionEnd path below (resolveMode maps it to claude-code).
|
|
664
|
-
mode: "capture",
|
|
713
|
+
mode: process.argv[2] === "backup-cursor" ? "capture-cursor" : "capture",
|
|
665
714
|
session_id,
|
|
666
715
|
transcript_path,
|
|
667
716
|
cwd,
|
|
@@ -721,10 +770,9 @@ async function main() {
|
|
|
721
770
|
transcript_path,
|
|
722
771
|
cwd,
|
|
723
772
|
workspace_roots,
|
|
724
|
-
// final:true
|
|
725
|
-
// per-turn Stop hook through "capture-codex" —
|
|
726
|
-
// close the server lease every turn.
|
|
727
|
-
// behaviour (final omitted).
|
|
773
|
+
// final:true for real SessionEnd signals (Claude Code and Cursor).
|
|
774
|
+
// Codex routes its per-turn Stop hook through "capture-codex" —
|
|
775
|
+
// final:true there would close the server lease every turn.
|
|
728
776
|
final: (0, utils_1.resolveCaptureFinal)(process.argv[2]),
|
|
729
777
|
}, process.argv[1], { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile: jobFile, openSync: fs.openSync, closeSync: fs.closeSync });
|
|
730
778
|
}
|
|
@@ -801,7 +849,9 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
801
849
|
outcome: "skip",
|
|
802
850
|
detail: reason,
|
|
803
851
|
});
|
|
804
|
-
|
|
852
|
+
if ((0, utils_1.shouldReportCaptureSkip)(input.final)) {
|
|
853
|
+
await postSessionSkip(config, session_id, reason);
|
|
854
|
+
}
|
|
805
855
|
return;
|
|
806
856
|
}
|
|
807
857
|
// Snapshot the RAW transcript before normalization below flattens it to
|
|
@@ -813,6 +863,9 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
813
863
|
if (parserMode === "opencode") {
|
|
814
864
|
transcriptContent = (0, utils_1.normalizeOpenCodeTranscript)(transcriptContent);
|
|
815
865
|
}
|
|
866
|
+
else if (parserMode === "cursor") {
|
|
867
|
+
transcriptContent = (0, cursorHooks_1.normalizeCursorTranscript)(transcriptContent);
|
|
868
|
+
}
|
|
816
869
|
// Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
|
|
817
870
|
// need the git remote of the workspace.
|
|
818
871
|
let remoteUrl = null;
|
|
@@ -839,9 +892,26 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
839
892
|
const matchedRepo = remoteUrl ? (0, utils_1.matchRepo)(remoteUrl, config.repos) : null;
|
|
840
893
|
const stats = parserMode === "codex"
|
|
841
894
|
? (0, utils_1.parseCodexStats)(transcriptContent)
|
|
842
|
-
:
|
|
843
|
-
|
|
844
|
-
|
|
895
|
+
: (0, utils_1.parseClaudeCodeStats)(transcriptContent);
|
|
896
|
+
// Cursor can emit duplicate lifecycle notifications while one transcript
|
|
897
|
+
// path is still byte-empty. Never use parsed message count here: Cursor's
|
|
898
|
+
// private JSONL shape can evolve independently, and parser drift must not
|
|
899
|
+
// silently discard a real non-empty session.
|
|
900
|
+
if (parserMode === "cursor" && (0, cursorHooks_1.isCursorTranscriptEmpty)(rawTranscriptContent)) {
|
|
901
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
902
|
+
stage: "capture-finish",
|
|
903
|
+
sessionId: session_id,
|
|
904
|
+
outcome: "skip",
|
|
905
|
+
detail: "empty-cursor-transcript",
|
|
906
|
+
});
|
|
907
|
+
// A non-final stop snapshot is only a backup; reporting it as a skipped
|
|
908
|
+
// session would close/remove server-side start coverage while the session
|
|
909
|
+
// is still alive. The real SessionEnd remains responsible for a skip ping.
|
|
910
|
+
if ((0, utils_1.shouldReportCaptureSkip)(input.final)) {
|
|
911
|
+
await postSessionSkip(config, session_id, "empty-cursor-transcript");
|
|
912
|
+
}
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
845
915
|
const transcriptBase64 = Buffer.from(transcriptContent, "utf8").toString("base64");
|
|
846
916
|
// Session→merge join keys: the branch + HEAD sha the work happened on. Matched
|
|
847
917
|
// server-side against a merged MR/PR's source_branch / commits for AI attribution.
|
|
@@ -934,7 +1004,7 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
934
1004
|
// which is the "supersede" mechanism for the deferred path.
|
|
935
1005
|
try {
|
|
936
1006
|
const hookScriptPath = process.argv[1];
|
|
937
|
-
const tmpFile =
|
|
1007
|
+
const tmpFile = (0, utils_1.captureUploadTmpFile)(session_id, decision.mode);
|
|
938
1008
|
if (decision.mode === "immediate") {
|
|
939
1009
|
(0, utils_1.dispatchCaptureDetached)({
|
|
940
1010
|
url: `${config.backendUrl}/api/captureSession`,
|
|
@@ -942,6 +1012,11 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
942
1012
|
body: payload,
|
|
943
1013
|
scopePaths,
|
|
944
1014
|
}, hookScriptPath, { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile, openSync: fs.openSync, closeSync: fs.closeSync });
|
|
1015
|
+
// The immediate payload is durable before removing the older scheduled
|
|
1016
|
+
// snapshot. Do this for every immediate dispatch, not only SessionEnd:
|
|
1017
|
+
// first/outside-window snapshots also supersede a sleeping deferred job.
|
|
1018
|
+
if (session_id)
|
|
1019
|
+
(0, utils_1.cancelDeferredCaptureUpload)(session_id);
|
|
945
1020
|
// Retry any earlier capture jobs that failed to upload (transient network
|
|
946
1021
|
// blips), excluding the one we just dispatched. Best-effort, detached.
|
|
947
1022
|
// Skip the sweep on deferred path — the deferred child will handle uploading.
|
package/dist/limitEvent.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
export type LimitKind = "session" | "org_spend" | "other";
|
|
1
|
+
export type LimitKind = "session" | "user_spend" | "org_spend" | "other";
|
|
2
2
|
export interface LimitEvent {
|
|
3
3
|
kind: LimitKind;
|
|
4
4
|
at: number;
|
|
5
5
|
resetText: string | null;
|
|
6
|
+
reason?: string;
|
|
6
7
|
}
|
|
7
8
|
export declare function classifyLimitKind(text: string): LimitKind;
|
|
8
9
|
export declare function parseLimitEventFromTranscript(transcript: string): LimitEvent | null;
|
|
10
|
+
/**
|
|
11
|
+
* Codex writes a structured RateLimitSnapshot on token_count events. A normal
|
|
12
|
+
* turn has `rate_limit_reached_type: null`; a blocked turn names the exact
|
|
13
|
+
* boundary. Reading that field avoids locale-dependent terminal copy.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseCodexLimitEventFromTranscript(transcript: string): LimitEvent | null;
|
|
9
16
|
export declare function tailForLimitEvent(transcript: string, maxChars?: number): string;
|