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.
- package/README.md +5 -4
- package/dist/captureLog.d.ts +1 -1
- package/dist/cli.js +11 -1
- package/dist/codexLimitScan.d.ts +0 -2
- package/dist/codexLimitScan.js +2 -15
- package/dist/codie.js +146 -1
- package/dist/codieLauncher.d.ts +5 -0
- package/dist/codieLauncher.js +23 -1
- package/dist/codieRuntime.d.ts +11 -1
- package/dist/codieRuntime.js +20 -3
- package/dist/codieRuntimeInstall.d.ts +12 -11
- package/dist/codieRuntimeInstall.js +3 -1
- package/dist/codieStartup.d.ts +26 -0
- package/dist/codieStartup.js +30 -0
- package/dist/commands/doctor.js +8 -0
- package/dist/commands/security.d.ts +1 -0
- package/dist/commands/security.js +156 -0
- package/dist/connect.js +56 -4
- package/dist/cursorHooks.d.ts +16 -2
- package/dist/cursorHooks.js +31 -6
- package/dist/hook.js +232 -22
- package/dist/limitEvent.d.ts +5 -1
- package/dist/limitEvent.js +6 -27
- package/dist/security.d.ts +102 -0
- package/dist/security.js +437 -0
- package/dist/securityDeepScan.d.ts +41 -0
- package/dist/securityDeepScan.js +248 -0
- package/dist/securityDoctor.d.ts +32 -0
- package/dist/securityDoctor.js +63 -0
- package/dist/securityFeature.d.ts +5 -0
- package/dist/securityFeature.js +7 -0
- package/dist/securityInstall.d.ts +63 -0
- package/dist/securityInstall.js +239 -0
- package/dist/securityInventory.d.ts +54 -0
- package/dist/securityInventory.js +556 -0
- package/dist/securityPilot.d.ts +19 -0
- package/dist/securityPilot.js +61 -0
- package/dist/securityPolicy.d.ts +33 -0
- package/dist/securityPolicy.js +188 -0
- package/dist/securitySession.d.ts +6 -0
- package/dist/securitySession.js +72 -0
- package/dist/upgradeNudge.d.ts +1 -0
- package/dist/upgradeNudge.js +1 -0
- package/dist/utils.d.ts +12 -6
- package/dist/utils.js +201 -38
- 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 (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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
|
-
|
|
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
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
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
|
|
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:
|
|
2553
|
-
//
|
|
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
|
-
//
|
|
2557
|
-
//
|
|
2558
|
-
//
|
|
2559
|
-
//
|
|
2560
|
-
//
|
|
2561
|
-
//
|
|
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.
|
|
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",
|