codiedev 0.9.26 → 0.9.28

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
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DIRECT_UPLOAD_THRESHOLD_CHARS = exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS = exports.FINISH_ORPHAN_AGE_MS = exports.CLAIM_STALE_MS = exports.UPLOAD_TTL_MS = exports.MAX_UPLOAD_ATTEMPTS = void 0;
36
+ exports.DIRECT_UPLOAD_THRESHOLD_CHARS = exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS = exports.FINISH_ORPHAN_AGE_MS = exports.CLAIM_STALE_MS = exports.UPLOAD_TTL_MS = exports.MAX_UPLOAD_ATTEMPTS = exports.CONFIG_PATH = exports.codiedevStateDir = void 0;
37
37
  exports.readConfig = readConfig;
38
38
  exports.writeConfig = writeConfig;
39
39
  exports.writeConfigAtPath = writeConfigAtPath;
@@ -58,6 +58,8 @@ exports.resetStableNodeCacheForTests = resetStableNodeCacheForTests;
58
58
  exports.claudeCodeInstalled = claudeCodeInstalled;
59
59
  exports.codexInstalled = codexInstalled;
60
60
  exports.cursorInstalled = cursorInstalled;
61
+ exports.openCodeInstalled = openCodeInstalled;
62
+ exports.installOpenCodeCapturePlugin = installOpenCodeCapturePlugin;
61
63
  exports.vscodeCopilotInstalled = vscodeCopilotInstalled;
62
64
  exports.upsertCodiedevHook = upsertCodiedevHook;
63
65
  exports.installHook = installHook;
@@ -85,6 +87,7 @@ exports.parseSteering = parseSteering;
85
87
  exports.parseUsage = parseUsage;
86
88
  exports.parseSessionStart = parseSessionStart;
87
89
  exports.parseClaudeCodeStats = parseClaudeCodeStats;
90
+ exports.normalizeOpenCodeTranscript = normalizeOpenCodeTranscript;
88
91
  exports.parseCodexStats = parseCodexStats;
89
92
  exports.extractCodexRepoUrl = extractCodexRepoUrl;
90
93
  exports.dispatchCaptureFinish = dispatchCaptureFinish;
@@ -109,13 +112,15 @@ const os = __importStar(require("os"));
109
112
  const child_process_1 = require("child_process");
110
113
  const detection_1 = require("./detection");
111
114
  const captureLog_1 = require("./captureLog");
112
- const CONFIG_DIR = path.join(os.homedir(), ".codiedev");
113
- const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
115
+ const configDir_1 = require("./configDir");
116
+ Object.defineProperty(exports, "codiedevStateDir", { enumerable: true, get: function () { return configDir_1.codiedevStateDir; } });
117
+ const CONFIG_DIR = (0, configDir_1.codiedevStateDir)();
118
+ exports.CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
114
119
  function readConfig() {
115
120
  try {
116
- if (!fs.existsSync(CONFIG_PATH))
121
+ if (!fs.existsSync(exports.CONFIG_PATH))
117
122
  return null;
118
- const raw = fs.readFileSync(CONFIG_PATH, "utf8");
123
+ const raw = fs.readFileSync(exports.CONFIG_PATH, "utf8");
119
124
  return JSON.parse(raw);
120
125
  }
121
126
  catch {
@@ -123,7 +128,7 @@ function readConfig() {
123
128
  }
124
129
  }
125
130
  function writeConfig(config) {
126
- writeConfigAtPath(config, CONFIG_DIR, CONFIG_PATH);
131
+ writeConfigAtPath(config, CONFIG_DIR, exports.CONFIG_PATH);
127
132
  }
128
133
  /** Exported for permission-focused tests; normal callers use writeConfig. */
129
134
  function writeConfigAtPath(config, configDir, configPath) {
@@ -695,12 +700,15 @@ function buildCapturePayload(input) {
695
700
  inputTokens: input.inputTokens,
696
701
  outputTokens: input.outputTokens,
697
702
  cacheTokens: input.cacheTokens,
703
+ cacheReadTokens: input.cacheReadTokens,
704
+ cacheCreationTokens: input.cacheCreationTokens,
698
705
  model: input.model ?? undefined,
699
706
  sessionStartedAt: input.sessionStartedAt ?? undefined,
700
707
  interruptCount: input.interruptCount,
701
708
  rejectionCount: input.rejectionCount,
702
709
  // final is only emitted when explicitly set (not for Codex/Cursor).
703
710
  final: input.final,
711
+ costUsd: input.costUsd,
704
712
  };
705
713
  }
706
714
  function hashToken(token) {
@@ -717,6 +725,9 @@ const CLAUDE_DIR = path.join(os.homedir(), ".claude");
717
725
  const CODEX_HOOKS_PATH = path.join(os.homedir(), ".codex", "hooks.json");
718
726
  const CODEX_DIR = path.join(os.homedir(), ".codex");
719
727
  const CURSOR_DIR = path.join(os.homedir(), ".cursor");
728
+ const OPENCODE_DIR = path.join(os.homedir(), ".config", "opencode");
729
+ const OPENCODE_PLUGIN_DIR = path.join(OPENCODE_DIR, "plugins");
730
+ const OPENCODE_PLUGIN_PATH = path.join(OPENCODE_PLUGIN_DIR, "codiedev.js");
720
731
  const CURSOR_HOOKS_PATH = path.join(CURSOR_DIR, "hooks.json");
721
732
  const CURSOR_MCP_PATH = path.join(CURSOR_DIR, "mcp.json");
722
733
  const CURSOR_RULES_DIR = path.join(CURSOR_DIR, "rules");
@@ -832,7 +843,7 @@ function resolveHookNodePath(preferredNodePath) {
832
843
  // still an absolute path a GUI-launched agent can execute without PATH.
833
844
  return process.execPath;
834
845
  }
835
- function resolveHookCommand(subcommand, preferredNodePath) {
846
+ function resolveHookInvocation(subcommand, preferredNodePath) {
836
847
  try {
837
848
  const cliEntry = process.argv[1];
838
849
  if (cliEntry) {
@@ -841,14 +852,22 @@ function resolveHookCommand(subcommand, preferredNodePath) {
841
852
  const hookScript = path.join(distDir, "hook.js");
842
853
  if (fs.existsSync(hookScript)) {
843
854
  const nodePath = resolveHookNodePath(preferredNodePath);
844
- return `${shellQuote(nodePath)} ${shellQuote(hookScript)} ${subcommand}`;
855
+ return { command: nodePath, args: [hookScript, subcommand] };
845
856
  }
846
857
  }
847
858
  }
848
859
  catch {
849
860
  // Fall through to npx fallback.
850
861
  }
851
- return `npx codiedev-hook ${subcommand}`;
862
+ return {
863
+ command: process.platform === "win32" ? "npx.cmd" : "npx",
864
+ args: ["codiedev-hook", subcommand],
865
+ };
866
+ }
867
+ function resolveHookCommand(subcommand, preferredNodePath) {
868
+ const invocation = resolveHookInvocation(subcommand, preferredNodePath);
869
+ const argsBeforeSubcommand = invocation.args.slice(0, -1).map(shellQuote);
870
+ return [shellQuote(invocation.command), ...argsBeforeSubcommand, subcommand].join(" ");
852
871
  }
853
872
  // `isCodiedevHookCommand` is defined in `./detection` so it's testable
854
873
  // without mocking fs/path. Re-imported below; do not re-define here.
@@ -873,6 +892,95 @@ function codexInstalled() {
873
892
  function cursorInstalled() {
874
893
  return fs.existsSync(CURSOR_DIR);
875
894
  }
895
+ function openCodeInstalled() {
896
+ if (fs.existsSync(OPENCODE_DIR))
897
+ return true;
898
+ try {
899
+ (0, child_process_1.execSync)("command -v opencode", { stdio: ["pipe", "pipe", "pipe"] });
900
+ return true;
901
+ }
902
+ catch {
903
+ return false;
904
+ }
905
+ }
906
+ /** Install a dormant global OpenCode adapter. Installing it unconditionally
907
+ * keeps `codiedev connect` as the only setup step even when Codie/OpenCode is
908
+ * installed later. OpenCode loads this file from its documented plugin dir. */
909
+ function installOpenCodeCapturePlugin() {
910
+ const start = resolveHookInvocation("session-start-opencode");
911
+ const capture = resolveHookInvocation("capture-opencode");
912
+ const finish = resolveHookInvocation("capture-opencode-final");
913
+ const source = `// Managed by codiedev connect. Safe to remove and reinstall.\n` +
914
+ `import { spawn, spawnSync } from "node:child_process";\n` +
915
+ `import { mkdir, writeFile } from "node:fs/promises";\n` +
916
+ `import os from "node:os";\n` +
917
+ `import path from "node:path";\n\n` +
918
+ `const START = ${JSON.stringify(start)};\n` +
919
+ `const CAPTURE = ${JSON.stringify(capture)};\n` +
920
+ `const FINISH = ${JSON.stringify(finish)};\n` +
921
+ `const fingerprints = new Map();\n\n` +
922
+ `const transcriptPaths = new Map();\n\n` +
923
+ `function sessionId(event) {\n` +
924
+ ` return event?.properties?.sessionID ?? event?.properties?.sessionId ?? event?.properties?.info?.id ?? null;\n` +
925
+ `}\n\n` +
926
+ `function dispatch(invocation, payload) {\n` +
927
+ ` try {\n` +
928
+ ` const child = spawn(invocation.command, invocation.args, { detached: true, stdio: ["pipe", "ignore", "ignore"] });\n` +
929
+ ` child.stdin.end(JSON.stringify(payload));\n` +
930
+ ` child.unref();\n` +
931
+ ` } catch {}\n` +
932
+ `}\n\n` +
933
+ `function dispatchSync(invocation, payload) {\n` +
934
+ ` try { spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), stdio: ["pipe", "ignore", "ignore"], timeout: 1500 }); } catch {}\n` +
935
+ `}\n\n` +
936
+ `export const CodiedevCapture = async ({ client, directory, worktree }) => {\n` +
937
+ ` const finalizeAll = () => {\n` +
938
+ ` for (const [id, transcriptPath] of transcriptPaths) {\n` +
939
+ ` const cwd = worktree || directory || process.cwd();\n` +
940
+ ` dispatchSync(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
941
+ ` }\n` +
942
+ ` };\n` +
943
+ ` process.once("exit", finalizeAll);\n` +
944
+ ` return ({\n` +
945
+ ` event: async ({ event }) => {\n` +
946
+ ` const id = sessionId(event);\n` +
947
+ ` if (!id) return;\n` +
948
+ ` const cwd = worktree || directory || process.cwd();\n` +
949
+ ` if (event.type === "session.created") {\n` +
950
+ ` dispatch(START, { session_id: id, cwd, workspace_roots: [cwd] });\n` +
951
+ ` return;\n` +
952
+ ` }\n` +
953
+ ` if (event.type === "session.deleted") {\n` +
954
+ ` const transcriptPath = transcriptPaths.get(id);\n` +
955
+ ` if (transcriptPath) dispatch(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
956
+ ` transcriptPaths.delete(id);\n` +
957
+ ` fingerprints.delete(id);\n` +
958
+ ` return;\n` +
959
+ ` }\n` +
960
+ ` if (event.type !== "session.idle") return;\n` +
961
+ ` try {\n` +
962
+ ` const response = await client.session.messages({ path: { id } });\n` +
963
+ ` const messages = response?.data ?? response ?? [];\n` +
964
+ ` const list = Array.isArray(messages) ? messages : [];\n` +
965
+ ` const last = list.at(-1);\n` +
966
+ ` const fingerprint = String(list.length) + ":" + String(last?.info?.id ?? last?.info?.time?.completed ?? "");\n` +
967
+ ` if (fingerprints.get(id) === fingerprint) return;\n` +
968
+ ` fingerprints.set(id, fingerprint);\n` +
969
+ ` const captureDir = path.join(os.homedir(), ".codiedev", "opencode-captures");\n` +
970
+ ` await mkdir(captureDir, { recursive: true, mode: 0o700 });\n` +
971
+ ` const safeId = String(id).replace(/[^a-zA-Z0-9._-]/g, "_");\n` +
972
+ ` const transcriptPath = path.join(captureDir, safeId + ".json");\n` +
973
+ ` await writeFile(transcriptPath, JSON.stringify(list), { mode: 0o600 });\n` +
974
+ ` transcriptPaths.set(id, transcriptPath);\n` +
975
+ ` dispatch(CAPTURE, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
976
+ ` } catch {}\n` +
977
+ ` },\n` +
978
+ ` });\n` +
979
+ `};\n`;
980
+ fs.mkdirSync(OPENCODE_PLUGIN_DIR, { recursive: true, mode: 0o700 });
981
+ fs.writeFileSync(OPENCODE_PLUGIN_PATH, source, { encoding: "utf8", mode: 0o600 });
982
+ fs.chmodSync(OPENCODE_PLUGIN_PATH, 0o600);
983
+ }
876
984
  /**
877
985
  * VS Code with the GitHub Copilot Chat extension installed. The extension
878
986
  * directory pattern is `~/.vscode/extensions/github.copilot-chat-*`. We treat
@@ -1643,7 +1751,8 @@ function parseClaudeUsage(content) {
1643
1751
  // already fresh (cache read/creation are reported separately).
1644
1752
  let inputTokens = 0;
1645
1753
  let outputTokens = 0;
1646
- let cacheTokens = 0;
1754
+ let cacheReadTokens = 0;
1755
+ let cacheCreationTokens = 0;
1647
1756
  let model = null;
1648
1757
  for (const line of content.split("\n")) {
1649
1758
  if (!line.trim())
@@ -1655,7 +1764,8 @@ function parseClaudeUsage(content) {
1655
1764
  if (u) {
1656
1765
  inputTokens += u.input_tokens ?? 0;
1657
1766
  outputTokens += u.output_tokens ?? 0;
1658
- cacheTokens += (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0);
1767
+ cacheReadTokens += u.cache_read_input_tokens ?? 0;
1768
+ cacheCreationTokens += u.cache_creation_input_tokens ?? 0;
1659
1769
  }
1660
1770
  if (typeof m?.model === "string" && m.model)
1661
1771
  model = m.model;
@@ -1664,7 +1774,14 @@ function parseClaudeUsage(content) {
1664
1774
  // skip malformed
1665
1775
  }
1666
1776
  }
1667
- return { inputTokens, outputTokens, cacheTokens, model };
1777
+ return {
1778
+ inputTokens,
1779
+ outputTokens,
1780
+ cacheTokens: cacheReadTokens + cacheCreationTokens,
1781
+ cacheReadTokens,
1782
+ cacheCreationTokens,
1783
+ model,
1784
+ };
1668
1785
  }
1669
1786
  function parseCodexUsage(content) {
1670
1787
  // Codex reports a CUMULATIVE token_count per turn — the last one is the
@@ -1689,18 +1806,87 @@ function parseCodexUsage(content) {
1689
1806
  // skip malformed
1690
1807
  }
1691
1808
  }
1692
- if (!last)
1693
- return { inputTokens: 0, outputTokens: 0, cacheTokens: 0, model };
1809
+ if (!last) {
1810
+ return { inputTokens: 0, outputTokens: 0, cacheTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, model };
1811
+ }
1694
1812
  const cached = last.cached_input_tokens ?? 0;
1695
1813
  const input = last.input_tokens ?? 0;
1696
1814
  return {
1697
1815
  inputTokens: Math.max(0, input - cached),
1698
1816
  outputTokens: (last.output_tokens ?? 0) + (last.reasoning_output_tokens ?? 0),
1817
+ // Codex only reports cached reads; creation is structurally zero.
1699
1818
  cacheTokens: cached,
1819
+ cacheReadTokens: cached,
1820
+ cacheCreationTokens: 0,
1700
1821
  model,
1701
1822
  };
1702
1823
  }
1824
+ // opencode SDK transcripts (the plugin's client.session.messages dump) are a
1825
+ // single JSON ARRAY of {info, parts} — not Claude Code's JSONL. Sum assistant
1826
+ // token/cost metadata; model = providerID/modelID (matches modelPricing slugs
1827
+ // like "openrouter/…" never resolving — stored cost is authoritative here).
1828
+ function parseOpencodeUsage(content) {
1829
+ let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheCreationTokens = 0, costUsd = 0;
1830
+ // A session can switch models (user picks another, or a provider falls back).
1831
+ // Attribute it to the model that did the most work rather than whichever
1832
+ // spoke last, since this label drives pricing and the model-mix card.
1833
+ const outputByModel = new Map();
1834
+ let lastModel = null;
1835
+ try {
1836
+ const parsed = JSON.parse(content);
1837
+ // Accept both shapes the SDK can produce: a bare array, or the
1838
+ // {data: [...]} envelope normalizeOpenCodeTranscript already unwraps.
1839
+ // Disagreeing with that parser would silently zero out a session's usage.
1840
+ const list = Array.isArray(parsed)
1841
+ ? parsed
1842
+ : (parsed && typeof parsed === "object" && Array.isArray(parsed.data)
1843
+ ? parsed.data
1844
+ : null);
1845
+ if (Array.isArray(list)) {
1846
+ for (const msg of list) {
1847
+ const info = msg?.info;
1848
+ if (!info || info.role !== "assistant")
1849
+ continue;
1850
+ const t = info.tokens;
1851
+ if (t) {
1852
+ inputTokens += t.input ?? 0;
1853
+ outputTokens += t.output ?? 0;
1854
+ cacheReadTokens += t.cache?.read ?? 0;
1855
+ cacheCreationTokens += t.cache?.write ?? 0;
1856
+ }
1857
+ if (typeof info.cost === "number" && Number.isFinite(info.cost) && info.cost > 0) {
1858
+ costUsd += info.cost;
1859
+ }
1860
+ if (typeof info.modelID === "string" && info.modelID) {
1861
+ const id = typeof info.providerID === "string" && info.providerID
1862
+ ? `${info.providerID}/${info.modelID}` : info.modelID;
1863
+ lastModel = id;
1864
+ outputByModel.set(id, (outputByModel.get(id) ?? 0) + (t?.output ?? 0));
1865
+ }
1866
+ }
1867
+ }
1868
+ }
1869
+ catch { /* malformed transcript — zero-safe */ }
1870
+ // Ties (e.g. every turn reporting zero output) fall back to the last model
1871
+ // seen, preserving the previous behaviour for single-model sessions.
1872
+ let model = lastModel;
1873
+ let best = -1;
1874
+ for (const [id, out] of outputByModel) {
1875
+ // >= so the later model wins a tie, matching the previous last-wins
1876
+ // behaviour when no turn reports output tokens.
1877
+ if (out >= best) {
1878
+ best = out;
1879
+ model = id;
1880
+ }
1881
+ }
1882
+ return { inputTokens, outputTokens,
1883
+ cacheTokens: cacheReadTokens + cacheCreationTokens,
1884
+ cacheReadTokens, cacheCreationTokens, model,
1885
+ costUsd: costUsd > 0 ? costUsd : undefined };
1886
+ }
1703
1887
  function parseUsage(content, mode) {
1888
+ if (mode === "desktop")
1889
+ return parseOpencodeUsage(content);
1704
1890
  return mode === "codex" ? parseCodexUsage(content) : parseClaudeUsage(content);
1705
1891
  }
1706
1892
  // Lead-time start proxy: the earliest top-level `timestamp` in the transcript.
@@ -1751,6 +1937,81 @@ function parseClaudeCodeStats(content) {
1751
1937
  }
1752
1938
  return { messageCount, toolCallCount };
1753
1939
  }
1940
+ /** Convert OpenCode's SDK message array (`{info, parts}[]`) into the same
1941
+ * newline-delimited shape used by Claude Code. The rest of capture—privacy
1942
+ * exclusions, git keys, token parsing, scrubbing, storage, and merge joins—can
1943
+ * therefore stay one shared pipeline instead of becoming a parallel product. */
1944
+ function normalizeOpenCodeTranscript(content) {
1945
+ let parsed;
1946
+ try {
1947
+ parsed = JSON.parse(content);
1948
+ }
1949
+ catch {
1950
+ return content;
1951
+ }
1952
+ const candidate = parsed && typeof parsed === "object" && "data" in parsed
1953
+ ? parsed.data
1954
+ : parsed;
1955
+ if (!Array.isArray(candidate))
1956
+ return content;
1957
+ const lines = [];
1958
+ for (const item of candidate) {
1959
+ if (!item || typeof item !== "object")
1960
+ continue;
1961
+ const record = item;
1962
+ const info = record.info && typeof record.info === "object" ? record.info : {};
1963
+ const role = info.role === "user" ? "user" : "assistant";
1964
+ const parts = Array.isArray(record.parts) ? record.parts : [];
1965
+ const normalizedParts = [];
1966
+ for (const part of parts) {
1967
+ if (!part || typeof part !== "object")
1968
+ continue;
1969
+ if (part.type === "text" && typeof part.text === "string") {
1970
+ normalizedParts.push({ type: "text", text: part.text });
1971
+ }
1972
+ else if (part.type === "reasoning" && typeof part.text === "string") {
1973
+ normalizedParts.push({ type: "thinking", thinking: part.text });
1974
+ }
1975
+ else if (part.type === "tool") {
1976
+ normalizedParts.push({
1977
+ type: "tool_use",
1978
+ id: part.callID ?? part.id ?? "tool",
1979
+ name: part.tool ?? "tool",
1980
+ input: part.state?.input ?? {},
1981
+ });
1982
+ if (part.state?.output != null || part.state?.error != null) {
1983
+ normalizedParts.push({
1984
+ type: "tool_result",
1985
+ tool_use_id: part.callID ?? part.id ?? "tool",
1986
+ content: part.state?.output ?? part.state?.error,
1987
+ is_error: part.state?.status === "error" || part.state?.error != null,
1988
+ });
1989
+ }
1990
+ }
1991
+ }
1992
+ const tokens = info.tokens && typeof info.tokens === "object" ? info.tokens : {};
1993
+ const created = Number(info.time?.created);
1994
+ const timestamp = Number.isFinite(created)
1995
+ ? new Date(created).toISOString()
1996
+ : new Date(0).toISOString();
1997
+ lines.push(JSON.stringify({
1998
+ type: role,
1999
+ timestamp,
2000
+ message: {
2001
+ role,
2002
+ model: typeof info.modelID === "string" ? info.modelID : undefined,
2003
+ usage: role === "assistant" ? {
2004
+ input_tokens: Number(tokens.input) || 0,
2005
+ output_tokens: (Number(tokens.output) || 0) + (Number(tokens.reasoning) || 0),
2006
+ cache_creation_input_tokens: Number(tokens.cache?.write) || 0,
2007
+ cache_read_input_tokens: Number(tokens.cache?.read) || 0,
2008
+ } : undefined,
2009
+ content: normalizedParts,
2010
+ },
2011
+ }));
2012
+ }
2013
+ return lines.join("\n") + (lines.length ? "\n" : "");
2014
+ }
1754
2015
  // Codex session JSONL format (rollout-*.jsonl):
1755
2016
  // {type:"session_meta", payload:{id, cwd, git:{repository_url,...}, ...}}
1756
2017
  // {type:"response_item", payload:{type:"message", role, content:[{type:"input_text",text}|{type:"output_text",text}]}}
@@ -2052,13 +2313,20 @@ exports.CAPTURE_THROTTLE_MS = 10 * 60 * 1000;
2052
2313
  exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS + 2 * 60 * 1000;
2053
2314
  // Map the hook's capture argv to the payload's `final` flag. EXPLICIT
2054
2315
  // ALLOWLIST: only claude-code's SessionEnd entrypoints ("capture", or the bare
2055
- // hook with no subcommand) are a true final signal. Everything else —
2056
- // capture-codex (Codex's Stop fires per-turn; final:true would close the
2057
- // server-side session lease every turn), capture-cursor, and any FUTURE mode
2058
- // (e.g. a capture-windsurf added to resolveMode but not audited here) — gets
2059
- // undefined, i.e. legacy server behaviour. Defaulting unknowns to true would
2060
- // silently re-introduce the per-turn lease-close bug for every new agent.
2316
+ // hook with no subcommand) are a true final signal. capture-opencode/
2317
+ // capture-desktop (per-turn snapshot) and capture-opencode-final/
2318
+ // capture-desktop-final (real session end) get an explicit false/true.
2319
+ // Everything else — capture-codex (Codex's Stop fires per-turn; final:true
2320
+ // would close the server-side session lease every turn), capture-cursor, and
2321
+ // any FUTURE mode (e.g. a capture-windsurf added to resolveMode but not
2322
+ // audited here) — gets undefined, i.e. legacy server behaviour. Defaulting
2323
+ // unknowns to true would silently re-introduce the per-turn lease-close bug
2324
+ // for every new agent.
2061
2325
  function resolveCaptureFinal(argv2) {
2326
+ if (argv2 === "capture-opencode" || argv2 === "capture-desktop")
2327
+ return false;
2328
+ if (argv2 === "capture-opencode-final" || argv2 === "capture-desktop-final")
2329
+ return true;
2062
2330
  return argv2 === "capture" || argv2 === undefined ? true : undefined;
2063
2331
  }
2064
2332
  // Build a UNIQUE finish-input tmpFile name per dispatch. Both the Stop hook and
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.26",
4
- "description": "Connect Claude Code, Codex, Cursor, or VS Code Copilot to Codiedev for org-wide session capture and artifact collaboration",
3
+ "version": "0.9.28",
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",
7
- "codiedev-hook": "dist/hook.js"
7
+ "codiedev-hook": "dist/hook.js",
8
+ "codie": "dist/codie.js"
8
9
  },
9
10
  "scripts": {
10
11
  "build": "tsc",
@@ -15,6 +16,7 @@
15
16
  },
16
17
  "files": [
17
18
  "dist",
19
+ "assets",
18
20
  "README.md"
19
21
  ],
20
22
  "keywords": [
@@ -24,6 +26,7 @@
24
26
  "codex",
25
27
  "openai",
26
28
  "cursor",
29
+ "opencode",
27
30
  "copilot",
28
31
  "github-copilot",
29
32
  "vscode",
@@ -42,9 +45,14 @@
42
45
  "devDependencies": {
43
46
  "@types/node": "^20.0.0",
44
47
  "typescript": "^5.4.0",
45
- "vitest": "^3.2.4"
48
+ "vitest": "^3.2.6"
46
49
  },
47
50
  "dependencies": {
48
- "@modelcontextprotocol/sdk": "^1.29.0"
51
+ "@modelcontextprotocol/sdk": "^1.30.0"
52
+ },
53
+ "overrides": {
54
+ "@hono/node-server": "^2.0.12",
55
+ "ip-address": "^10.4.0",
56
+ "esbuild": "^0.28.1"
49
57
  }
50
58
  }