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/README.md +15 -3
- package/assets/opencode-plugin.mjs +54 -0
- package/assets/tui-brand-plugin.tsx +63 -0
- package/dist/backup/cache.js +2 -2
- package/dist/captureLog.js +6 -3
- package/dist/cli.js +7 -0
- package/dist/codie.d.ts +2 -0
- package/dist/codie.js +233 -0
- package/dist/codieLauncher.d.ts +41 -0
- package/dist/codieLauncher.js +352 -0
- package/dist/codieRuntime.d.ts +19 -0
- package/dist/codieRuntime.js +91 -0
- package/dist/codieRuntimeInstall.d.ts +27 -0
- package/dist/codieRuntimeInstall.js +145 -0
- package/dist/configDir.d.ts +1 -0
- package/dist/configDir.js +54 -0
- package/dist/connect.js +8 -2
- package/dist/hook.js +39 -41
- package/dist/upgradeNudge.js +6 -5
- package/dist/utils.d.ts +23 -3
- package/dist/utils.js +288 -20
- package/package.json +13 -5
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
|
|
113
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
2056
|
-
// capture-
|
|
2057
|
-
//
|
|
2058
|
-
//
|
|
2059
|
-
//
|
|
2060
|
-
//
|
|
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.
|
|
4
|
-
"description": "Connect Claude Code, Codex, Cursor, or VS Code Copilot to Codiedev for
|
|
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.
|
|
48
|
+
"vitest": "^3.2.6"
|
|
46
49
|
},
|
|
47
50
|
"dependencies": {
|
|
48
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
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
|
}
|