codiedev 0.9.51 → 0.10.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/utils.js CHANGED
@@ -83,7 +83,9 @@ exports.cleanupVSCodeMcp = cleanupVSCodeMcp;
83
83
  exports.cleanupCodexMcp = cleanupCodexMcp;
84
84
  exports.installCodexMcp = installCodexMcp;
85
85
  exports.installCodexHook = installCodexHook;
86
+ exports.ensureCodexLimitHookInstalled = ensureCodexLimitHookInstalled;
86
87
  exports.installCursorHook = installCursorHook;
88
+ exports.ensureCursorCodiedevHooksInstalled = ensureCursorCodiedevHooksInstalled;
87
89
  exports.parseClaudeSteering = parseClaudeSteering;
88
90
  exports.parseCodexSteering = parseCodexSteering;
89
91
  exports.parseSteering = parseSteering;
@@ -99,6 +101,8 @@ exports.dispatchDeferredUpload = dispatchDeferredUpload;
99
101
  exports.sweepPendingCaptures = sweepPendingCaptures;
100
102
  exports.resolveCaptureFinal = resolveCaptureFinal;
101
103
  exports.finishInputTmpFile = finishInputTmpFile;
104
+ exports.captureUploadTmpFile = captureUploadTmpFile;
105
+ exports.cancelDeferredCaptureUpload = cancelDeferredCaptureUpload;
102
106
  exports.decideDispatch = decideDispatch;
103
107
  exports.readSessionUploadState = readSessionUploadState;
104
108
  exports.writeSessionUploadState = writeSessionUploadState;
@@ -109,6 +113,7 @@ exports.readLastCapture = readLastCapture;
109
113
  exports.runCaptureUpload = runCaptureUpload;
110
114
  exports.runDeferredUpload = runDeferredUpload;
111
115
  exports.transcriptReadSkipReason = transcriptReadSkipReason;
116
+ exports.shouldReportCaptureSkip = shouldReportCaptureSkip;
112
117
  const fs = __importStar(require("fs"));
113
118
  const path = __importStar(require("path"));
114
119
  const os = __importStar(require("os"));
@@ -852,7 +857,7 @@ function buildCapturePayload(input) {
852
857
  sessionStartedAt: input.sessionStartedAt ?? undefined,
853
858
  interruptCount: input.interruptCount,
854
859
  rejectionCount: input.rejectionCount,
855
- // final is only emitted when explicitly set (not for Codex/Cursor).
860
+ // final is only emitted when explicitly set (omitted for Codex Stop).
856
861
  final: input.final,
857
862
  costUsd: input.costUsd,
858
863
  };
@@ -1091,22 +1096,19 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1091
1096
  `function dispatchSync(invocation, payload) {\n` +
1092
1097
  ` try { spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), stdio: ["pipe", "ignore", "ignore"], timeout: 1500 }); } catch {}\n` +
1093
1098
  `}\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` +
1099
+ `function invokeSync(invocation, payload) {\n` +
1097
1100
  ` 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` +
1101
+ ` const result = spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500 });\n` +
1102
+ ` if (result.error) return null;\n` +
1103
+ ` return JSON.parse(result.stdout || "{}");\n` +
1104
+ ` } catch { return null; }\n` +
1105
1105
  `}\n\n` +
1106
1106
  `export const CodiedevCapture = async ({ client, directory, worktree }) => {\n` +
1107
1107
  ` const enforce = async (invocation, payload) => {\n` +
1108
- ` const reason = enforceSync(invocation, payload);\n` +
1109
- ` if (!reason) return;\n` +
1108
+ ` const decision = invokeSync(invocation, payload);\n` +
1109
+ ` const denied = decision?.decision === "block" || decision?.hookSpecificOutput?.permissionDecision === "deny";\n` +
1110
+ ` const reason = denied ? (decision?.reason || decision?.hookSpecificOutput?.permissionDecisionReason || "Codie security policy blocked this action") : null;\n` +
1111
+ ` if (!reason) return decision;\n` +
1110
1112
  ` try { await client.tui.showToast({ title: "Codie Security", message: reason, variant: "error", duration: 10000 }); } catch {}\n` +
1111
1113
  ` if (process.env.CODIEDEV_SECURITY_BLOCK_OUTPUT === "marker") {\n` +
1112
1114
  ` process.stderr.write("__CODIEDEV_SECURITY_BLOCK__" + JSON.stringify({ reason }) + "\\n");\n` +
@@ -1141,7 +1143,9 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
1141
1143
  ` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
1142
1144
  ` const cwd = await resolveCwd(id);\n` +
1143
1145
  ` 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` +
1146
+ ` const decision = await enforce(PROMPT, { session_id: id, cwd, prompt });\n` +
1147
+ ` const context = decision?.hookSpecificOutput?.additionalContext;\n` +
1148
+ ` if (context && Array.isArray(output?.parts)) output.parts.push({ type: "text", text: context });\n` +
1145
1149
  ` },\n` +
1146
1150
  ` "tool.execute.before": async (input, output) => {\n` +
1147
1151
  ` if (!AGENT_SECURITY_ENABLED) return;\n` +
@@ -1214,7 +1218,7 @@ function vscodeCopilotInstalled() {
1214
1218
  return false;
1215
1219
  }
1216
1220
  }
1217
- // Idempotently put a single codiedev hook (matcher ".*", running our hook.js
1221
+ // Idempotently put a single codiedev hook (matcher ".*" by default, running our hook.js
1218
1222
  // with `subcommand`) into the given hook event, stripping any prior codiedev
1219
1223
  // entries first so connect re-runs / upgrades refresh the resolved binary path.
1220
1224
  // Mutates `hooks` in place. Exported for unit testing.
@@ -1246,7 +1250,7 @@ function upsertCodiedevHook(hooks, eventName, subcommand, matcher = ".*") {
1246
1250
  return !(0, detection_1.isCodiedevHookCommand)(hook.matcher);
1247
1251
  });
1248
1252
  filtered.push({
1249
- matcher,
1253
+ ...(matcher === null ? {} : { matcher }),
1250
1254
  hooks: [{ type: "command", command: resolveHookCommand(subcommand, preservedNodePath) }],
1251
1255
  });
1252
1256
  hooks[eventName] = filtered;
@@ -1512,7 +1516,6 @@ thought, use the \`codiedev\` CLI via Bash:**
1512
1516
  | "note that X is a follow-up" / "remember X" | \`codiedev note "<text>"\` |
1513
1517
  | "promote the extracted spec" | \`codiedev promote <artifact-id>\` |
1514
1518
  | "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
1519
  | "share with a teammate" / "give someone access" (no notification) | \`codiedev share <key> <name> [--role read\\|edit]\` |
1517
1520
  | "send this to a teammate" / "ask someone to look at this" | \`codiedev send <key> <name> ["<msg>"]\` |
1518
1521
  | "find something about X" / "has anyone solved Y?" | \`codiedev search "<query>" [--limit N]\` |
@@ -1546,31 +1549,6 @@ read it back to the user so they can confirm.
1546
1549
  **Teammate names:** first name usually works (\`codiedev ping <name> ...\`).
1547
1550
  If ambiguous, the CLI returns candidates — retry with the full email.
1548
1551
 
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
1552
  **Search before pushing.** For any "save this as …" request, run
1575
1553
  \`codiedev search "<query>"\` first. If prior art exists, pull it and
1576
1554
  iterate rather than duplicating.
@@ -1898,6 +1876,18 @@ function installCodexHook(agentSecurityEnabled = true) {
1898
1876
  return true;
1899
1877
  return !inner.some((h) => (0, detection_1.isCodiedevHookCommand)(h.command));
1900
1878
  });
1879
+ // Codex has no StopFailure matcher. Its Stop transcript includes a
1880
+ // structured rate-limit snapshot, so this tiny hook reports only when that
1881
+ // snapshot says a boundary was reached.
1882
+ filtered.push({
1883
+ hooks: [
1884
+ {
1885
+ type: "command",
1886
+ command: resolveHookCommand("limit-codex"),
1887
+ timeout: 10,
1888
+ },
1889
+ ],
1890
+ });
1901
1891
  filtered.push({
1902
1892
  hooks: [
1903
1893
  {
@@ -1933,18 +1923,64 @@ function installCodexHook(agentSecurityEnabled = true) {
1933
1923
  }
1934
1924
  // Prompt guard + skill discovery. The guard runs locally before any prompt
1935
1925
  // content can reach either the model or Codiedev's discovery endpoint.
1936
- upsertCodiedevHook(hooks, "UserPromptSubmit", "discover-codex");
1926
+ // Codex models UserPromptSubmit as a non-matcher event (the trust-key model
1927
+ // intentionally excludes it from MATCHER_EVENTS), so omit the matcher key.
1928
+ upsertCodiedevHook(hooks, "UserPromptSubmit", "discover-codex", null);
1937
1929
  if (!fs.existsSync(CODEX_DIR)) {
1938
1930
  fs.mkdirSync(CODEX_DIR, { recursive: true });
1939
1931
  }
1940
1932
  fs.writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
1941
1933
  (0, codexHookTrust_1.writeCodexHookTrust)(CODEX_CONFIG_PATH, CODEX_HOOKS_PATH, hooksFile);
1942
1934
  }
1935
+ // Existing Codex users already have capture-codex on Stop. After a CLI
1936
+ // upgrade, that hook repairs newer lifecycle hooks without requiring connect.
1937
+ function ensureCodexLimitHookInstalled() {
1938
+ try {
1939
+ if (!fs.existsSync(CODEX_HOOKS_PATH))
1940
+ return;
1941
+ const hooksFile = JSON.parse(fs.readFileSync(CODEX_HOOKS_PATH, "utf8"));
1942
+ const hooks = (hooksFile.hooks ?? {});
1943
+ const stop = Array.isArray(hooks.Stop) ? hooks.Stop : [];
1944
+ const hasCommand = (entries, subcommand) => entries.some((group) => {
1945
+ const record = group;
1946
+ const flat = record.command;
1947
+ if (typeof flat === "string" && flat.trim().endsWith(subcommand))
1948
+ return true;
1949
+ const inner = record.hooks;
1950
+ return Array.isArray(inner) && inner.some((hook) => typeof hook.command === "string" && hook.command.trim().endsWith(subcommand));
1951
+ });
1952
+ let changed = false;
1953
+ if (!hasCommand(stop, "limit-codex")) {
1954
+ stop.unshift({
1955
+ hooks: [{ type: "command", command: resolveHookCommand("limit-codex"), timeout: 10 }],
1956
+ });
1957
+ hooks.Stop = stop;
1958
+ changed = true;
1959
+ }
1960
+ const promptEntries = Array.isArray(hooks.UserPromptSubmit)
1961
+ ? hooks.UserPromptSubmit
1962
+ : [];
1963
+ if (!hasCommand(promptEntries, "discover-codex")) {
1964
+ upsertCodiedevHook(hooks, "UserPromptSubmit", "discover-codex", null);
1965
+ changed = true;
1966
+ }
1967
+ if (!changed)
1968
+ return;
1969
+ hooksFile.hooks = hooks;
1970
+ fs.writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
1971
+ // Trust keys include each hook's positional index. Adding the new hook at
1972
+ // the front shifts capture/backup, so refresh every Codiedev hash together.
1973
+ (0, codexHookTrust_1.writeCodexHookTrust)(CODEX_CONFIG_PATH, CODEX_HOOKS_PATH, hooksFile);
1974
+ }
1975
+ catch {
1976
+ // Best-effort: capture must never fail because hook repair did.
1977
+ }
1978
+ }
1943
1979
  // Cursor's hooks.json uses { version: 1, hooks: { sessionEnd: [{ command, type, timeout }] } }.
1944
1980
  // `sessionEnd` fires when a conversation closes (completed, aborted, error,
1945
1981
  // window_close, user_close); the base stdin payload includes transcript_path
1946
1982
  // and session_id which `codiedev-hook capture-cursor` reads to upload.
1947
- function installCursorHook() {
1983
+ function installCursorHook(agentSecurityEnabled = true) {
1948
1984
  if (!fs.existsSync(CURSOR_DIR)) {
1949
1985
  fs.mkdirSync(CURSOR_DIR, { recursive: true });
1950
1986
  }
@@ -1965,25 +2001,89 @@ function installCursorHook() {
1965
2001
  hooksFile.hooks = {};
1966
2002
  }
1967
2003
  const hooks = hooksFile.hooks;
1968
- if (!hooks.sessionEnd) {
1969
- hooks.sessionEnd = [];
2004
+ const replaceManaged = (eventName, subcommands) => {
2005
+ const existing = Array.isArray(hooks[eventName])
2006
+ ? hooks[eventName]
2007
+ : [];
2008
+ const preserved = existing.filter((hook) => !(0, detection_1.isCodiedevHookCommand)(hook.command));
2009
+ const managed = subcommands.map((subcommand) => ({
2010
+ command: resolveHookCommand(subcommand),
2011
+ type: "command",
2012
+ timeout: 30,
2013
+ }));
2014
+ if (preserved.length + managed.length === 0)
2015
+ delete hooks[eventName];
2016
+ else
2017
+ hooks[eventName] = [...preserved, ...managed];
2018
+ };
2019
+ // Cursor-native lifecycle names and flat hook entries. Capture only at the
2020
+ // real IDE session boundary; backup at every completed agent turn. The
2021
+ // Cursor-specific backup entrypoint keeps that non-final snapshot labeled
2022
+ // and normalized as Cursor rather than accidentally treating it as Claude.
2023
+ replaceManaged("sessionStart", ["session-start-cursor"]);
2024
+ replaceManaged("sessionEnd", ["capture-cursor"]);
2025
+ replaceManaged("stop", ["backup-cursor"]);
2026
+ if (agentSecurityEnabled) {
2027
+ // Cursor supports blocking here, but not dynamic context injection.
2028
+ replaceManaged("beforeSubmitPrompt", ["security-prompt-cursor"]);
2029
+ replaceManaged("preToolUse", ["security-pre-cursor"]);
2030
+ replaceManaged("postToolUse", ["security-post-cursor"]);
2031
+ replaceManaged("postToolUseFailure", ["security-failure-cursor"]);
2032
+ }
2033
+ else {
2034
+ replaceManaged("beforeSubmitPrompt", []);
2035
+ replaceManaged("preToolUse", []);
2036
+ replaceManaged("postToolUse", []);
2037
+ replaceManaged("postToolUseFailure", []);
1970
2038
  }
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;
1985
2039
  fs.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
1986
2040
  }
2041
+ /** Repair Cursor's complete flat hook set after upgrades or configuration
2042
+ * drift. This is deliberately separate from Claude Code's nested-hook healer:
2043
+ * the schemas and event names differ, and mixing them risks corrupting both. */
2044
+ function ensureCursorCodiedevHooksInstalled(agentSecurityEnabled = readConfig()?.agentSecurityEnabled === true) {
2045
+ try {
2046
+ if (!fs.existsSync(CURSOR_HOOKS_PATH))
2047
+ return;
2048
+ const parsed = JSON.parse(fs.readFileSync(CURSOR_HOOKS_PATH, "utf8"));
2049
+ const hooks = parsed.hooks ?? {};
2050
+ const has = (eventName, subcommand) => {
2051
+ const entries = hooks[eventName];
2052
+ return Array.isArray(entries) && entries.some((entry) => {
2053
+ const command = entry.command;
2054
+ return typeof command === "string"
2055
+ && (0, detection_1.isCodiedevHookCommand)(command)
2056
+ && command.trim().endsWith(subcommand);
2057
+ });
2058
+ };
2059
+ const required = [
2060
+ ["sessionStart", "session-start-cursor"],
2061
+ ["sessionEnd", "capture-cursor"],
2062
+ ["stop", "backup-cursor"],
2063
+ ...(agentSecurityEnabled
2064
+ ? [
2065
+ ["beforeSubmitPrompt", "security-prompt-cursor"],
2066
+ ["preToolUse", "security-pre-cursor"],
2067
+ ["postToolUse", "security-post-cursor"],
2068
+ ["postToolUseFailure", "security-failure-cursor"],
2069
+ ]
2070
+ : []),
2071
+ ];
2072
+ const hasStaleSecurity = !agentSecurityEnabled && [
2073
+ "beforeSubmitPrompt", "preToolUse", "postToolUse", "postToolUseFailure",
2074
+ ].some((eventName) => {
2075
+ const entries = hooks[eventName];
2076
+ return Array.isArray(entries) && entries.some((entry) => (0, detection_1.isCodiedevHookCommand)(entry.command));
2077
+ });
2078
+ if (required.every(([eventName, subcommand]) => has(eventName, subcommand)) && !hasStaleSecurity) {
2079
+ return;
2080
+ }
2081
+ installCursorHook(agentSecurityEnabled);
2082
+ }
2083
+ catch {
2084
+ // Best-effort — a malformed third-party config must never block capture.
2085
+ }
2086
+ }
1987
2087
  function parseClaudeSteering(content) {
1988
2088
  let interrupts = 0;
1989
2089
  let rejections = 0;
@@ -2594,13 +2694,13 @@ exports.CAPTURE_THROTTLE_MS = 10 * 60 * 1000;
2594
2694
  // retry storms.
2595
2695
  exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS + 2 * 60 * 1000;
2596
2696
  // Map the hook's capture argv to the payload's `final` flag. EXPLICIT
2597
- // ALLOWLIST: only claude-code's SessionEnd entrypoints ("capture", or the bare
2598
- // hook with no subcommand) are a true final signal. capture-opencode/
2697
+ // ALLOWLIST: Claude Code and Cursor SessionEnd entrypoints are true final
2698
+ // signals. capture-opencode/
2599
2699
  // capture-desktop (per-turn snapshot) and capture-opencode-final/
2600
2700
  // capture-desktop-final (real session end) get an explicit false/true.
2601
2701
  // 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
2702
+ // would close the server-side session lease every turn), and any FUTURE mode
2703
+ // (e.g. a capture-windsurf added to resolveMode but not
2604
2704
  // audited here) — gets undefined, i.e. legacy server behaviour. Defaulting
2605
2705
  // unknowns to true would silently re-introduce the per-turn lease-close bug
2606
2706
  // for every new agent.
@@ -2609,7 +2709,9 @@ function resolveCaptureFinal(argv2) {
2609
2709
  return false;
2610
2710
  if (argv2 === "capture-opencode-final" || argv2 === "capture-desktop-final")
2611
2711
  return true;
2612
- return argv2 === "capture" || argv2 === undefined ? true : undefined;
2712
+ return argv2 === "capture" || argv2 === "capture-cursor" || argv2 === undefined
2713
+ ? true
2714
+ : undefined;
2613
2715
  }
2614
2716
  // Build a UNIQUE finish-input tmpFile name per dispatch. Both the Stop hook and
2615
2717
  // SessionEnd dispatch capture-finish children, and the child unlinks its input
@@ -2622,7 +2724,35 @@ function resolveCaptureFinal(argv2) {
2622
2724
  // negligible volume (one tiny JSON per crashed child), though note Windows
2623
2725
  // %TEMP% is not auto-cleaned — acceptable.
2624
2726
  function finishInputTmpFile(sessionId) {
2625
- return path.join(os.tmpdir(), `codiedev-finish-${sessionId ?? process.pid}-${process.pid}-${Date.now()}.json`);
2727
+ const safeId = String(sessionId ?? process.pid).replace(/[^A-Za-z0-9._-]/g, "_");
2728
+ return path.join(os.tmpdir(), `codiedev-finish-${safeId}-${process.pid}-${Date.now()}.json`);
2729
+ }
2730
+ // Immediate uploads must also own unique job files. A Codie/OpenCode runtime
2731
+ // emits idle + final captures almost simultaneously on process exit; when both
2732
+ // children shared codiedev-capture-<sid>.json, their uploader grandchildren
2733
+ // raced to read/unlink the same file and could lose the final handoff entirely.
2734
+ // Deferred uploads intentionally keep the stable per-session name because
2735
+ // overwriting it is how newer turns supersede older scheduled snapshots.
2736
+ function captureUploadTmpFile(sessionId, mode, pid = process.pid, now = Date.now()) {
2737
+ const id = String(sessionId ?? pid).replace(/[^A-Za-z0-9._-]/g, "_");
2738
+ return path.join(os.tmpdir(), mode === "immediate"
2739
+ ? `codiedev-capture-${id}-${pid}-${now}.json`
2740
+ : `codiedev-capture-${id}.json`);
2741
+ }
2742
+ // Best-effort cancellation for a scheduled per-turn upload when a real final
2743
+ // capture arrives. Deferred jobs deliberately share one canonical filename;
2744
+ // removing it makes every sleeping uploader lose its later claim-by-rename.
2745
+ // The backend's capturedAt guard covers the narrow race where a child claimed
2746
+ // the file immediately before this unlink.
2747
+ function cancelDeferredCaptureUpload(sessionId, deps = fs) {
2748
+ try {
2749
+ deps.unlinkSync(captureUploadTmpFile(sessionId, "deferred"));
2750
+ return true;
2751
+ }
2752
+ catch {
2753
+ // Missing means there was nothing pending (or another process won).
2754
+ return false;
2755
+ }
2626
2756
  }
2627
2757
  // Decide whether to upload immediately or defer until the throttle window expires.
2628
2758
  // final=true → always immediate (SessionEnd: close the session now)
@@ -2947,3 +3077,9 @@ function transcriptReadSkipReason(err) {
2947
3077
  return "no-transcript";
2948
3078
  return `transcript-unreadable:${code || "unknown"}`;
2949
3079
  }
3080
+ // Per-turn snapshots are backups, not session endings. Reporting their local
3081
+ // read/empty condition as a session skip would hide or close coverage while the
3082
+ // agent is still running; only final/legacy capture paths emit the skip ping.
3083
+ function shouldReportCaptureSkip(final) {
3084
+ return final !== false;
3085
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.51",
3
+ "version": "0.10.0-beta.1",
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,7 +12,10 @@
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",
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
+ "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",
16
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",
17
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"
@@ -1,2 +0,0 @@
1
- import type { CodiedevConfig } from "../utils";
2
- export declare function runPost(args: string[], configOverride?: CodiedevConfig): Promise<void>;
@@ -1,96 +0,0 @@
1
- "use strict";
2
- // `codiedev post --title ... --body ... [--intent share|...] [--filename ...]
3
- // [--mention NAME ...] [--tag X ...]` — publish to the team feed. Intended
4
- // for broad-team reach: announcing a skill, asking who knows about X,
5
- // proposing a change. Use `codiedev send` to address a single teammate.
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.runPost = runPost;
8
- const shared_1 = require("./shared");
9
- const VALID_INTENTS = new Set([
10
- "share",
11
- "request_review",
12
- "request_expertise",
13
- "link_share",
14
- "rfc",
15
- ]);
16
- const VALID_FORMATS = new Set(["article", "quick", "workflow", "review"]);
17
- function parseArgs(args) {
18
- let title;
19
- let body;
20
- let intent;
21
- let filename;
22
- let format;
23
- const mentions = [];
24
- const tags = [];
25
- for (let i = 0; i < args.length; i++) {
26
- const a = args[i];
27
- const next = (k) => a === k && i + 1 < args.length ? args[++i] : a.startsWith(k + "=") ? a.slice(k.length + 1) : undefined;
28
- let v;
29
- if ((v = next("--title")) !== undefined)
30
- title = v;
31
- else if ((v = next("--body")) !== undefined)
32
- body = v;
33
- else if ((v = next("--intent")) !== undefined)
34
- intent = v;
35
- else if ((v = next("--filename")) !== undefined)
36
- filename = v;
37
- else if ((v = next("--format")) !== undefined)
38
- format = v;
39
- else if ((v = next("--mention")) !== undefined)
40
- mentions.push(v);
41
- else if ((v = next("--tag")) !== undefined)
42
- tags.push(v);
43
- }
44
- if (!title || !title.trim()) {
45
- console.error('Usage: codiedev post --title "<title>" --body "<body>" [--intent share|request_review|request_expertise|link_share|rfc] [--filename <key>] [--mention <name>]...');
46
- process.exit(1);
47
- }
48
- if (!body || !body.trim()) {
49
- console.error("--body required");
50
- process.exit(1);
51
- }
52
- if (intent && !VALID_INTENTS.has(intent)) {
53
- console.error(`Invalid --intent. Valid: ${[...VALID_INTENTS].join(", ")}`);
54
- process.exit(1);
55
- }
56
- if (format && !VALID_FORMATS.has(format)) {
57
- console.error(`Invalid --format. Valid: ${[...VALID_FORMATS].join(", ")}`);
58
- process.exit(1);
59
- }
60
- return { title, body, intent, filename, format, mentions, tags };
61
- }
62
- async function runPost(args, configOverride) {
63
- const parsed = parseArgs(args);
64
- const config = configOverride ?? (0, shared_1.requireConfig)();
65
- const start = Date.now();
66
- try {
67
- const res = await (0, shared_1.apiRequest)("POST", "/api/cli/post-to-feed", {
68
- config,
69
- body: {
70
- title: parsed.title,
71
- body: parsed.body,
72
- intent: parsed.intent,
73
- filename: parsed.filename,
74
- format: parsed.format,
75
- mentions: parsed.mentions.length > 0 ? parsed.mentions : undefined,
76
- tags: parsed.tags.length > 0 ? parsed.tags : undefined,
77
- },
78
- });
79
- (0, shared_1.recordAgentEvent)(config, {
80
- tool: "codiedev_post_to_feed",
81
- ok: true,
82
- latencyMs: Date.now() - start,
83
- });
84
- console.log(`Posted to the team feed${res.postId ? ` (id: ${res.postId})` : ""}.`);
85
- }
86
- catch (err) {
87
- (0, shared_1.recordAgentEvent)(config, {
88
- tool: "codiedev_post_to_feed",
89
- ok: false,
90
- latencyMs: Date.now() - start,
91
- error: err.message,
92
- });
93
- console.error(`Post failed: ${err.message}`);
94
- process.exit(1);
95
- }
96
- }