codiedev 0.9.51 → 0.9.53

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.
Files changed (50) hide show
  1. package/README.md +8 -1
  2. package/dist/captureLog.d.ts +1 -1
  3. package/dist/cli.js +7 -22
  4. package/dist/codie.js +1 -146
  5. package/dist/codieLauncher.d.ts +0 -5
  6. package/dist/codieLauncher.js +1 -22
  7. package/dist/codieRuntime.d.ts +1 -11
  8. package/dist/codieRuntime.js +3 -20
  9. package/dist/codieRuntimeInstall.d.ts +11 -12
  10. package/dist/codieRuntimeInstall.js +1 -3
  11. package/dist/commands/doctor.js +0 -8
  12. package/dist/connect.js +4 -53
  13. package/dist/cursorHooks.d.ts +9 -0
  14. package/dist/cursorHooks.js +56 -0
  15. package/dist/hook.js +95 -227
  16. package/dist/limitEvent.d.ts +4 -1
  17. package/dist/limitEvent.js +54 -0
  18. package/dist/limitEventReporter.d.ts +21 -0
  19. package/dist/limitEventReporter.js +91 -0
  20. package/dist/mcp.js +1 -93
  21. package/dist/repoResolver.js +1 -1
  22. package/dist/upgradeNudge.d.ts +0 -1
  23. package/dist/upgradeNudge.js +0 -1
  24. package/dist/utils.d.ts +10 -8
  25. package/dist/utils.js +174 -199
  26. package/package.json +5 -11
  27. package/dist/codieStartup.d.ts +0 -24
  28. package/dist/codieStartup.js +0 -29
  29. package/dist/commands/post.d.ts +0 -2
  30. package/dist/commands/post.js +0 -96
  31. package/dist/commands/security.d.ts +0 -1
  32. package/dist/commands/security.js +0 -156
  33. package/dist/security.d.ts +0 -97
  34. package/dist/security.js +0 -422
  35. package/dist/securityDeepScan.d.ts +0 -41
  36. package/dist/securityDeepScan.js +0 -248
  37. package/dist/securityDoctor.d.ts +0 -32
  38. package/dist/securityDoctor.js +0 -63
  39. package/dist/securityFeature.d.ts +0 -5
  40. package/dist/securityFeature.js +0 -7
  41. package/dist/securityInstall.d.ts +0 -62
  42. package/dist/securityInstall.js +0 -227
  43. package/dist/securityInventory.d.ts +0 -54
  44. package/dist/securityInventory.js +0 -556
  45. package/dist/securityPilot.d.ts +0 -19
  46. package/dist/securityPilot.js +0 -61
  47. package/dist/securityPolicy.d.ts +0 -32
  48. package/dist/securityPolicy.js +0 -179
  49. package/dist/securitySession.d.ts +0 -6
  50. package/dist/securitySession.js +0 -72
package/dist/utils.js CHANGED
@@ -64,7 +64,6 @@ exports.openCodeInstalled = openCodeInstalled;
64
64
  exports.installOpenCodeCapturePlugin = installOpenCodeCapturePlugin;
65
65
  exports.vscodeCopilotInstalled = vscodeCopilotInstalled;
66
66
  exports.upsertCodiedevHook = upsertCodiedevHook;
67
- exports.removeCodiedevHook = removeCodiedevHook;
68
67
  exports.installHook = installHook;
69
68
  exports.ensureDiscoveryHookInstalled = ensureDiscoveryHookInstalled;
70
69
  exports.ensureBackupHookInstalled = ensureBackupHookInstalled;
@@ -83,7 +82,9 @@ exports.cleanupVSCodeMcp = cleanupVSCodeMcp;
83
82
  exports.cleanupCodexMcp = cleanupCodexMcp;
84
83
  exports.installCodexMcp = installCodexMcp;
85
84
  exports.installCodexHook = installCodexHook;
85
+ exports.ensureCodexLimitHookInstalled = ensureCodexLimitHookInstalled;
86
86
  exports.installCursorHook = installCursorHook;
87
+ exports.ensureCursorCodiedevHooksInstalled = ensureCursorCodiedevHooksInstalled;
87
88
  exports.parseClaudeSteering = parseClaudeSteering;
88
89
  exports.parseCodexSteering = parseCodexSteering;
89
90
  exports.parseSteering = parseSteering;
@@ -99,6 +100,8 @@ exports.dispatchDeferredUpload = dispatchDeferredUpload;
99
100
  exports.sweepPendingCaptures = sweepPendingCaptures;
100
101
  exports.resolveCaptureFinal = resolveCaptureFinal;
101
102
  exports.finishInputTmpFile = finishInputTmpFile;
103
+ exports.captureUploadTmpFile = captureUploadTmpFile;
104
+ exports.cancelDeferredCaptureUpload = cancelDeferredCaptureUpload;
102
105
  exports.decideDispatch = decideDispatch;
103
106
  exports.readSessionUploadState = readSessionUploadState;
104
107
  exports.writeSessionUploadState = writeSessionUploadState;
@@ -109,6 +112,7 @@ exports.readLastCapture = readLastCapture;
109
112
  exports.runCaptureUpload = runCaptureUpload;
110
113
  exports.runDeferredUpload = runDeferredUpload;
111
114
  exports.transcriptReadSkipReason = transcriptReadSkipReason;
115
+ exports.shouldReportCaptureSkip = shouldReportCaptureSkip;
112
116
  const fs = __importStar(require("fs"));
113
117
  const path = __importStar(require("path"));
114
118
  const os = __importStar(require("os"));
@@ -852,7 +856,7 @@ function buildCapturePayload(input) {
852
856
  sessionStartedAt: input.sessionStartedAt ?? undefined,
853
857
  interruptCount: input.interruptCount,
854
858
  rejectionCount: input.rejectionCount,
855
- // final is only emitted when explicitly set (not for Codex/Cursor).
859
+ // final is only emitted when explicitly set (not for Codex).
856
860
  final: input.final,
857
861
  costUsd: input.costUsd,
858
862
  };
@@ -1052,15 +1056,11 @@ function openCodeInstalled() {
1052
1056
  /** Install a dormant global OpenCode adapter. Installing it unconditionally
1053
1057
  * keeps `codiedev connect` as the only setup step even when Codie/OpenCode is
1054
1058
  * installed later. OpenCode loads this file from its documented plugin dir. */
1055
- function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1059
+ function installOpenCodeCapturePlugin() {
1056
1060
  const start = resolveHookInvocation("session-start-opencode");
1057
1061
  const capture = resolveHookInvocation("capture-opencode");
1058
1062
  const finish = resolveHookInvocation("capture-opencode-final");
1059
- const backup = resolveHookInvocation("backup");
1060
1063
  const prompt = resolveHookInvocation("discover-codie");
1061
- const securityPre = resolveHookInvocation("security-pre-codie");
1062
- const securityPost = resolveHookInvocation("security-post-codie");
1063
- const securityFailure = resolveHookInvocation("security-failure-codie");
1064
1064
  const source = `// Managed by codiedev connect. Safe to remove and reinstall.\n` +
1065
1065
  `import { spawn, spawnSync } from "node:child_process";\n` +
1066
1066
  `import { mkdir, writeFile } from "node:fs/promises";\n` +
@@ -1069,15 +1069,9 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1069
1069
  `const START = ${JSON.stringify(start)};\n` +
1070
1070
  `const CAPTURE = ${JSON.stringify(capture)};\n` +
1071
1071
  `const FINISH = ${JSON.stringify(finish)};\n` +
1072
- `const BACKUP = ${JSON.stringify(backup)};\n` +
1073
1072
  `const PROMPT = ${JSON.stringify(prompt)};\n` +
1074
- `const SECURITY_PRE = ${JSON.stringify(securityPre)};\n` +
1075
- `const SECURITY_POST = ${JSON.stringify(securityPost)};\n` +
1076
- `const SECURITY_FAILURE = ${JSON.stringify(securityFailure)};\n` +
1077
- `const AGENT_SECURITY_ENABLED = ${JSON.stringify(agentSecurityEnabled)};\n` +
1078
1073
  `const fingerprints = new Map();\n\n` +
1079
1074
  `const transcriptPaths = new Map();\n\n` +
1080
- `const sessionDirectories = new Map();\n\n` +
1081
1075
  `function sessionId(event) {\n` +
1082
1076
  ` return event?.properties?.sessionID ?? event?.properties?.sessionId ?? event?.properties?.info?.id ?? null;\n` +
1083
1077
  `}\n\n` +
@@ -1091,47 +1085,17 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1091
1085
  `function dispatchSync(invocation, payload) {\n` +
1092
1086
  ` try { spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), stdio: ["pipe", "ignore", "ignore"], timeout: 1500 }); } catch {}\n` +
1093
1087
  `}\n\n` +
1094
- `function enforceSync(invocation, payload) {\n` +
1095
- ` const result = spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500 });\n` +
1096
- ` if (result.error) return null;\n` +
1088
+ `function invokeSync(invocation, payload) {\n` +
1097
1089
  ` try {\n` +
1098
- ` const decision = JSON.parse(result.stdout || "{}");\n` +
1099
- ` const denied = decision?.decision === "block" || decision?.hookSpecificOutput?.permissionDecision === "deny";\n` +
1100
- ` return denied ? (decision?.reason || decision?.hookSpecificOutput?.permissionDecisionReason || "Codie security policy blocked this action") : null;\n` +
1101
- ` } catch (error) {\n` +
1102
- ` if (error instanceof SyntaxError) return null;\n` +
1103
- ` return null;\n` +
1104
- ` }\n` +
1090
+ ` const result = spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500 });\n` +
1091
+ ` if (result.error) return null;\n` +
1092
+ ` return JSON.parse(result.stdout || "{}");\n` +
1093
+ ` } catch { return null; }\n` +
1105
1094
  `}\n\n` +
1106
1095
  `export const CodiedevCapture = async ({ client, directory, worktree }) => {\n` +
1107
- ` const enforce = async (invocation, payload) => {\n` +
1108
- ` const reason = enforceSync(invocation, payload);\n` +
1109
- ` if (!reason) return;\n` +
1110
- ` try { await client.tui.showToast({ title: "Codie Security", message: reason, variant: "error", duration: 10000 }); } catch {}\n` +
1111
- ` if (process.env.CODIEDEV_SECURITY_BLOCK_OUTPUT === "marker") {\n` +
1112
- ` process.stderr.write("__CODIEDEV_SECURITY_BLOCK__" + JSON.stringify({ reason }) + "\\n");\n` +
1113
- ` process.exit(1);\n` +
1114
- ` } else {\n` +
1115
- ` process.stderr.write("\\nCodie Security blocked this action:\\n" + reason + "\\n");\n` +
1116
- ` }\n` +
1117
- ` throw new Error(reason);\n` +
1118
- ` };\n` +
1119
- ` const resolveCwd = async (id, hint) => {\n` +
1120
- ` if (typeof hint === "string" && hint && hint !== "/") { sessionDirectories.set(id, hint); return hint; }\n` +
1121
- ` const remembered = sessionDirectories.get(id);\n` +
1122
- ` if (remembered) return remembered;\n` +
1123
- ` try {\n` +
1124
- ` const response = await client.session.get({ path: { id } });\n` +
1125
- ` const session = response?.data ?? response;\n` +
1126
- ` if (typeof session?.directory === "string" && session.directory) { sessionDirectories.set(id, session.directory); return session.directory; }\n` +
1127
- ` } catch {}\n` +
1128
- ` const fallback = worktree || directory || process.cwd();\n` +
1129
- ` sessionDirectories.set(id, fallback);\n` +
1130
- ` return fallback;\n` +
1131
- ` };\n` +
1132
1096
  ` const finalizeAll = () => {\n` +
1133
1097
  ` for (const [id, transcriptPath] of transcriptPaths) {\n` +
1134
- ` const cwd = sessionDirectories.get(id) || worktree || directory || process.cwd();\n` +
1098
+ ` const cwd = worktree || directory || process.cwd();\n` +
1135
1099
  ` dispatchSync(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
1136
1100
  ` }\n` +
1137
1101
  ` };\n` +
@@ -1139,27 +1103,16 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1139
1103
  ` return ({\n` +
1140
1104
  ` "chat.message": async (input, output) => {\n` +
1141
1105
  ` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
1142
- ` const cwd = await resolveCwd(id);\n` +
1106
+ ` const cwd = worktree || directory || process.cwd();\n` +
1143
1107
  ` const prompt = Array.isArray(output?.parts) ? output.parts.filter((part) => part?.type === "text").map((part) => part.text || "").join("\\n") : "";\n` +
1144
- ` await enforce(PROMPT, { sessionId: id, cwd, prompt });\n` +
1145
- ` },\n` +
1146
- ` "tool.execute.before": async (input, output) => {\n` +
1147
- ` if (!AGENT_SECURITY_ENABLED) return;\n` +
1148
- ` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
1149
- ` const cwd = await resolveCwd(id);\n` +
1150
- ` await enforce(SECURITY_PRE, { sessionId: id, cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: output?.args ?? output });\n` +
1151
- ` },\n` +
1152
- ` "tool.execute.after": async (input, output) => {\n` +
1153
- ` if (!AGENT_SECURITY_ENABLED) return;\n` +
1154
- ` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
1155
- ` const cwd = await resolveCwd(id);\n` +
1156
- ` const payload = { sessionId: id, cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: input?.args, toolResponse: output };\n` +
1157
- ` dispatch(output?.error ? SECURITY_FAILURE : SECURITY_POST, payload);\n` +
1108
+ ` const decision = invokeSync(PROMPT, { session_id: id, cwd, prompt });\n` +
1109
+ ` const context = decision?.hookSpecificOutput?.additionalContext;\n` +
1110
+ ` if (context && Array.isArray(output?.parts)) output.parts.push({ type: "text", text: context });\n` +
1158
1111
  ` },\n` +
1159
1112
  ` event: async ({ event }) => {\n` +
1160
1113
  ` const id = sessionId(event);\n` +
1161
1114
  ` if (!id) return;\n` +
1162
- ` const cwd = await resolveCwd(id, event?.properties?.info?.directory);\n` +
1115
+ ` const cwd = worktree || directory || process.cwd();\n` +
1163
1116
  ` if (event.type === "session.created") {\n` +
1164
1117
  ` dispatch(START, { session_id: id, cwd, workspace_roots: [cwd] });\n` +
1165
1118
  ` return;\n` +
@@ -1168,7 +1121,6 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1168
1121
  ` const transcriptPath = transcriptPaths.get(id);\n` +
1169
1122
  ` if (transcriptPath) dispatch(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
1170
1123
  ` transcriptPaths.delete(id);\n` +
1171
- ` sessionDirectories.delete(id);\n` +
1172
1124
  ` fingerprints.delete(id);\n` +
1173
1125
  ` return;\n` +
1174
1126
  ` }\n` +
@@ -1188,7 +1140,6 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1188
1140
  ` await writeFile(transcriptPath, JSON.stringify(list), { mode: 0o600 });\n` +
1189
1141
  ` transcriptPaths.set(id, transcriptPath);\n` +
1190
1142
  ` dispatch(CAPTURE, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
1191
- ` dispatch(BACKUP, { session_id: id, cwd, workspace_roots: [cwd] });\n` +
1192
1143
  ` } catch {}\n` +
1193
1144
  ` },\n` +
1194
1145
  ` });\n` +
@@ -1214,7 +1165,7 @@ function vscodeCopilotInstalled() {
1214
1165
  return false;
1215
1166
  }
1216
1167
  }
1217
- // Idempotently put a single codiedev hook (matcher ".*", running our hook.js
1168
+ // Idempotently put a single codiedev hook (matcher ".*" by default, running our hook.js
1218
1169
  // with `subcommand`) into the given hook event, stripping any prior codiedev
1219
1170
  // entries first so connect re-runs / upgrades refresh the resolved binary path.
1220
1171
  // Mutates `hooks` in place. Exported for unit testing.
@@ -1246,41 +1197,12 @@ function upsertCodiedevHook(hooks, eventName, subcommand, matcher = ".*") {
1246
1197
  return !(0, detection_1.isCodiedevHookCommand)(hook.matcher);
1247
1198
  });
1248
1199
  filtered.push({
1249
- matcher,
1200
+ ...(matcher === null ? {} : { matcher }),
1250
1201
  hooks: [{ type: "command", command: resolveHookCommand(subcommand, preservedNodePath) }],
1251
1202
  });
1252
1203
  hooks[eventName] = filtered;
1253
1204
  }
1254
- /** Remove Codiedev's entries for one event while preserving foreign hooks. */
1255
- function removeCodiedevHook(hooks, eventName) {
1256
- const entries = hooks[eventName];
1257
- if (!Array.isArray(entries))
1258
- return false;
1259
- let changed = false;
1260
- const filtered = entries.flatMap((entry) => {
1261
- const record = entry;
1262
- const inner = record.hooks;
1263
- if (!Array.isArray(inner))
1264
- return [record];
1265
- const remaining = inner.filter((hook) => {
1266
- const remove = (0, detection_1.isCodiedevHookCommand)(hook.command);
1267
- if (remove)
1268
- changed = true;
1269
- return !remove;
1270
- });
1271
- if (remaining.length === 0)
1272
- return [];
1273
- return [{ ...record, hooks: remaining }];
1274
- });
1275
- if (!changed)
1276
- return false;
1277
- if (filtered.length === 0)
1278
- delete hooks[eventName];
1279
- else
1280
- hooks[eventName] = filtered;
1281
- return true;
1282
- }
1283
- function installHook(agentSecurityEnabled = true) {
1205
+ function installHook() {
1284
1206
  let settings = {};
1285
1207
  try {
1286
1208
  if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
@@ -1297,18 +1219,6 @@ function installHook(agentSecurityEnabled = true) {
1297
1219
  const hooks = settings.hooks;
1298
1220
  // SessionEnd → capture (upload the finished session for the team library).
1299
1221
  upsertCodiedevHook(hooks, "SessionEnd", "capture");
1300
- // Tool boundaries → ADR shadow detection. PreToolUse is the eventual
1301
- // enforcement point; alert mode always returns allow.
1302
- if (agentSecurityEnabled) {
1303
- upsertCodiedevHook(hooks, "PreToolUse", "security-pre");
1304
- upsertCodiedevHook(hooks, "PostToolUse", "security-post");
1305
- upsertCodiedevHook(hooks, "PostToolUseFailure", "security-failure");
1306
- }
1307
- else {
1308
- removeCodiedevHook(hooks, "PreToolUse");
1309
- removeCodiedevHook(hooks, "PostToolUse");
1310
- removeCodiedevHook(hooks, "PostToolUseFailure");
1311
- }
1312
1222
  // UserPromptSubmit → discover (surface a matching team skill for the task).
1313
1223
  upsertCodiedevHook(hooks, "UserPromptSubmit", "discover");
1314
1224
  // Stop → backup (auto-back-up changed artifact files to your private library).
@@ -1410,7 +1320,7 @@ function ensureLimitHookInstalled() {
1410
1320
  // shrinks that window and removes the intra-process version entirely.
1411
1321
  // The per-hook ensure* functions below are kept for tests/back-compat but are
1412
1322
  // no longer called from the hook entrypoint.
1413
- function ensureAllCodiedevHooksInstalled(agentSecurityEnabled = readConfig()?.agentSecurityEnabled === true) {
1323
+ function ensureAllCodiedevHooksInstalled() {
1414
1324
  try {
1415
1325
  if (!fs.existsSync(CLAUDE_SETTINGS_PATH))
1416
1326
  return; // not a Claude Code user
@@ -1423,17 +1333,8 @@ function ensureAllCodiedevHooksInstalled(agentSecurityEnabled = readConfig()?.ag
1423
1333
  ["StopFailure", "limit", "rate_limit"],
1424
1334
  ["SessionStart", "session-start"],
1425
1335
  ];
1426
- if (agentSecurityEnabled) {
1427
- wanted.splice(1, 0, ["PreToolUse", "security-pre"], ["PostToolUse", "security-post"], ["PostToolUseFailure", "security-failure"]);
1428
- }
1429
- let changed = false;
1430
- if (!agentSecurityEnabled) {
1431
- for (const eventName of ["PreToolUse", "PostToolUse", "PostToolUseFailure"]) {
1432
- changed = removeCodiedevHook(hooks, eventName) || changed;
1433
- }
1434
- }
1435
1336
  const missing = wanted.filter(([eventName]) => !hasCodiedevHook(hooks, eventName));
1436
- if (missing.length === 0 && !changed)
1337
+ if (missing.length === 0)
1437
1338
  return; // steady state: no write, no churn
1438
1339
  settings.hooks = hooks;
1439
1340
  for (const [eventName, subcommand, matcher] of missing) {
@@ -1512,7 +1413,6 @@ thought, use the \`codiedev\` CLI via Bash:**
1512
1413
  | "note that X is a follow-up" / "remember X" | \`codiedev note "<text>"\` |
1513
1414
  | "promote the extracted spec" | \`codiedev promote <artifact-id>\` |
1514
1415
  | "create a ticket" / "scope this into tickets" / "ticket this for me" | \`codiedev create-ticket [optional-inline-prompt]\` |
1515
- | "post this to the team" / "publish to the feed" / "share with the team" | \`codiedev post --title "<t>" --body "<b>" [--intent <intent>] [--filename <key>] [--mention <name>]\` |
1516
1416
  | "share with a teammate" / "give someone access" (no notification) | \`codiedev share <key> <name> [--role read\\|edit]\` |
1517
1417
  | "send this to a teammate" / "ask someone to look at this" | \`codiedev send <key> <name> ["<msg>"]\` |
1518
1418
  | "find something about X" / "has anyone solved Y?" | \`codiedev search "<query>" [--limit N]\` |
@@ -1546,31 +1446,6 @@ read it back to the user so they can confirm.
1546
1446
  **Teammate names:** first name usually works (\`codiedev ping <name> ...\`).
1547
1447
  If ambiguous, the CLI returns candidates — retry with the full email.
1548
1448
 
1549
- **Feed posts (broad team reach):** use \`codiedev post\` when the user
1550
- wants visibility beyond one teammate — e.g., announcing a new skill,
1551
- asking the team who knows about X, proposing a change. Always pass
1552
- \`--intent\` to match the request: \`share\` (here's a skill),
1553
- \`request_review\`, \`request_expertise\` (stuck, who knows this),
1554
- \`link_share\` (fyi), or \`rfc\` (proposing a change). Pass \`--filename\`
1555
- to link a specific artifact and \`--mention <name>\` (repeatable) to
1556
- tag teammates.
1557
-
1558
- **Default feed-post shape.** When the user explicitly says "push/share/post
1559
- to the feed", default to this shape unless they ask for something else:
1560
- - **Title** ≤10 words: *what + why now*. Imperative or declarative — never
1561
- "Check this out".
1562
- - **Body** — three short lines, grounded in the current session and the
1563
- artifact:
1564
- - **Why this exists** — 1–2 sentences from session context (the problem
1565
- or insight that produced the artifact).
1566
- - **What's in it** — 1 sentence pulled from the artifact's frontmatter
1567
- \`description:\` (skills) or first paragraph (specs). Don't restate
1568
- the whole file — the card preview carries the detail.
1569
- - **Ask** — 1 sentence. *Use it / review it / ping me with thoughts.*
1570
- - **Filename**: always attach so the card preview renders.
1571
- This is the default. If the user wants a one-liner or a different
1572
- structure, follow their lead.
1573
-
1574
1449
  **Search before pushing.** For any "save this as …" request, run
1575
1450
  \`codiedev search "<query>"\` first. If prior art exists, pull it and
1576
1451
  iterate rather than duplicating.
@@ -1874,7 +1749,7 @@ function installCodexMcp() {
1874
1749
  // Codex doesn't have a SessionEnd event yet — Stop fires after every turn.
1875
1750
  // The backend dedupes on (companyId, sessionId) and replaces the stored
1876
1751
  // transcript on re-upload, so the latest turn's snapshot wins.
1877
- function installCodexHook(agentSecurityEnabled = true) {
1752
+ function installCodexHook() {
1878
1753
  let hooksFile = {};
1879
1754
  try {
1880
1755
  if (fs.existsSync(CODEX_HOOKS_PATH)) {
@@ -1889,8 +1764,9 @@ function installCodexHook(agentSecurityEnabled = true) {
1889
1764
  hooksFile.hooks = {};
1890
1765
  }
1891
1766
  const hooks = hooksFile.hooks;
1892
- if (!hooks.Stop)
1767
+ if (!hooks.Stop) {
1893
1768
  hooks.Stop = [];
1769
+ }
1894
1770
  const existing = hooks.Stop;
1895
1771
  const filtered = existing.filter((hook) => {
1896
1772
  const inner = hook.hooks;
@@ -1898,6 +1774,15 @@ function installCodexHook(agentSecurityEnabled = true) {
1898
1774
  return true;
1899
1775
  return !inner.some((h) => (0, detection_1.isCodiedevHookCommand)(h.command));
1900
1776
  });
1777
+ filtered.push({
1778
+ hooks: [
1779
+ {
1780
+ type: "command",
1781
+ command: resolveHookCommand("limit-codex"),
1782
+ timeout: 10,
1783
+ },
1784
+ ],
1785
+ });
1901
1786
  filtered.push({
1902
1787
  hooks: [
1903
1788
  {
@@ -1918,28 +1803,59 @@ function installCodexHook(agentSecurityEnabled = true) {
1918
1803
  ],
1919
1804
  });
1920
1805
  hooks.Stop = filtered;
1921
- // Codex uses the same Claude-style matcher-group shape. Keep these as
1922
- // separate entries because Codex hook coverage is still evolving and a
1923
- // malformed/unavailable post hook must not interfere with PreToolUse.
1924
- if (agentSecurityEnabled) {
1925
- upsertCodiedevHook(hooks, "PreToolUse", "security-pre-codex");
1926
- upsertCodiedevHook(hooks, "PostToolUse", "security-post-codex");
1927
- upsertCodiedevHook(hooks, "PostToolUseFailure", "security-failure-codex");
1928
- }
1929
- else {
1930
- removeCodiedevHook(hooks, "PreToolUse");
1931
- removeCodiedevHook(hooks, "PostToolUse");
1932
- removeCodiedevHook(hooks, "PostToolUseFailure");
1933
- }
1934
- // Prompt guard + skill discovery. The guard runs locally before any prompt
1935
- // content can reach either the model or Codiedev's discovery endpoint.
1936
- upsertCodiedevHook(hooks, "UserPromptSubmit", "discover-codex");
1806
+ // Codex models UserPromptSubmit as a non-matcher event (the trust-key model
1807
+ // intentionally excludes it from MATCHER_EVENTS), so omit the matcher key.
1808
+ upsertCodiedevHook(hooks, "UserPromptSubmit", "discover-codex", null);
1937
1809
  if (!fs.existsSync(CODEX_DIR)) {
1938
1810
  fs.mkdirSync(CODEX_DIR, { recursive: true });
1939
1811
  }
1940
1812
  fs.writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
1941
1813
  (0, codexHookTrust_1.writeCodexHookTrust)(CODEX_CONFIG_PATH, CODEX_HOOKS_PATH, hooksFile);
1942
1814
  }
1815
+ // Existing Codex users already have capture-codex on Stop. After a CLI
1816
+ // upgrade, that hook repairs newer lifecycle hooks without requiring connect.
1817
+ function ensureCodexLimitHookInstalled() {
1818
+ try {
1819
+ if (!fs.existsSync(CODEX_HOOKS_PATH))
1820
+ return;
1821
+ const hooksFile = JSON.parse(fs.readFileSync(CODEX_HOOKS_PATH, "utf8"));
1822
+ const hooks = (hooksFile.hooks ?? {});
1823
+ const stop = Array.isArray(hooks.Stop) ? hooks.Stop : [];
1824
+ const hasCommand = (entries, subcommand) => entries.some((group) => {
1825
+ const record = group;
1826
+ const flat = record.command;
1827
+ if (typeof flat === "string" && flat.trim().endsWith(subcommand))
1828
+ return true;
1829
+ const inner = record.hooks;
1830
+ return Array.isArray(inner) && inner.some((hook) => typeof hook.command === "string" && hook.command.trim().endsWith(subcommand));
1831
+ });
1832
+ let changed = false;
1833
+ if (!hasCommand(stop, "limit-codex")) {
1834
+ stop.unshift({
1835
+ hooks: [{ type: "command", command: resolveHookCommand("limit-codex"), timeout: 10 }],
1836
+ });
1837
+ hooks.Stop = stop;
1838
+ changed = true;
1839
+ }
1840
+ const promptEntries = Array.isArray(hooks.UserPromptSubmit)
1841
+ ? hooks.UserPromptSubmit
1842
+ : [];
1843
+ if (!hasCommand(promptEntries, "discover-codex")) {
1844
+ upsertCodiedevHook(hooks, "UserPromptSubmit", "discover-codex", null);
1845
+ changed = true;
1846
+ }
1847
+ if (!changed)
1848
+ return;
1849
+ hooksFile.hooks = hooks;
1850
+ fs.writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
1851
+ // Trust keys include each hook's positional index. Adding the new hook at
1852
+ // the front shifts capture/backup, so refresh every Codiedev hash together.
1853
+ (0, codexHookTrust_1.writeCodexHookTrust)(CODEX_CONFIG_PATH, CODEX_HOOKS_PATH, hooksFile);
1854
+ }
1855
+ catch {
1856
+ // Best-effort: capture must never fail because hook repair did.
1857
+ }
1858
+ }
1943
1859
  // Cursor's hooks.json uses { version: 1, hooks: { sessionEnd: [{ command, type, timeout }] } }.
1944
1860
  // `sessionEnd` fires when a conversation closes (completed, aborted, error,
1945
1861
  // window_close, user_close); the base stdin payload includes transcript_path
@@ -1965,25 +1881,61 @@ function installCursorHook() {
1965
1881
  hooksFile.hooks = {};
1966
1882
  }
1967
1883
  const hooks = hooksFile.hooks;
1968
- if (!hooks.sessionEnd) {
1969
- hooks.sessionEnd = [];
1970
- }
1971
- const existing = hooks.sessionEnd;
1972
- const filtered = existing.filter((h) => !(0, detection_1.isCodiedevHookCommand)(h.command));
1973
- filtered.push({
1974
- command: resolveHookCommand("capture-cursor"),
1975
- type: "command",
1976
- timeout: 30,
1977
- });
1978
- // sessionEnd → backup: auto-back-up changed artifact files to the private library.
1979
- filtered.push({
1980
- command: resolveHookCommand("backup"),
1981
- type: "command",
1982
- timeout: 30,
1983
- });
1984
- hooks.sessionEnd = filtered;
1884
+ const replaceManaged = (eventName, subcommands) => {
1885
+ const existing = Array.isArray(hooks[eventName])
1886
+ ? hooks[eventName]
1887
+ : [];
1888
+ const preserved = existing.filter((hook) => !(0, detection_1.isCodiedevHookCommand)(hook.command));
1889
+ const managed = subcommands.map((subcommand) => ({
1890
+ command: resolveHookCommand(subcommand),
1891
+ type: "command",
1892
+ timeout: 30,
1893
+ }));
1894
+ if (preserved.length + managed.length === 0)
1895
+ delete hooks[eventName];
1896
+ else
1897
+ hooks[eventName] = [...preserved, ...managed];
1898
+ };
1899
+ replaceManaged("sessionStart", ["session-start-cursor"]);
1900
+ replaceManaged("sessionEnd", ["capture-cursor"]);
1901
+ replaceManaged("stop", ["backup-cursor"]);
1902
+ // Cursor cannot inject dynamic context from beforeSubmitPrompt today.
1903
+ // Remove older Codiedev discovery entries so prompts are not uploaded for
1904
+ // a response the host will discard; preserve any third-party hooks.
1905
+ replaceManaged("beforeSubmitPrompt", []);
1985
1906
  fs.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
1986
1907
  }
1908
+ function ensureCursorCodiedevHooksInstalled() {
1909
+ try {
1910
+ if (!fs.existsSync(CURSOR_HOOKS_PATH))
1911
+ return;
1912
+ const parsed = JSON.parse(fs.readFileSync(CURSOR_HOOKS_PATH, "utf8"));
1913
+ const hooks = parsed.hooks ?? {};
1914
+ const required = [
1915
+ ["sessionStart", "session-start-cursor"],
1916
+ ["sessionEnd", "capture-cursor"],
1917
+ ["stop", "backup-cursor"],
1918
+ ];
1919
+ const has = (eventName, subcommand) => {
1920
+ const entries = hooks[eventName];
1921
+ return Array.isArray(entries) && entries.some((entry) => {
1922
+ const command = entry.command;
1923
+ return typeof command === "string"
1924
+ && (0, detection_1.isCodiedevHookCommand)(command)
1925
+ && command.trim().endsWith(subcommand);
1926
+ });
1927
+ };
1928
+ const promptEntries = hooks.beforeSubmitPrompt;
1929
+ const hasStalePromptHook = Array.isArray(promptEntries) && promptEntries.some((entry) => (0, detection_1.isCodiedevHookCommand)(entry.command));
1930
+ if (!required.every(([eventName, subcommand]) => has(eventName, subcommand)) ||
1931
+ hasStalePromptHook) {
1932
+ installCursorHook();
1933
+ }
1934
+ }
1935
+ catch {
1936
+ // Best-effort repair only.
1937
+ }
1938
+ }
1987
1939
  function parseClaudeSteering(content) {
1988
1940
  let interrupts = 0;
1989
1941
  let rejections = 0;
@@ -2598,18 +2550,21 @@ exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS + 2 * 60 * 1000;
2598
2550
  // hook with no subcommand) are a true final signal. capture-opencode/
2599
2551
  // capture-desktop (per-turn snapshot) and capture-opencode-final/
2600
2552
  // capture-desktop-final (real session end) get an explicit false/true.
2601
- // Everything else — capture-codex (Codex's Stop fires per-turn; final:true
2602
- // would close the server-side session lease every turn), capture-cursor, and
2603
- // any FUTURE mode (e.g. a capture-windsurf added to resolveMode but not
2604
- // audited here) — gets undefined, i.e. legacy server behaviour. Defaulting
2605
- // unknowns to true would silently re-introduce the per-turn lease-close bug
2606
- // for every new agent.
2553
+ // Cursor's capture-cursor is wired only to sessionEnd, so it is also a true
2554
+ // final signal. Everything else — capture-codex (Codex's Stop fires per-turn;
2555
+ // final:true would close the server-side session lease every turn) and any
2556
+ // FUTURE mode (e.g. a capture-windsurf added to resolveMode but not audited
2557
+ // here) — gets undefined, i.e. legacy server behaviour. Defaulting unknowns
2558
+ // to true would silently re-introduce the per-turn lease-close bug for every
2559
+ // new agent.
2607
2560
  function resolveCaptureFinal(argv2) {
2608
2561
  if (argv2 === "capture-opencode" || argv2 === "capture-desktop")
2609
2562
  return false;
2610
2563
  if (argv2 === "capture-opencode-final" || argv2 === "capture-desktop-final")
2611
2564
  return true;
2612
- return argv2 === "capture" || argv2 === undefined ? true : undefined;
2565
+ return argv2 === "capture" || argv2 === "capture-cursor" || argv2 === undefined
2566
+ ? true
2567
+ : undefined;
2613
2568
  }
2614
2569
  // Build a UNIQUE finish-input tmpFile name per dispatch. Both the Stop hook and
2615
2570
  // SessionEnd dispatch capture-finish children, and the child unlinks its input
@@ -2622,7 +2577,29 @@ function resolveCaptureFinal(argv2) {
2622
2577
  // negligible volume (one tiny JSON per crashed child), though note Windows
2623
2578
  // %TEMP% is not auto-cleaned — acceptable.
2624
2579
  function finishInputTmpFile(sessionId) {
2625
- return path.join(os.tmpdir(), `codiedev-finish-${sessionId ?? process.pid}-${process.pid}-${Date.now()}.json`);
2580
+ const safeId = String(sessionId ?? process.pid).replace(/[^A-Za-z0-9._-]/g, "_");
2581
+ return path.join(os.tmpdir(), `codiedev-finish-${safeId}-${process.pid}-${Date.now()}.json`);
2582
+ }
2583
+ function captureUploadTmpFile(sessionId, mode, pid = process.pid, now = Date.now()) {
2584
+ const id = String(sessionId ?? pid).replace(/[^A-Za-z0-9._-]/g, "_");
2585
+ return path.join(os.tmpdir(), mode === "immediate"
2586
+ ? `codiedev-capture-${id}-${pid}-${now}.json`
2587
+ : `codiedev-capture-${id}.json`);
2588
+ }
2589
+ // Best-effort cancellation for a scheduled per-turn upload when a real final
2590
+ // capture arrives. Deferred jobs deliberately share one canonical filename;
2591
+ // removing it makes every sleeping uploader lose its later claim-by-rename.
2592
+ // The backend's capturedAt guard covers the narrow race where a child claimed
2593
+ // the file immediately before this unlink.
2594
+ function cancelDeferredCaptureUpload(sessionId, deps = fs) {
2595
+ try {
2596
+ deps.unlinkSync(captureUploadTmpFile(sessionId, "deferred"));
2597
+ return true;
2598
+ }
2599
+ catch {
2600
+ // Missing means there was nothing pending (or another process won).
2601
+ return false;
2602
+ }
2626
2603
  }
2627
2604
  // Decide whether to upload immediately or defer until the throttle window expires.
2628
2605
  // final=true → always immediate (SessionEnd: close the session now)
@@ -2787,14 +2764,6 @@ async function runCaptureUpload(jobFile, deps) {
2787
2764
  const { storageId } = await deps.uploadBytes(uploadUrl, Buffer.from(transcript, "base64"), "application/json");
2788
2765
  body = { ...body, transcriptStorageId: storageId };
2789
2766
  delete body.transcript;
2790
- // Persist the storage id before the metadata POST. Previously a failed
2791
- // captureSession request retried the entire direct upload, creating one
2792
- // unreferenced raw transcript blob per attempt. Once the PUT succeeds,
2793
- // every retry must reuse that exact blob.
2794
- job.body = body;
2795
- if (deps.writeFileSync) {
2796
- deps.writeFileSync(jobFile, JSON.stringify(job), { mode: 0o600 });
2797
- }
2798
2767
  }
2799
2768
  await deps.post(job.url, body, job.token);
2800
2769
  // success — drop a "last session captured" breadcrumb (before removing the
@@ -2947,3 +2916,9 @@ function transcriptReadSkipReason(err) {
2947
2916
  return "no-transcript";
2948
2917
  return `transcript-unreadable:${code || "unknown"}`;
2949
2918
  }
2919
+ // Per-turn snapshots are backups, not session endings. Reporting their local
2920
+ // read/empty condition as a session skip would hide or close coverage while the
2921
+ // agent is still running; only final/legacy capture paths emit the skip ping.
2922
+ function shouldReportCaptureSkip(final) {
2923
+ return final !== false;
2924
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.51",
3
+ "version": "0.9.53",
4
4
  "description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",
@@ -12,9 +12,9 @@
12
12
  "dev": "tsc --watch",
13
13
  "prepublishOnly": "tsc",
14
14
  "test": "vitest run",
15
- "test:security-beta": "vitest run src/__tests__/security.test.ts src/__tests__/securityCommandEntitlement.test.ts src/__tests__/securityDeepScan.test.ts src/__tests__/securityDoctor.test.ts src/__tests__/securityEntitlementInstall.test.ts src/__tests__/securityFeature.test.ts src/__tests__/securityInstall.test.ts src/__tests__/securityInventory.test.ts src/__tests__/securityPilot.test.ts src/__tests__/securityPolicy.test.ts src/__tests__/securitySession.test.ts src/__tests__/codexHookTrust.test.ts src/__tests__/codieSecurityAdapterWiring.test.ts && tsc && node dist/cli.js security pilot --json",
16
- "test:security-pilot": "vitest run src/__tests__/security.test.ts src/__tests__/securityInstall.test.ts src/__tests__/securityPilot.test.ts src/__tests__/codieSecurityAdapterWiring.test.ts && tsc && node dist/cli.js security pilot --json",
17
- "test:opencode-attribution": "vitest run src/__tests__/repoResolver.test.ts src/__tests__/openCodeCapture.test.ts src/__tests__/openCodePluginGitRefs.test.ts src/__tests__/capturePayloadFinal.test.ts src/__tests__/captureDirectUpload.test.ts src/__tests__/resolveCaptureFinal.test.ts && tsc",
15
+ "test:functional": "vitest run src/__tests__/limitEvent.functional.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts",
16
+ "test:harness-parity": "vitest run src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts",
17
+ "test:harness-binaries": "node scripts/checkHarnessBinaries.mjs",
18
18
  "test:watch": "vitest"
19
19
  },
20
20
  "files": [
@@ -38,18 +38,12 @@
38
38
  "session-capture"
39
39
  ],
40
40
  "homepage": "https://codiedev.com",
41
- "repository": {
42
- "type": "git",
43
- "url": "git+https://github.com/Signal-and-Code/codiedev.git",
44
- "directory": "packages/codiedev-cli"
45
- },
46
41
  "license": "MIT",
47
42
  "engines": {
48
43
  "node": ">=18"
49
44
  },
50
45
  "publishConfig": {
51
- "access": "public",
52
- "provenance": true
46
+ "access": "public"
53
47
  },
54
48
  "devDependencies": {
55
49
  "@types/node": "^20.0.0",
@@ -1,24 +0,0 @@
1
- import type { AgentSurface, EndpointSecurityPolicy } from "./security";
2
- import type { AgentSecurityEntitlement } from "./securityFeature";
3
- export interface CodieStartupConfig extends AgentSecurityEntitlement {
4
- backendUrl?: string;
5
- token?: string;
6
- companyId?: string;
7
- }
8
- export type CodiePolicyRefreshResult = {
9
- status: "disabled";
10
- } | {
11
- status: "synced";
12
- policy: EndpointSecurityPolicy;
13
- } | {
14
- status: "cached";
15
- error: string;
16
- };
17
- /** Refresh the endpoint policy before Codie hands the terminal to OpenCode.
18
- * Enforcement remains available offline because hooks keep using the last
19
- * verified company-scoped policy when this best-effort refresh cannot land. */
20
- export declare function refreshCodieSecurityPolicy(config: CodieStartupConfig, syncPolicy?: (config: {
21
- backendUrl: string;
22
- token: string;
23
- companyId: string;
24
- }, source?: AgentSurface) => Promise<EndpointSecurityPolicy>): Promise<CodiePolicyRefreshResult>;