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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.classifyLimitKind = classifyLimitKind;
4
4
  exports.parseLimitEventFromTranscript = parseLimitEventFromTranscript;
5
+ exports.parseCodexLimitEventFromTranscript = parseCodexLimitEventFromTranscript;
5
6
  exports.tailForLimitEvent = tailForLimitEvent;
6
7
  function classifyLimitKind(text) {
7
8
  const t = text.toLowerCase();
@@ -50,6 +51,63 @@ function parseLimitEventFromTranscript(transcript) {
50
51
  }
51
52
  return found;
52
53
  }
54
+ function codexLimitKind(reason, spendControlReached) {
55
+ if (spendControlReached || reason.startsWith("workspace_owner_"))
56
+ return "org_spend";
57
+ if (reason.startsWith("workspace_member_"))
58
+ return "user_spend";
59
+ return reason === "rate_limit_reached" ? "session" : "other";
60
+ }
61
+ function codexResetText(rateLimits) {
62
+ const windows = [rateLimits?.primary, rateLimits?.secondary]
63
+ .filter((window) => window && typeof window === "object");
64
+ const reached = windows.filter((window) => Number(window.used_percent) >= 100);
65
+ const candidates = reached.length > 0 ? reached : windows;
66
+ const resetAt = candidates
67
+ .map((window) => Number(window.resets_at))
68
+ .filter((value) => Number.isFinite(value) && value > 0)
69
+ .sort((a, b) => a - b)[0];
70
+ return resetAt ? new Date(resetAt * 1000).toISOString() : null;
71
+ }
72
+ /**
73
+ * Codex writes a structured RateLimitSnapshot on token_count events. A normal
74
+ * turn has `rate_limit_reached_type: null`; a blocked turn names the exact
75
+ * boundary. Reading that field avoids locale-dependent terminal copy.
76
+ */
77
+ function parseCodexLimitEventFromTranscript(transcript) {
78
+ let found = null;
79
+ for (const line of transcript.split("\n")) {
80
+ if (!line.includes('"rate_limit_reached_type"') && !line.includes('"spend_control_reached"'))
81
+ continue;
82
+ let entry;
83
+ try {
84
+ entry = JSON.parse(line);
85
+ }
86
+ catch {
87
+ continue;
88
+ }
89
+ if (entry?.type !== "event_msg" || entry?.payload?.type !== "token_count")
90
+ continue;
91
+ const rateLimits = entry.payload.rate_limits;
92
+ if (!rateLimits || typeof rateLimits !== "object")
93
+ continue;
94
+ const reachedType = typeof rateLimits.rate_limit_reached_type === "string"
95
+ ? rateLimits.rate_limit_reached_type
96
+ : null;
97
+ const spendControlReached = rateLimits.spend_control_reached === true;
98
+ if (!reachedType && !spendControlReached)
99
+ continue;
100
+ const reason = reachedType ?? "spend_control_reached";
101
+ const at = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : NaN;
102
+ found = {
103
+ kind: codexLimitKind(reason, spendControlReached),
104
+ at: Number.isNaN(at) ? Date.now() : at,
105
+ resetText: codexResetText(rateLimits),
106
+ reason,
107
+ };
108
+ }
109
+ return found;
110
+ }
53
111
  // Tail of the raw transcript to ship with the limit event so the server can
54
112
  // summarize "what they were working on" immediately — without waiting for the
55
113
  // SessionEnd capture (which can lag hours, or drop entirely on Windows).
@@ -0,0 +1,21 @@
1
+ import { type LimitEvent } from "./limitEvent";
2
+ export type LimitEventSource = "claude-code" | "codex";
3
+ export type LimitEventPayload = LimitEvent & {
4
+ source: LimitEventSource;
5
+ sessionId?: string;
6
+ transcriptTail?: string;
7
+ };
8
+ export declare function postLimitEvent(config: {
9
+ backendUrl: string;
10
+ token: string;
11
+ }, payload: LimitEventPayload, timeoutMs?: number): Promise<void>;
12
+ export declare function reportTranscriptLimitEvent(args: {
13
+ config: {
14
+ backendUrl: string;
15
+ token: string;
16
+ };
17
+ source: LimitEventSource;
18
+ sessionId?: string;
19
+ transcript: string;
20
+ emitMinimalOnMiss?: boolean;
21
+ }): Promise<boolean>;
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.postLimitEvent = postLimitEvent;
37
+ exports.reportTranscriptLimitEvent = reportTranscriptLimitEvent;
38
+ const http = __importStar(require("node:http"));
39
+ const https = __importStar(require("node:https"));
40
+ const version_1 = require("./version");
41
+ const limitEvent_1 = require("./limitEvent");
42
+ function postLimitEvent(config, payload, timeoutMs = 5_000) {
43
+ return new Promise((resolve, reject) => {
44
+ const target = new URL("/api/cli/limitEvent", config.backendUrl);
45
+ const data = JSON.stringify(payload);
46
+ const transport = target.protocol === "https:" ? https : http;
47
+ const request = transport.request({
48
+ hostname: target.hostname,
49
+ port: target.port || (target.protocol === "https:" ? 443 : 80),
50
+ path: target.pathname + target.search,
51
+ method: "POST",
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ "Content-Length": Buffer.byteLength(data),
55
+ Authorization: `Bearer ${config.token}`,
56
+ "X-Codiedev-Cli-Version": version_1.CLI_VERSION,
57
+ },
58
+ }, (response) => {
59
+ let responseBody = "";
60
+ response.on("data", (chunk) => { responseBody += chunk; });
61
+ response.on("end", () => {
62
+ if (response.statusCode && response.statusCode >= 200 && response.statusCode < 300) {
63
+ resolve();
64
+ }
65
+ else {
66
+ reject(new Error(`HTTP ${response.statusCode}: ${responseBody}`));
67
+ }
68
+ });
69
+ });
70
+ request.on("error", reject);
71
+ request.setTimeout(timeoutMs, () => request.destroy(new Error(`timeout after ${timeoutMs}ms`)));
72
+ request.write(data);
73
+ request.end();
74
+ });
75
+ }
76
+ async function reportTranscriptLimitEvent(args) {
77
+ const parse = args.source === "codex"
78
+ ? limitEvent_1.parseCodexLimitEventFromTranscript
79
+ : limitEvent_1.parseLimitEventFromTranscript;
80
+ const parsed = parse(args.transcript);
81
+ if (!parsed && !args.emitMinimalOnMiss)
82
+ return false;
83
+ const event = parsed ?? { kind: "other", at: Date.now(), resetText: null };
84
+ await postLimitEvent(args.config, {
85
+ source: args.source,
86
+ ...event,
87
+ sessionId: args.sessionId,
88
+ transcriptTail: args.transcript ? (0, limitEvent_1.tailForLimitEvent)(args.transcript) : undefined,
89
+ });
90
+ return true;
91
+ }
package/dist/mcp.js CHANGED
@@ -260,68 +260,6 @@ const TOOLS = [
260
260
  required: ["artifactId"],
261
261
  },
262
262
  },
263
- {
264
- name: "codiedev_post_to_feed",
265
- description: "Publish an artifact to the team's break-room feed with an intent " +
266
- "and optional @mentions. Use when the user asks to 'post this to " +
267
- "the team', 'publish to the feed', 'share with the team', 'ask the " +
268
- "team', or when they want broader visibility than a single ping. " +
269
- "Pass the artifact's filename (e.g., 'spec-214.md') and pick an " +
270
- "intent that matches the request: share = here's a skill/spec; " +
271
- "request_review = please review; request_expertise = I'm stuck, " +
272
- "who knows this; link_share = fyi; rfc = proposing a change. " +
273
- "Optionally @mention teammates (by first name) so they get a " +
274
- "notification. Returns the feed post id.",
275
- inputSchema: {
276
- type: "object",
277
- properties: {
278
- filename: {
279
- type: "string",
280
- description: "Artifact filename to attach (e.g., 'spec-214.md'). The post " +
281
- "will render a preview card linking to it.",
282
- },
283
- title: {
284
- type: "string",
285
- description: "Headline of the post. Short, imperative — e.g., 'Check my " +
286
- "OAuth middleware spec' or 'Anyone know how we handle X?'.",
287
- },
288
- body: {
289
- type: "string",
290
- description: "Body of the post in markdown. A short explanation of why the " +
291
- "user is posting (the artifact carries the detail).",
292
- },
293
- intent: {
294
- type: "string",
295
- enum: [
296
- "share",
297
- "request_review",
298
- "request_expertise",
299
- "link_share",
300
- "rfc",
301
- ],
302
- description: "Why the user is posting. See tool description.",
303
- },
304
- mentions: {
305
- type: "array",
306
- items: { type: "string" },
307
- description: "Teammate first names (or handles) to @mention. Each mention " +
308
- "creates a notification for that teammate.",
309
- },
310
- format: {
311
- type: "string",
312
- enum: ["article", "quick", "workflow", "review"],
313
- description: "Post format. Default 'article' if omitted. Use 'quick' for " +
314
- "short one-liners.",
315
- },
316
- tags: {
317
- type: "array",
318
- items: { type: "string" },
319
- description: "Optional tags, lowercase, hyphen-safe.",
320
- },
321
- },
322
- required: ["title", "body"],
323
- },
324
- },
325
263
  {
326
264
  name: "codiedev_share_with",
327
265
  description: "Grant a teammate persistent read (or edit) access to an artifact " +
@@ -374,7 +312,7 @@ const TOOLS = [
374
312
  "to that post with X', 'mark as used', or 'upvote'. The 🛠 " +
375
313
  "reaction is the signal for 'I used this' and bumps the linked " +
376
314
  "artifact's reuse count. Pass the post id (get it from " +
377
- "codiedev_get_library or a previous post-to-feed response).",
315
+ "codiedev_get_library or a previous feed response).",
378
316
  inputSchema: {
379
317
  type: "object",
380
318
  properties: {
@@ -602,8 +540,6 @@ async function dispatchTool(name, args, config) {
602
540
  return await handleNote(args, config);
603
541
  case "codiedev_promote":
604
542
  return await handlePromote(args, config);
605
- case "codiedev_post_to_feed":
606
- return await handlePostToFeed(args, config);
607
543
  case "codiedev_share_with":
608
544
  return await handleShareWith(args, config);
609
545
  case "codiedev_send_to":
@@ -784,34 +720,6 @@ async function handlePromote(args, config) {
784
720
  ],
785
721
  };
786
722
  }
787
- async function handlePostToFeed(args, config) {
788
- const filename = asStringOrUndefined(args.filename);
789
- const title = asString(args.title);
790
- const body = asString(args.body);
791
- const intent = asStringOrUndefined(args.intent);
792
- const format = asStringOrUndefined(args.format);
793
- const mentionsRaw = Array.isArray(args.mentions) ? args.mentions : [];
794
- const mentions = mentionsRaw.filter((m) => typeof m === "string");
795
- const tagsRaw = Array.isArray(args.tags) ? args.tags : [];
796
- const tags = tagsRaw.filter((t) => typeof t === "string");
797
- if (!title)
798
- throw new Error("title required");
799
- if (!body)
800
- throw new Error("body required");
801
- const res = await (0, shared_1.apiRequest)("POST", "/api/cli/post-to-feed", {
802
- config,
803
- body: { filename, title, body, intent, format, mentions, tags },
804
- });
805
- const pieces = [`✓ Posted to feed (id=${res.postId}).`];
806
- if (res.linkedArtifact?.key)
807
- pieces.push(` attached: ${res.linkedArtifact.key}`);
808
- if (res.intent)
809
- pieces.push(` intent: ${res.intent}`);
810
- if (res.mentions?.length) {
811
- pieces.push(` mentioned: ${res.mentions.map((m) => m.name).join(", ")}`);
812
- }
813
- return { content: [{ type: "text", text: pieces.join("\n") }] };
814
- }
815
723
  async function handleShareWith(args, config) {
816
724
  const filename = asString(args.filename);
817
725
  const to = asString(args.to);
@@ -23,7 +23,7 @@ function parseRemoteUrl(url) {
23
23
  // Harness checkout loses its repoId even though transcript capture can
24
24
  // resolve the same raw remote correctly. Check this after URL forms so the
25
25
  // colon in `https://` is never mistaken for an SCP separator.
26
- const scp = s.match(/^(?:[^@\s]+@)?([^:/\s]+):(.+)$/);
26
+ const scp = s.match(/^(?:[^@\s]+@)?([^:/\s]+):([^/]+\/.+)$/);
27
27
  if (scp) {
28
28
  return { host: scp[1].toLowerCase(), ownerName: scp[2].toLowerCase() };
29
29
  }
@@ -1,5 +1,6 @@
1
1
  export type SecurityHookPhase = "pre" | "post" | "failure";
2
- export type AgentSurface = "claude-code" | "codex" | "codie-harness";
2
+ export type AgentSurface = "claude-code" | "codex" | "cursor" | "codie-harness";
3
+ export declare const SECURITY_RUNTIME_VERSION = 1;
3
4
  export interface RawSecurityHookInput {
4
5
  session_id?: string;
5
6
  sessionId?: string;
@@ -36,6 +37,8 @@ export interface SecurityWorkflowEvent {
36
37
  endpointId?: string;
37
38
  endpointName?: string;
38
39
  cliVersion?: string;
40
+ securityRuntimeVersion?: number;
41
+ securityPolicyVersion?: number;
39
42
  }
40
43
  export type LocalSecurityAction = "allow" | "alert" | "block";
41
44
  export type SecurityCategory = "secret_in_prompt" | "secret_exfiltration" | "destructive_outside_workspace" | "sensitive_file_read" | "sensitive_file_write" | "protected_agent_config_write" | "unapproved_network_egress" | "untrusted_mcp_tool" | "unapproved_command" | "obfuscated_shell_command" | "package_install" | "sensitive_read_then_egress";
@@ -70,6 +73,8 @@ export declare function withEndpointIdentity(event: SecurityWorkflowEvent, ident
70
73
  endpointId?: string;
71
74
  endpointName?: string;
72
75
  cliVersion?: string;
76
+ securityRuntimeVersion?: number;
77
+ securityPolicyVersion?: number;
73
78
  }): SecurityWorkflowEvent;
74
79
  /**
75
80
  * Runs before a user prompt reaches the model. This is intentionally narrow:
package/dist/security.js CHANGED
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SECURITY_RUNTIME_VERSION = void 0;
36
37
  exports.withEndpointIdentity = withEndpointIdentity;
37
38
  exports.evaluatePromptSecurity = evaluatePromptSecurity;
38
39
  exports.evaluateToolSecurity = evaluateToolSecurity;
@@ -41,12 +42,26 @@ exports.buildSecurityWorkflowEvent = buildSecurityWorkflowEvent;
41
42
  exports.buildSecurityPromptEvent = buildSecurityPromptEvent;
42
43
  exports.buildBlockedSecurityPromptEvent = buildBlockedSecurityPromptEvent;
43
44
  const crypto = __importStar(require("crypto"));
45
+ // Advances only when endpoint enforcement behavior changes. This is
46
+ // intentionally independent from the package version so security coverage can
47
+ // remain current across unrelated CLI releases.
48
+ exports.SECURITY_RUNTIME_VERSION = 1;
44
49
  function withEndpointIdentity(event, identity) {
45
50
  return {
46
51
  ...event,
47
52
  ...(identity.endpointId ? { endpointId: identity.endpointId.slice(0, 240) } : {}),
48
53
  ...(identity.endpointName ? { endpointName: identity.endpointName.slice(0, 240) } : {}),
49
54
  ...(identity.cliVersion ? { cliVersion: identity.cliVersion.slice(0, 80) } : {}),
55
+ ...(typeof identity.securityRuntimeVersion === "number"
56
+ && Number.isInteger(identity.securityRuntimeVersion)
57
+ && identity.securityRuntimeVersion > 0
58
+ ? { securityRuntimeVersion: identity.securityRuntimeVersion }
59
+ : {}),
60
+ ...(typeof identity.securityPolicyVersion === "number"
61
+ && Number.isInteger(identity.securityPolicyVersion)
62
+ && identity.securityPolicyVersion > 0
63
+ ? { securityPolicyVersion: identity.securityPolicyVersion }
64
+ : {}),
50
65
  };
51
66
  }
52
67
  function scopeMatches(value, candidate) {
@@ -1,4 +1,4 @@
1
- export type SecuritySurface = "claude-code" | "codex" | "codie-harness";
1
+ export type SecuritySurface = "claude-code" | "codex" | "cursor" | "codie-harness";
2
2
  export interface SecurityInstallTarget {
3
3
  surface: SecuritySurface;
4
4
  configPath: string;
@@ -58,5 +58,6 @@ export declare function inspectSecurityInstallation(paths?: {
58
58
  claudeSettingsPath?: string;
59
59
  codexHooksPath?: string;
60
60
  codexConfigPath?: string;
61
+ cursorHooksPath?: string;
61
62
  codiePluginPath?: string;
62
63
  }): SecurityInstallInspection[];
@@ -153,12 +153,16 @@ function restoreSecurityInstallSnapshot(manifest) {
153
153
  }
154
154
  return { restored, snapshotId: manifest.rollbackSnapshotId };
155
155
  }
156
- function hookCommands(parsed, eventName) {
156
+ function hookCommands(parsed, eventName, allowFlat) {
157
157
  const entries = parsed?.hooks?.[eventName];
158
158
  if (!Array.isArray(entries))
159
159
  return [];
160
160
  const commands = [];
161
161
  for (const entry of entries) {
162
+ const flatCommand = entry.command;
163
+ if (allowFlat && typeof flatCommand === "string" && (0, detection_1.isCodiedevHookCommand)(flatCommand)) {
164
+ commands.push(flatCommand);
165
+ }
162
166
  const nested = entry?.hooks;
163
167
  if (!Array.isArray(nested))
164
168
  continue;
@@ -176,17 +180,23 @@ function inspectJsonSurface(surface, filePath, codexConfigPath) {
176
180
  }
177
181
  try {
178
182
  const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
179
- const suffix = surface === "codex" ? "-codex" : "";
180
- const expected = [
181
- ["UserPromptSubmit", `discover${suffix}`],
182
- ["PreToolUse", `security-pre${suffix}`],
183
- ["PostToolUse", `security-post${suffix}`],
184
- ...(surface === "claude-code"
185
- ? [["PostToolUseFailure", `security-failure${suffix}`]]
186
- : []),
187
- ];
183
+ const expected = surface === "cursor"
184
+ ? [
185
+ ["beforeSubmitPrompt", "security-prompt-cursor"],
186
+ ["preToolUse", "security-pre-cursor"],
187
+ ["postToolUse", "security-post-cursor"],
188
+ ["postToolUseFailure", "security-failure-cursor"],
189
+ ]
190
+ : [
191
+ ["UserPromptSubmit", surface === "codex" ? "discover-codex" : "discover"],
192
+ ["PreToolUse", surface === "codex" ? "security-pre-codex" : "security-pre"],
193
+ ["PostToolUse", surface === "codex" ? "security-post-codex" : "security-post"],
194
+ ...(surface === "claude-code"
195
+ ? [["PostToolUseFailure", "security-failure"]]
196
+ : []),
197
+ ];
188
198
  const missing = expected
189
- .filter(([eventName, subcommand]) => !hookCommands(parsed, eventName).some((command) => command.trim().endsWith(subcommand)))
199
+ .filter(([eventName, subcommand]) => !hookCommands(parsed, eventName, surface === "cursor").some((command) => command.trim().endsWith(subcommand)))
190
200
  .map(([eventName]) => eventName);
191
201
  if (missing.length > 0) {
192
202
  return { surface, status: "fail", detail: `missing security hooks: ${missing.join(", ")}` };
@@ -207,10 +217,12 @@ function inspectSecurityInstallation(paths = {}) {
207
217
  const home = os.homedir();
208
218
  const claude = paths.claudeSettingsPath ?? path.join(home, ".claude", "settings.json");
209
219
  const codex = paths.codexHooksPath ?? path.join(home, ".codex", "hooks.json");
220
+ const cursor = paths.cursorHooksPath ?? path.join(home, ".cursor", "hooks.json");
210
221
  const codie = paths.codiePluginPath ?? path.join(home, ".config", "opencode", "plugins", "codiedev.js");
211
222
  const results = [
212
223
  inspectJsonSurface("claude-code", claude),
213
224
  inspectJsonSurface("codex", codex, paths.codexConfigPath),
225
+ inspectJsonSurface("cursor", cursor),
214
226
  ];
215
227
  if (!fs.existsSync(codie)) {
216
228
  results.push({ surface: "codie-harness", status: "not-installed", detail: `${codie} not found` });
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runSyntheticSecurityPilot = runSyntheticSecurityPilot;
4
4
  const security_1 = require("./security");
5
- const SURFACES = ["claude-code", "codex", "codie-harness"];
5
+ const SURFACES = ["claude-code", "codex", "cursor", "codie-harness"];
6
6
  function toolInput(surface, cwd, commandOrPath) {
7
7
  const { toolName, ...toolInputValue } = commandOrPath;
8
8
  const resolvedToolName = toolName ?? (commandOrPath.file_path ? "Read" : "Bash");
@@ -1,4 +1,4 @@
1
- import type { AgentSurface, EndpointSecurityPolicy } from "./security";
1
+ import { type AgentSurface, type EndpointSecurityPolicy } from "./security";
2
2
  export type TrustedPolicyKeys = Record<string, string>;
3
3
  /**
4
4
  * Vendor policy roots are compiled into the endpoint binary. This prevents a
@@ -29,4 +29,5 @@ export declare function syncEndpointSecurityPolicy(config: {
29
29
  backendUrl: string;
30
30
  token: string;
31
31
  companyId: string;
32
+ deviceId?: string;
32
33
  }, source?: AgentSurface, timeoutMs?: number, stateDir?: string, trustedKeys?: TrustedPolicyKeys): Promise<EndpointSecurityPolicy>;
@@ -46,6 +46,8 @@ const http = __importStar(require("http"));
46
46
  const https = __importStar(require("https"));
47
47
  const path = __importStar(require("path"));
48
48
  const configDir_1 = require("./configDir");
49
+ const security_1 = require("./security");
50
+ const version_1 = require("./version");
49
51
  const POLICY_FILE = "security-policy.json";
50
52
  function policyFile(companyId) {
51
53
  if (!companyId)
@@ -150,6 +152,13 @@ async function syncEndpointSecurityPolicy(config, source, timeoutMs = 3_000, sta
150
152
  const url = new URL("/api/cli/securityPolicy", config.backendUrl);
151
153
  if (source)
152
154
  url.searchParams.set("source", source);
155
+ const cachedPolicy = readEndpointSecurityPolicy(stateDir, trustedKeys, config.companyId);
156
+ url.searchParams.set("securityRuntimeVersion", String(security_1.SECURITY_RUNTIME_VERSION));
157
+ url.searchParams.set("cliVersion", version_1.CLI_VERSION);
158
+ if (cachedPolicy?.policyVersion)
159
+ url.searchParams.set("securityPolicyVersion", String(cachedPolicy.policyVersion));
160
+ if (config.deviceId)
161
+ url.searchParams.set("endpointId", config.deviceId);
153
162
  const transport = url.protocol === "https:" ? https : http;
154
163
  const policy = await new Promise((resolve, reject) => {
155
164
  const request = transport.request(url, {
package/dist/utils.d.ts CHANGED
@@ -104,7 +104,7 @@ export declare function installOpenCodeCapturePlugin(agentSecurityEnabled?: bool
104
104
  * the presence of any `github.copilot*` directory as the Copilot fingerprint.
105
105
  */
106
106
  export declare function vscodeCopilotInstalled(): boolean;
107
- export declare function upsertCodiedevHook(hooks: Record<string, unknown>, eventName: string, subcommand: string, matcher?: string): void;
107
+ export declare function upsertCodiedevHook(hooks: Record<string, unknown>, eventName: string, subcommand: string, matcher?: string | null): void;
108
108
  /** Remove Codiedev's entries for one event while preserving foreign hooks. */
109
109
  export declare function removeCodiedevHook(hooks: Record<string, unknown>, eventName: string): boolean;
110
110
  export declare function installHook(agentSecurityEnabled?: boolean): void;
@@ -170,7 +170,12 @@ export declare function cleanupCodexMcp(): void;
170
170
  */
171
171
  export declare function installCodexMcp(): boolean;
172
172
  export declare function installCodexHook(agentSecurityEnabled?: boolean): void;
173
- export declare function installCursorHook(): void;
173
+ export declare function ensureCodexLimitHookInstalled(): void;
174
+ export declare function installCursorHook(agentSecurityEnabled?: boolean): void;
175
+ /** Repair Cursor's complete flat hook set after upgrades or configuration
176
+ * drift. This is deliberately separate from Claude Code's nested-hook healer:
177
+ * the schemas and event names differ, and mixing them risks corrupting both. */
178
+ export declare function ensureCursorCodiedevHooksInstalled(agentSecurityEnabled?: boolean): void;
174
179
  export interface ParsedStats {
175
180
  messageCount: number;
176
181
  toolCallCount: number;
@@ -258,6 +263,8 @@ export declare const CAPTURE_THROTTLE_MS: number;
258
263
  export declare const SWEEP_MIN_AGE_MS: number;
259
264
  export declare function resolveCaptureFinal(argv2: string | undefined): boolean | undefined;
260
265
  export declare function finishInputTmpFile(sessionId: string | undefined): string;
266
+ export declare function captureUploadTmpFile(sessionId: string | undefined, mode: "immediate" | "deferred", pid?: number, now?: number): string;
267
+ export declare function cancelDeferredCaptureUpload(sessionId: string, deps?: Pick<typeof fs, "unlinkSync">): boolean;
261
268
  export declare function decideDispatch(args: {
262
269
  final: boolean;
263
270
  lastUploadAt: number | null;
@@ -332,3 +339,4 @@ interface DeferredUploadDeps extends UploadDeps {
332
339
  }
333
340
  export declare function runDeferredUpload(jobFile: string, delayMs: number, deps: DeferredUploadDeps): Promise<void>;
334
341
  export declare function transcriptReadSkipReason(err: unknown): string;
342
+ export declare function shouldReportCaptureSkip(final: boolean | undefined): boolean;