codiedev 0.9.56 → 0.10.0-beta.2

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 (46) hide show
  1. package/README.md +5 -4
  2. package/dist/captureLog.d.ts +1 -1
  3. package/dist/cli.js +11 -1
  4. package/dist/codexLimitScan.d.ts +0 -2
  5. package/dist/codexLimitScan.js +2 -15
  6. package/dist/codie.js +146 -1
  7. package/dist/codieLauncher.d.ts +5 -0
  8. package/dist/codieLauncher.js +23 -1
  9. package/dist/codieRuntime.d.ts +11 -1
  10. package/dist/codieRuntime.js +20 -3
  11. package/dist/codieRuntimeInstall.d.ts +12 -11
  12. package/dist/codieRuntimeInstall.js +3 -1
  13. package/dist/codieStartup.d.ts +26 -0
  14. package/dist/codieStartup.js +30 -0
  15. package/dist/commands/doctor.js +8 -0
  16. package/dist/commands/security.d.ts +1 -0
  17. package/dist/commands/security.js +156 -0
  18. package/dist/connect.js +56 -4
  19. package/dist/cursorHooks.d.ts +16 -2
  20. package/dist/cursorHooks.js +31 -6
  21. package/dist/hook.js +232 -22
  22. package/dist/limitEvent.d.ts +5 -1
  23. package/dist/limitEvent.js +6 -27
  24. package/dist/security.d.ts +102 -0
  25. package/dist/security.js +437 -0
  26. package/dist/securityDeepScan.d.ts +41 -0
  27. package/dist/securityDeepScan.js +248 -0
  28. package/dist/securityDoctor.d.ts +32 -0
  29. package/dist/securityDoctor.js +63 -0
  30. package/dist/securityFeature.d.ts +5 -0
  31. package/dist/securityFeature.js +7 -0
  32. package/dist/securityInstall.d.ts +63 -0
  33. package/dist/securityInstall.js +239 -0
  34. package/dist/securityInventory.d.ts +54 -0
  35. package/dist/securityInventory.js +556 -0
  36. package/dist/securityPilot.d.ts +19 -0
  37. package/dist/securityPilot.js +61 -0
  38. package/dist/securityPolicy.d.ts +33 -0
  39. package/dist/securityPolicy.js +188 -0
  40. package/dist/securitySession.d.ts +6 -0
  41. package/dist/securitySession.js +72 -0
  42. package/dist/upgradeNudge.d.ts +1 -0
  43. package/dist/upgradeNudge.js +1 -0
  44. package/dist/utils.d.ts +12 -6
  45. package/dist/utils.js +201 -38
  46. package/package.json +11 -2
package/dist/utils.js CHANGED
@@ -64,6 +64,7 @@ exports.openCodeInstalled = openCodeInstalled;
64
64
  exports.installOpenCodeCapturePlugin = installOpenCodeCapturePlugin;
65
65
  exports.vscodeCopilotInstalled = vscodeCopilotInstalled;
66
66
  exports.upsertCodiedevHook = upsertCodiedevHook;
67
+ exports.removeCodiedevHook = removeCodiedevHook;
67
68
  exports.installHook = installHook;
68
69
  exports.ensureDiscoveryHookInstalled = ensureDiscoveryHookInstalled;
69
70
  exports.ensureBackupHookInstalled = ensureBackupHookInstalled;
@@ -858,7 +859,7 @@ function buildCapturePayload(input) {
858
859
  sessionStartedAt: input.sessionStartedAt ?? undefined,
859
860
  interruptCount: input.interruptCount,
860
861
  rejectionCount: input.rejectionCount,
861
- // final is only emitted when explicitly set (not for Codex).
862
+ // final is only emitted when explicitly set (omitted for Codex Stop).
862
863
  final: input.final,
863
864
  costUsd: input.costUsd,
864
865
  };
@@ -1058,11 +1059,15 @@ function openCodeInstalled() {
1058
1059
  /** Install a dormant global OpenCode adapter. Installing it unconditionally
1059
1060
  * keeps `codiedev connect` as the only setup step even when Codie/OpenCode is
1060
1061
  * installed later. OpenCode loads this file from its documented plugin dir. */
1061
- function installOpenCodeCapturePlugin() {
1062
+ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1062
1063
  const start = resolveHookInvocation("session-start-opencode");
1063
1064
  const capture = resolveHookInvocation("capture-opencode");
1064
1065
  const finish = resolveHookInvocation("capture-opencode-final");
1066
+ const backup = resolveHookInvocation("backup");
1065
1067
  const prompt = resolveHookInvocation("discover-codie");
1068
+ const securityPre = resolveHookInvocation("security-pre-codie");
1069
+ const securityPost = resolveHookInvocation("security-post-codie");
1070
+ const securityFailure = resolveHookInvocation("security-failure-codie");
1066
1071
  const source = `// Managed by codiedev connect. Safe to remove and reinstall.\n` +
1067
1072
  `import { spawn, spawnSync } from "node:child_process";\n` +
1068
1073
  `import { mkdir, writeFile } from "node:fs/promises";\n` +
@@ -1071,9 +1076,15 @@ function installOpenCodeCapturePlugin() {
1071
1076
  `const START = ${JSON.stringify(start)};\n` +
1072
1077
  `const CAPTURE = ${JSON.stringify(capture)};\n` +
1073
1078
  `const FINISH = ${JSON.stringify(finish)};\n` +
1079
+ `const BACKUP = ${JSON.stringify(backup)};\n` +
1074
1080
  `const PROMPT = ${JSON.stringify(prompt)};\n` +
1081
+ `const SECURITY_PRE = ${JSON.stringify(securityPre)};\n` +
1082
+ `const SECURITY_POST = ${JSON.stringify(securityPost)};\n` +
1083
+ `const SECURITY_FAILURE = ${JSON.stringify(securityFailure)};\n` +
1084
+ `const AGENT_SECURITY_ENABLED = ${JSON.stringify(agentSecurityEnabled)};\n` +
1075
1085
  `const fingerprints = new Map();\n\n` +
1076
1086
  `const transcriptPaths = new Map();\n\n` +
1087
+ `const sessionDirectories = new Map();\n\n` +
1077
1088
  `function sessionId(event) {\n` +
1078
1089
  ` return event?.properties?.sessionID ?? event?.properties?.sessionId ?? event?.properties?.info?.id ?? null;\n` +
1079
1090
  `}\n\n` +
@@ -1095,9 +1106,36 @@ function installOpenCodeCapturePlugin() {
1095
1106
  ` } catch { return null; }\n` +
1096
1107
  `}\n\n` +
1097
1108
  `export const CodiedevCapture = async ({ client, directory, worktree }) => {\n` +
1109
+ ` const enforce = async (invocation, payload) => {\n` +
1110
+ ` const decision = invokeSync(invocation, payload);\n` +
1111
+ ` const denied = decision?.decision === "block" || decision?.hookSpecificOutput?.permissionDecision === "deny";\n` +
1112
+ ` const reason = denied ? (decision?.reason || decision?.hookSpecificOutput?.permissionDecisionReason || "Codie security policy blocked this action") : null;\n` +
1113
+ ` if (!reason) return decision;\n` +
1114
+ ` try { await client.tui.showToast({ title: "Codie Security", message: reason, variant: "error", duration: 10000 }); } catch {}\n` +
1115
+ ` if (process.env.CODIEDEV_SECURITY_BLOCK_OUTPUT === "marker") {\n` +
1116
+ ` process.stderr.write("__CODIEDEV_SECURITY_BLOCK__" + JSON.stringify({ reason }) + "\\n");\n` +
1117
+ ` process.exit(1);\n` +
1118
+ ` } else {\n` +
1119
+ ` process.stderr.write("\\nCodie Security blocked this action:\\n" + reason + "\\n");\n` +
1120
+ ` }\n` +
1121
+ ` throw new Error(reason);\n` +
1122
+ ` };\n` +
1123
+ ` const resolveCwd = async (id, hint) => {\n` +
1124
+ ` if (typeof hint === "string" && hint && hint !== "/") { sessionDirectories.set(id, hint); return hint; }\n` +
1125
+ ` const remembered = sessionDirectories.get(id);\n` +
1126
+ ` if (remembered) return remembered;\n` +
1127
+ ` try {\n` +
1128
+ ` const response = await client.session.get({ path: { id } });\n` +
1129
+ ` const session = response?.data ?? response;\n` +
1130
+ ` if (typeof session?.directory === "string" && session.directory) { sessionDirectories.set(id, session.directory); return session.directory; }\n` +
1131
+ ` } catch {}\n` +
1132
+ ` const fallback = worktree || directory || process.cwd();\n` +
1133
+ ` sessionDirectories.set(id, fallback);\n` +
1134
+ ` return fallback;\n` +
1135
+ ` };\n` +
1098
1136
  ` const finalizeAll = () => {\n` +
1099
1137
  ` for (const [id, transcriptPath] of transcriptPaths) {\n` +
1100
- ` const cwd = worktree || directory || process.cwd();\n` +
1138
+ ` const cwd = sessionDirectories.get(id) || worktree || directory || process.cwd();\n` +
1101
1139
  ` dispatchSync(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
1102
1140
  ` }\n` +
1103
1141
  ` };\n` +
@@ -1105,16 +1143,29 @@ function installOpenCodeCapturePlugin() {
1105
1143
  ` return ({\n` +
1106
1144
  ` "chat.message": async (input, output) => {\n` +
1107
1145
  ` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
1108
- ` const cwd = worktree || directory || process.cwd();\n` +
1146
+ ` const cwd = await resolveCwd(id);\n` +
1109
1147
  ` const prompt = Array.isArray(output?.parts) ? output.parts.filter((part) => part?.type === "text").map((part) => part.text || "").join("\\n") : "";\n` +
1110
- ` const decision = invokeSync(PROMPT, { session_id: id, cwd, prompt });\n` +
1148
+ ` const decision = await enforce(PROMPT, { session_id: id, cwd, prompt });\n` +
1111
1149
  ` const context = decision?.hookSpecificOutput?.additionalContext;\n` +
1112
1150
  ` if (context && Array.isArray(output?.parts)) output.parts.push({ type: "text", text: context });\n` +
1113
1151
  ` },\n` +
1152
+ ` "tool.execute.before": 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
+ ` await enforce(SECURITY_PRE, { sessionId: id, cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: output?.args ?? output });\n` +
1157
+ ` },\n` +
1158
+ ` "tool.execute.after": async (input, output) => {\n` +
1159
+ ` if (!AGENT_SECURITY_ENABLED) return;\n` +
1160
+ ` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
1161
+ ` const cwd = await resolveCwd(id);\n` +
1162
+ ` const payload = { sessionId: id, cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: input?.args, toolResponse: output };\n` +
1163
+ ` dispatch(output?.error ? SECURITY_FAILURE : SECURITY_POST, payload);\n` +
1164
+ ` },\n` +
1114
1165
  ` event: async ({ event }) => {\n` +
1115
1166
  ` const id = sessionId(event);\n` +
1116
1167
  ` if (!id) return;\n` +
1117
- ` const cwd = worktree || directory || process.cwd();\n` +
1168
+ ` const cwd = await resolveCwd(id, event?.properties?.info?.directory);\n` +
1118
1169
  ` if (event.type === "session.created") {\n` +
1119
1170
  ` dispatch(START, { session_id: id, cwd, workspace_roots: [cwd] });\n` +
1120
1171
  ` return;\n` +
@@ -1123,6 +1174,7 @@ function installOpenCodeCapturePlugin() {
1123
1174
  ` const transcriptPath = transcriptPaths.get(id);\n` +
1124
1175
  ` if (transcriptPath) dispatch(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
1125
1176
  ` transcriptPaths.delete(id);\n` +
1177
+ ` sessionDirectories.delete(id);\n` +
1126
1178
  ` fingerprints.delete(id);\n` +
1127
1179
  ` return;\n` +
1128
1180
  ` }\n` +
@@ -1142,6 +1194,7 @@ function installOpenCodeCapturePlugin() {
1142
1194
  ` await writeFile(transcriptPath, JSON.stringify(list), { mode: 0o600 });\n` +
1143
1195
  ` transcriptPaths.set(id, transcriptPath);\n` +
1144
1196
  ` dispatch(CAPTURE, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
1197
+ ` dispatch(BACKUP, { session_id: id, cwd, workspace_roots: [cwd] });\n` +
1145
1198
  ` } catch {}\n` +
1146
1199
  ` },\n` +
1147
1200
  ` });\n` +
@@ -1204,7 +1257,36 @@ function upsertCodiedevHook(hooks, eventName, subcommand, matcher = ".*") {
1204
1257
  });
1205
1258
  hooks[eventName] = filtered;
1206
1259
  }
1207
- function installHook() {
1260
+ /** Remove Codiedev's entries for one event while preserving foreign hooks. */
1261
+ function removeCodiedevHook(hooks, eventName) {
1262
+ const entries = hooks[eventName];
1263
+ if (!Array.isArray(entries))
1264
+ return false;
1265
+ let changed = false;
1266
+ const filtered = entries.flatMap((entry) => {
1267
+ const record = entry;
1268
+ const inner = record.hooks;
1269
+ if (!Array.isArray(inner))
1270
+ return [record];
1271
+ const remaining = inner.filter((hook) => {
1272
+ const remove = (0, detection_1.isCodiedevHookCommand)(hook.command);
1273
+ if (remove)
1274
+ changed = true;
1275
+ return !remove;
1276
+ });
1277
+ if (remaining.length === 0)
1278
+ return [];
1279
+ return [{ ...record, hooks: remaining }];
1280
+ });
1281
+ if (!changed)
1282
+ return false;
1283
+ if (filtered.length === 0)
1284
+ delete hooks[eventName];
1285
+ else
1286
+ hooks[eventName] = filtered;
1287
+ return true;
1288
+ }
1289
+ function installHook(agentSecurityEnabled = true) {
1208
1290
  let settings = {};
1209
1291
  try {
1210
1292
  if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
@@ -1221,6 +1303,18 @@ function installHook() {
1221
1303
  const hooks = settings.hooks;
1222
1304
  // SessionEnd → capture (upload the finished session for the team library).
1223
1305
  upsertCodiedevHook(hooks, "SessionEnd", "capture");
1306
+ // Tool boundaries → ADR shadow detection. PreToolUse is the eventual
1307
+ // enforcement point; alert mode always returns allow.
1308
+ if (agentSecurityEnabled) {
1309
+ upsertCodiedevHook(hooks, "PreToolUse", "security-pre");
1310
+ upsertCodiedevHook(hooks, "PostToolUse", "security-post");
1311
+ upsertCodiedevHook(hooks, "PostToolUseFailure", "security-failure");
1312
+ }
1313
+ else {
1314
+ removeCodiedevHook(hooks, "PreToolUse");
1315
+ removeCodiedevHook(hooks, "PostToolUse");
1316
+ removeCodiedevHook(hooks, "PostToolUseFailure");
1317
+ }
1224
1318
  // UserPromptSubmit → discover (surface a matching team skill for the task).
1225
1319
  upsertCodiedevHook(hooks, "UserPromptSubmit", "discover");
1226
1320
  // Stop → backup (auto-back-up changed artifact files to your private library).
@@ -1322,7 +1416,7 @@ function ensureLimitHookInstalled() {
1322
1416
  // shrinks that window and removes the intra-process version entirely.
1323
1417
  // The per-hook ensure* functions below are kept for tests/back-compat but are
1324
1418
  // no longer called from the hook entrypoint.
1325
- function ensureAllCodiedevHooksInstalled() {
1419
+ function ensureAllCodiedevHooksInstalled(agentSecurityEnabled = readConfig()?.agentSecurityEnabled === true) {
1326
1420
  try {
1327
1421
  if (!fs.existsSync(CLAUDE_SETTINGS_PATH))
1328
1422
  return; // not a Claude Code user
@@ -1335,8 +1429,17 @@ function ensureAllCodiedevHooksInstalled() {
1335
1429
  ["StopFailure", "limit", "rate_limit"],
1336
1430
  ["SessionStart", "session-start"],
1337
1431
  ];
1432
+ if (agentSecurityEnabled) {
1433
+ wanted.splice(1, 0, ["PreToolUse", "security-pre"], ["PostToolUse", "security-post"], ["PostToolUseFailure", "security-failure"]);
1434
+ }
1435
+ let changed = false;
1436
+ if (!agentSecurityEnabled) {
1437
+ for (const eventName of ["PreToolUse", "PostToolUse", "PostToolUseFailure"]) {
1438
+ changed = removeCodiedevHook(hooks, eventName) || changed;
1439
+ }
1440
+ }
1338
1441
  const missing = wanted.filter(([eventName]) => !hasCodiedevHook(hooks, eventName));
1339
- if (missing.length === 0)
1442
+ if (missing.length === 0 && !changed)
1340
1443
  return; // steady state: no write, no churn
1341
1444
  settings.hooks = hooks;
1342
1445
  for (const [eventName, subcommand, matcher] of missing) {
@@ -1751,7 +1854,7 @@ function installCodexMcp() {
1751
1854
  // Codex doesn't have a SessionEnd event yet — Stop fires after every turn.
1752
1855
  // The backend dedupes on (companyId, sessionId) and replaces the stored
1753
1856
  // transcript on re-upload, so the latest turn's snapshot wins.
1754
- function installCodexHook() {
1857
+ function installCodexHook(agentSecurityEnabled = true) {
1755
1858
  let hooksFile = {};
1756
1859
  try {
1757
1860
  if (fs.existsSync(CODEX_HOOKS_PATH)) {
@@ -1766,9 +1869,8 @@ function installCodexHook() {
1766
1869
  hooksFile.hooks = {};
1767
1870
  }
1768
1871
  const hooks = hooksFile.hooks;
1769
- if (!hooks.Stop) {
1872
+ if (!hooks.Stop)
1770
1873
  hooks.Stop = [];
1771
- }
1772
1874
  const existing = hooks.Stop;
1773
1875
  const filtered = existing.filter((hook) => {
1774
1876
  const inner = hook.hooks;
@@ -1776,6 +1878,9 @@ function installCodexHook() {
1776
1878
  return true;
1777
1879
  return !inner.some((h) => (0, detection_1.isCodiedevHookCommand)(h.command));
1778
1880
  });
1881
+ // Codex has no StopFailure matcher. Its Stop transcript includes a
1882
+ // structured rate-limit snapshot, so this tiny hook reports only when that
1883
+ // snapshot says a boundary was reached.
1779
1884
  filtered.push({
1780
1885
  hooks: [
1781
1886
  {
@@ -1805,6 +1910,21 @@ function installCodexHook() {
1805
1910
  ],
1806
1911
  });
1807
1912
  hooks.Stop = filtered;
1913
+ // Codex uses the same Claude-style matcher-group shape. Keep these as
1914
+ // separate entries because Codex hook coverage is still evolving and a
1915
+ // malformed/unavailable post hook must not interfere with PreToolUse.
1916
+ if (agentSecurityEnabled) {
1917
+ upsertCodiedevHook(hooks, "PreToolUse", "security-pre-codex");
1918
+ upsertCodiedevHook(hooks, "PostToolUse", "security-post-codex");
1919
+ upsertCodiedevHook(hooks, "PostToolUseFailure", "security-failure-codex");
1920
+ }
1921
+ else {
1922
+ removeCodiedevHook(hooks, "PreToolUse");
1923
+ removeCodiedevHook(hooks, "PostToolUse");
1924
+ removeCodiedevHook(hooks, "PostToolUseFailure");
1925
+ }
1926
+ // Prompt guard + skill discovery. The guard runs locally before any prompt
1927
+ // content can reach either the model or Codiedev's discovery endpoint.
1808
1928
  // Codex models UserPromptSubmit as a non-matcher event (the trust-key model
1809
1929
  // intentionally excludes it from MATCHER_EVENTS), so omit the matcher key.
1810
1930
  upsertCodiedevHook(hooks, "UserPromptSubmit", "discover-codex", null);
@@ -1862,7 +1982,7 @@ function ensureCodexLimitHookInstalled() {
1862
1982
  // `sessionEnd` fires when a conversation closes (completed, aborted, error,
1863
1983
  // window_close, user_close); the base stdin payload includes transcript_path
1864
1984
  // and session_id which `codiedev-hook capture-cursor` reads to upload.
1865
- function installCursorHook() {
1985
+ function installCursorHook(agentSecurityEnabled = true) {
1866
1986
  if (!fs.existsSync(CURSOR_DIR)) {
1867
1987
  fs.mkdirSync(CURSOR_DIR, { recursive: true });
1868
1988
  }
@@ -1898,26 +2018,37 @@ function installCursorHook() {
1898
2018
  else
1899
2019
  hooks[eventName] = [...preserved, ...managed];
1900
2020
  };
2021
+ // Cursor-native lifecycle names and flat hook entries. Capture only at the
2022
+ // real IDE session boundary; backup at every completed agent turn. The
2023
+ // Cursor-specific backup entrypoint keeps that non-final snapshot labeled
2024
+ // and normalized as Cursor rather than accidentally treating it as Claude.
1901
2025
  replaceManaged("sessionStart", ["session-start-cursor"]);
1902
2026
  replaceManaged("sessionEnd", ["capture-cursor"]);
1903
2027
  replaceManaged("stop", ["backup-cursor"]);
1904
- // Cursor cannot inject dynamic context from beforeSubmitPrompt today.
1905
- // Remove older Codiedev discovery entries so prompts are not uploaded for
1906
- // a response the host will discard; preserve any third-party hooks.
1907
- replaceManaged("beforeSubmitPrompt", []);
2028
+ if (agentSecurityEnabled) {
2029
+ // Cursor supports blocking here, but not dynamic context injection.
2030
+ replaceManaged("beforeSubmitPrompt", ["security-prompt-cursor"]);
2031
+ replaceManaged("preToolUse", ["security-pre-cursor"]);
2032
+ replaceManaged("postToolUse", ["security-post-cursor"]);
2033
+ replaceManaged("postToolUseFailure", ["security-failure-cursor"]);
2034
+ }
2035
+ else {
2036
+ replaceManaged("beforeSubmitPrompt", []);
2037
+ replaceManaged("preToolUse", []);
2038
+ replaceManaged("postToolUse", []);
2039
+ replaceManaged("postToolUseFailure", []);
2040
+ }
1908
2041
  fs.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
1909
2042
  }
1910
- function ensureCursorCodiedevHooksInstalled() {
2043
+ /** Repair Cursor's complete flat hook set after upgrades or configuration
2044
+ * drift. This is deliberately separate from Claude Code's nested-hook healer:
2045
+ * the schemas and event names differ, and mixing them risks corrupting both. */
2046
+ function ensureCursorCodiedevHooksInstalled(agentSecurityEnabled = readConfig()?.agentSecurityEnabled === true) {
1911
2047
  try {
1912
2048
  if (!fs.existsSync(CURSOR_HOOKS_PATH))
1913
2049
  return;
1914
2050
  const parsed = JSON.parse(fs.readFileSync(CURSOR_HOOKS_PATH, "utf8"));
1915
2051
  const hooks = parsed.hooks ?? {};
1916
- const required = [
1917
- ["sessionStart", "session-start-cursor"],
1918
- ["sessionEnd", "capture-cursor"],
1919
- ["stop", "backup-cursor"],
1920
- ];
1921
2052
  const has = (eventName, subcommand) => {
1922
2053
  const entries = hooks[eventName];
1923
2054
  return Array.isArray(entries) && entries.some((entry) => {
@@ -1927,15 +2058,32 @@ function ensureCursorCodiedevHooksInstalled() {
1927
2058
  && command.trim().endsWith(subcommand);
1928
2059
  });
1929
2060
  };
1930
- const promptEntries = hooks.beforeSubmitPrompt;
1931
- const hasStalePromptHook = Array.isArray(promptEntries) && promptEntries.some((entry) => (0, detection_1.isCodiedevHookCommand)(entry.command));
1932
- if (!required.every(([eventName, subcommand]) => has(eventName, subcommand)) ||
1933
- hasStalePromptHook) {
1934
- installCursorHook();
2061
+ const required = [
2062
+ ["sessionStart", "session-start-cursor"],
2063
+ ["sessionEnd", "capture-cursor"],
2064
+ ["stop", "backup-cursor"],
2065
+ ...(agentSecurityEnabled
2066
+ ? [
2067
+ ["beforeSubmitPrompt", "security-prompt-cursor"],
2068
+ ["preToolUse", "security-pre-cursor"],
2069
+ ["postToolUse", "security-post-cursor"],
2070
+ ["postToolUseFailure", "security-failure-cursor"],
2071
+ ]
2072
+ : []),
2073
+ ];
2074
+ const hasStaleSecurity = !agentSecurityEnabled && [
2075
+ "beforeSubmitPrompt", "preToolUse", "postToolUse", "postToolUseFailure",
2076
+ ].some((eventName) => {
2077
+ const entries = hooks[eventName];
2078
+ return Array.isArray(entries) && entries.some((entry) => (0, detection_1.isCodiedevHookCommand)(entry.command));
2079
+ });
2080
+ if (required.every(([eventName, subcommand]) => has(eventName, subcommand)) && !hasStaleSecurity) {
2081
+ return;
1935
2082
  }
2083
+ installCursorHook(agentSecurityEnabled);
1936
2084
  }
1937
2085
  catch {
1938
- // Best-effort repair only.
2086
+ // Best-effort — a malformed third-party config must never block capture.
1939
2087
  }
1940
2088
  }
1941
2089
  function parseClaudeSteering(content) {
@@ -2254,6 +2402,8 @@ function normalizeOpenCodeTranscript(content) {
2254
2402
  // {type:"response_item", payload:{type:"message", role, content:[{type:"input_text",text}|{type:"output_text",text}]}}
2255
2403
  // {type:"response_item", payload:{type:"function_call", name, arguments, call_id}}
2256
2404
  // {type:"response_item", payload:{type:"function_call_output", call_id, output}}
2405
+ // {type:"response_item", payload:{type:"custom_tool_call", name, input, call_id}}
2406
+ // {type:"response_item", payload:{type:"custom_tool_call_output", call_id, output}}
2257
2407
  // {type:"event_msg", payload:{type:"token_count"|"agent_reasoning"|...}}
2258
2408
  // {type:"turn_context", payload:{cwd, model, ...}}
2259
2409
  function parseCodexStats(content) {
@@ -2549,17 +2699,16 @@ exports.CAPTURE_THROTTLE_MS = 10 * 60 * 1000;
2549
2699
  // retry storms.
2550
2700
  exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS + 2 * 60 * 1000;
2551
2701
  // Map the hook's capture argv to the payload's `final` flag. EXPLICIT
2552
- // ALLOWLIST: only claude-code's SessionEnd entrypoints ("capture", or the bare
2553
- // hook with no subcommand) are a true final signal. capture-opencode/
2702
+ // ALLOWLIST: Claude Code and Cursor SessionEnd entrypoints are true final
2703
+ // signals. capture-opencode/
2554
2704
  // capture-desktop (per-turn snapshot) and capture-opencode-final/
2555
2705
  // capture-desktop-final (real session end) get an explicit false/true.
2556
- // Cursor's capture-cursor is wired only to sessionEnd, so it is also a true
2557
- // final signal. Everything else — capture-codex (Codex's Stop fires per-turn;
2558
- // final:true would close the server-side session lease every turn) and any
2559
- // FUTURE mode (e.g. a capture-windsurf added to resolveMode but not audited
2560
- // here) — gets undefined, i.e. legacy server behaviour. Defaulting unknowns
2561
- // to true would silently re-introduce the per-turn lease-close bug for every
2562
- // new agent.
2706
+ // Everything else — capture-codex (Codex's Stop fires per-turn; final:true
2707
+ // would close the server-side session lease every turn), and any FUTURE mode
2708
+ // (e.g. a capture-windsurf added to resolveMode but not
2709
+ // audited here) — gets undefined, i.e. legacy server behaviour. Defaulting
2710
+ // unknowns to true would silently re-introduce the per-turn lease-close bug
2711
+ // for every new agent.
2563
2712
  function resolveCaptureFinal(argv2) {
2564
2713
  if (argv2 === "capture-opencode" || argv2 === "capture-desktop")
2565
2714
  return false;
@@ -2583,6 +2732,12 @@ function finishInputTmpFile(sessionId) {
2583
2732
  const safeId = String(sessionId ?? process.pid).replace(/[^A-Za-z0-9._-]/g, "_");
2584
2733
  return path.join(os.tmpdir(), `codiedev-finish-${safeId}-${process.pid}-${Date.now()}.json`);
2585
2734
  }
2735
+ // Immediate uploads must also own unique job files. A Codie/OpenCode runtime
2736
+ // emits idle + final captures almost simultaneously on process exit; when both
2737
+ // children shared codiedev-capture-<sid>.json, their uploader grandchildren
2738
+ // raced to read/unlink the same file and could lose the final handoff entirely.
2739
+ // Deferred uploads intentionally keep the stable per-session name because
2740
+ // overwriting it is how newer turns supersede older scheduled snapshots.
2586
2741
  function captureUploadTmpFile(sessionId, mode, pid = process.pid, now = Date.now()) {
2587
2742
  const id = String(sessionId ?? pid).replace(/[^A-Za-z0-9._-]/g, "_");
2588
2743
  return path.join(os.tmpdir(), mode === "immediate"
@@ -2767,6 +2922,14 @@ async function runCaptureUpload(jobFile, deps) {
2767
2922
  const { storageId } = await deps.uploadBytes(uploadUrl, Buffer.from(transcript, "base64"), "application/json");
2768
2923
  body = { ...body, transcriptStorageId: storageId };
2769
2924
  delete body.transcript;
2925
+ // Persist the storage id before the metadata POST. Previously a failed
2926
+ // captureSession request retried the entire direct upload, creating one
2927
+ // unreferenced raw transcript blob per attempt. Once the PUT succeeds,
2928
+ // every retry must reuse that exact blob.
2929
+ job.body = body;
2930
+ if (deps.writeFileSync) {
2931
+ deps.writeFileSync(jobFile, JSON.stringify(job), { mode: 0o600 });
2932
+ }
2770
2933
  }
2771
2934
  await deps.post(job.url, body, job.token);
2772
2935
  // success — drop a "last session captured" breadcrumb (before removing the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.56",
3
+ "version": "0.10.0-beta.2",
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",
@@ -15,6 +15,9 @@
15
15
  "test:functional": "vitest run src/__tests__/limitEvent.functional.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts",
16
16
  "test:harness-parity": "vitest run src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts",
17
17
  "test:harness-binaries": "node scripts/checkHarnessBinaries.mjs",
18
+ "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 src/__tests__/limitEvent.functional.test.ts && tsc && node dist/cli.js security pilot --json",
19
+ "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",
20
+ "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",
18
21
  "test:watch": "vitest"
19
22
  },
20
23
  "files": [
@@ -38,12 +41,18 @@
38
41
  "session-capture"
39
42
  ],
40
43
  "homepage": "https://codiedev.com",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/Signal-and-Code/codiedev.git",
47
+ "directory": "packages/codiedev-cli"
48
+ },
41
49
  "license": "MIT",
42
50
  "engines": {
43
51
  "node": ">=18"
44
52
  },
45
53
  "publishConfig": {
46
- "access": "public"
54
+ "access": "public",
55
+ "provenance": true
47
56
  },
48
57
  "devDependencies": {
49
58
  "@types/node": "^20.0.0",