codiedev 0.9.25 → 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,9 +33,10 @@ 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
+ exports.writeConfigAtPath = writeConfigAtPath;
39
40
  exports.parseGitRemoteFromConfig = parseGitRemoteFromConfig;
40
41
  exports.readGitRemoteFromConfig = readGitRemoteFromConfig;
41
42
  exports.getGitRemoteUrl = getGitRemoteUrl;
@@ -57,6 +58,8 @@ exports.resetStableNodeCacheForTests = resetStableNodeCacheForTests;
57
58
  exports.claudeCodeInstalled = claudeCodeInstalled;
58
59
  exports.codexInstalled = codexInstalled;
59
60
  exports.cursorInstalled = cursorInstalled;
61
+ exports.openCodeInstalled = openCodeInstalled;
62
+ exports.installOpenCodeCapturePlugin = installOpenCodeCapturePlugin;
60
63
  exports.vscodeCopilotInstalled = vscodeCopilotInstalled;
61
64
  exports.upsertCodiedevHook = upsertCodiedevHook;
62
65
  exports.installHook = installHook;
@@ -84,6 +87,7 @@ exports.parseSteering = parseSteering;
84
87
  exports.parseUsage = parseUsage;
85
88
  exports.parseSessionStart = parseSessionStart;
86
89
  exports.parseClaudeCodeStats = parseClaudeCodeStats;
90
+ exports.normalizeOpenCodeTranscript = normalizeOpenCodeTranscript;
87
91
  exports.parseCodexStats = parseCodexStats;
88
92
  exports.extractCodexRepoUrl = extractCodexRepoUrl;
89
93
  exports.dispatchCaptureFinish = dispatchCaptureFinish;
@@ -108,13 +112,15 @@ const os = __importStar(require("os"));
108
112
  const child_process_1 = require("child_process");
109
113
  const detection_1 = require("./detection");
110
114
  const captureLog_1 = require("./captureLog");
111
- const CONFIG_DIR = path.join(os.homedir(), ".codiedev");
112
- 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");
113
119
  function readConfig() {
114
120
  try {
115
- if (!fs.existsSync(CONFIG_PATH))
121
+ if (!fs.existsSync(exports.CONFIG_PATH))
116
122
  return null;
117
- const raw = fs.readFileSync(CONFIG_PATH, "utf8");
123
+ const raw = fs.readFileSync(exports.CONFIG_PATH, "utf8");
118
124
  return JSON.parse(raw);
119
125
  }
120
126
  catch {
@@ -122,10 +128,19 @@ function readConfig() {
122
128
  }
123
129
  }
124
130
  function writeConfig(config) {
125
- if (!fs.existsSync(CONFIG_DIR)) {
126
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
131
+ writeConfigAtPath(config, CONFIG_DIR, exports.CONFIG_PATH);
132
+ }
133
+ /** Exported for permission-focused tests; normal callers use writeConfig. */
134
+ function writeConfigAtPath(config, configDir, configPath) {
135
+ if (!fs.existsSync(configDir)) {
136
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
127
137
  }
128
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
138
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), {
139
+ encoding: "utf8",
140
+ mode: 0o600,
141
+ });
142
+ // writeFile's mode does not replace permissions on a pre-existing file.
143
+ fs.chmodSync(configPath, 0o600);
129
144
  }
130
145
  // Parse a remote URL out of git-config INI text WITHOUT invoking git. Prefers
131
146
  // the "origin" remote; falls back to the first remote found. Returns null if
@@ -685,12 +700,15 @@ function buildCapturePayload(input) {
685
700
  inputTokens: input.inputTokens,
686
701
  outputTokens: input.outputTokens,
687
702
  cacheTokens: input.cacheTokens,
703
+ cacheReadTokens: input.cacheReadTokens,
704
+ cacheCreationTokens: input.cacheCreationTokens,
688
705
  model: input.model ?? undefined,
689
706
  sessionStartedAt: input.sessionStartedAt ?? undefined,
690
707
  interruptCount: input.interruptCount,
691
708
  rejectionCount: input.rejectionCount,
692
709
  // final is only emitted when explicitly set (not for Codex/Cursor).
693
710
  final: input.final,
711
+ costUsd: input.costUsd,
694
712
  };
695
713
  }
696
714
  function hashToken(token) {
@@ -707,6 +725,9 @@ const CLAUDE_DIR = path.join(os.homedir(), ".claude");
707
725
  const CODEX_HOOKS_PATH = path.join(os.homedir(), ".codex", "hooks.json");
708
726
  const CODEX_DIR = path.join(os.homedir(), ".codex");
709
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");
710
731
  const CURSOR_HOOKS_PATH = path.join(CURSOR_DIR, "hooks.json");
711
732
  const CURSOR_MCP_PATH = path.join(CURSOR_DIR, "mcp.json");
712
733
  const CURSOR_RULES_DIR = path.join(CURSOR_DIR, "rules");
@@ -822,7 +843,7 @@ function resolveHookNodePath(preferredNodePath) {
822
843
  // still an absolute path a GUI-launched agent can execute without PATH.
823
844
  return process.execPath;
824
845
  }
825
- function resolveHookCommand(subcommand, preferredNodePath) {
846
+ function resolveHookInvocation(subcommand, preferredNodePath) {
826
847
  try {
827
848
  const cliEntry = process.argv[1];
828
849
  if (cliEntry) {
@@ -831,14 +852,22 @@ function resolveHookCommand(subcommand, preferredNodePath) {
831
852
  const hookScript = path.join(distDir, "hook.js");
832
853
  if (fs.existsSync(hookScript)) {
833
854
  const nodePath = resolveHookNodePath(preferredNodePath);
834
- return `${shellQuote(nodePath)} ${shellQuote(hookScript)} ${subcommand}`;
855
+ return { command: nodePath, args: [hookScript, subcommand] };
835
856
  }
836
857
  }
837
858
  }
838
859
  catch {
839
860
  // Fall through to npx fallback.
840
861
  }
841
- 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(" ");
842
871
  }
843
872
  // `isCodiedevHookCommand` is defined in `./detection` so it's testable
844
873
  // without mocking fs/path. Re-imported below; do not re-define here.
@@ -863,6 +892,95 @@ function codexInstalled() {
863
892
  function cursorInstalled() {
864
893
  return fs.existsSync(CURSOR_DIR);
865
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
+ }
866
984
  /**
867
985
  * VS Code with the GitHub Copilot Chat extension installed. The extension
868
986
  * directory pattern is `~/.vscode/extensions/github.copilot-chat-*`. We treat
@@ -1633,7 +1751,8 @@ function parseClaudeUsage(content) {
1633
1751
  // already fresh (cache read/creation are reported separately).
1634
1752
  let inputTokens = 0;
1635
1753
  let outputTokens = 0;
1636
- let cacheTokens = 0;
1754
+ let cacheReadTokens = 0;
1755
+ let cacheCreationTokens = 0;
1637
1756
  let model = null;
1638
1757
  for (const line of content.split("\n")) {
1639
1758
  if (!line.trim())
@@ -1645,7 +1764,8 @@ function parseClaudeUsage(content) {
1645
1764
  if (u) {
1646
1765
  inputTokens += u.input_tokens ?? 0;
1647
1766
  outputTokens += u.output_tokens ?? 0;
1648
- 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;
1649
1769
  }
1650
1770
  if (typeof m?.model === "string" && m.model)
1651
1771
  model = m.model;
@@ -1654,7 +1774,14 @@ function parseClaudeUsage(content) {
1654
1774
  // skip malformed
1655
1775
  }
1656
1776
  }
1657
- return { inputTokens, outputTokens, cacheTokens, model };
1777
+ return {
1778
+ inputTokens,
1779
+ outputTokens,
1780
+ cacheTokens: cacheReadTokens + cacheCreationTokens,
1781
+ cacheReadTokens,
1782
+ cacheCreationTokens,
1783
+ model,
1784
+ };
1658
1785
  }
1659
1786
  function parseCodexUsage(content) {
1660
1787
  // Codex reports a CUMULATIVE token_count per turn — the last one is the
@@ -1679,18 +1806,87 @@ function parseCodexUsage(content) {
1679
1806
  // skip malformed
1680
1807
  }
1681
1808
  }
1682
- if (!last)
1683
- 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
+ }
1684
1812
  const cached = last.cached_input_tokens ?? 0;
1685
1813
  const input = last.input_tokens ?? 0;
1686
1814
  return {
1687
1815
  inputTokens: Math.max(0, input - cached),
1688
1816
  outputTokens: (last.output_tokens ?? 0) + (last.reasoning_output_tokens ?? 0),
1817
+ // Codex only reports cached reads; creation is structurally zero.
1689
1818
  cacheTokens: cached,
1819
+ cacheReadTokens: cached,
1820
+ cacheCreationTokens: 0,
1690
1821
  model,
1691
1822
  };
1692
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
+ }
1693
1887
  function parseUsage(content, mode) {
1888
+ if (mode === "desktop")
1889
+ return parseOpencodeUsage(content);
1694
1890
  return mode === "codex" ? parseCodexUsage(content) : parseClaudeUsage(content);
1695
1891
  }
1696
1892
  // Lead-time start proxy: the earliest top-level `timestamp` in the transcript.
@@ -1741,6 +1937,81 @@ function parseClaudeCodeStats(content) {
1741
1937
  }
1742
1938
  return { messageCount, toolCallCount };
1743
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
+ }
1744
2015
  // Codex session JSONL format (rollout-*.jsonl):
1745
2016
  // {type:"session_meta", payload:{id, cwd, git:{repository_url,...}, ...}}
1746
2017
  // {type:"response_item", payload:{type:"message", role, content:[{type:"input_text",text}|{type:"output_text",text}]}}
@@ -2042,13 +2313,20 @@ exports.CAPTURE_THROTTLE_MS = 10 * 60 * 1000;
2042
2313
  exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS + 2 * 60 * 1000;
2043
2314
  // Map the hook's capture argv to the payload's `final` flag. EXPLICIT
2044
2315
  // ALLOWLIST: only claude-code's SessionEnd entrypoints ("capture", or the bare
2045
- // hook with no subcommand) are a true final signal. Everything else —
2046
- // capture-codex (Codex's Stop fires per-turn; final:true would close the
2047
- // server-side session lease every turn), capture-cursor, and any FUTURE mode
2048
- // (e.g. a capture-windsurf added to resolveMode but not audited here) — gets
2049
- // undefined, i.e. legacy server behaviour. Defaulting unknowns to true would
2050
- // 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.
2051
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;
2052
2330
  return argv2 === "capture" || argv2 === undefined ? true : undefined;
2053
2331
  }
2054
2332
  // Build a UNIQUE finish-input tmpFile name per dispatch. Both the Stop hook and
@@ -2199,6 +2477,19 @@ async function runCaptureUpload(jobFile, deps) {
2199
2477
  return;
2200
2478
  }
2201
2479
  const sessionId = job.body?.sessionId;
2480
+ if (deps.shouldSkipJob?.(job)) {
2481
+ if (deps.log) {
2482
+ try {
2483
+ deps.log({ stage: "upload", sessionId, outcome: "skip", detail: "ignored-path" });
2484
+ }
2485
+ catch { /* log is best-effort */ }
2486
+ }
2487
+ try {
2488
+ deps.unlinkSync(jobFile);
2489
+ }
2490
+ catch { /* gone */ }
2491
+ return;
2492
+ }
2202
2493
  try {
2203
2494
  // Big transcript + capable deps → direct-to-storage: fetch a one-time
2204
2495
  // upload URL, PUT the raw bytes, send captureSession the storageId. Any
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.25",
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
  }