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/README.md +32 -4
- package/assets/opencode-plugin.mjs +54 -0
- package/assets/tui-brand-plugin.tsx +63 -0
- package/dist/backup/cache.js +2 -2
- package/dist/backup/run.js +5 -0
- package/dist/captureExclusions.d.ts +30 -0
- package/dist/captureExclusions.js +118 -0
- package/dist/captureLog.js +6 -3
- package/dist/cli.js +38 -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/commands/capture.d.ts +14 -0
- package/dist/commands/capture.js +148 -0
- package/dist/configDir.d.ts +1 -0
- package/dist/configDir.js +54 -0
- package/dist/connect.js +27 -2
- package/dist/hook.js +77 -43
- package/dist/upgradeNudge.js +6 -5
- package/dist/utils.d.ts +30 -3
- package/dist/utils.js +313 -22
- package/package.json +13 -5
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
|
|
112
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
2046
|
-
// capture-
|
|
2047
|
-
//
|
|
2048
|
-
//
|
|
2049
|
-
//
|
|
2050
|
-
//
|
|
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.
|
|
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
|
}
|