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.
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.codiedevStateDir = codiedevStateDir;
37
+ // Single source of truth for where the CLI's config/state lives on disk.
38
+ //
39
+ // Defaults to ~/.codiedev. The desktop app sets CODIEDEV_CONFIG_DIR to
40
+ // ~/.codiedev-desktop so a desktop session never touches the user's real
41
+ // prod config — env-based (not a flag) so detached capture-finish/upload
42
+ // children inherit the override automatically.
43
+ //
44
+ // Deliberately dependency-free (only node:path/os) so it can be imported
45
+ // by both utils.ts and modules utils.ts itself imports (e.g. captureLog.ts)
46
+ // without creating a circular import.
47
+ const path = __importStar(require("path"));
48
+ const os = __importStar(require("os"));
49
+ const CONFIG_DIR = process.env.CODIEDEV_CONFIG_DIR?.trim()
50
+ ? path.resolve(process.env.CODIEDEV_CONFIG_DIR.trim())
51
+ : path.join(os.homedir(), ".codiedev");
52
+ function codiedevStateDir() {
53
+ return CONFIG_DIR;
54
+ }
package/dist/connect.js CHANGED
@@ -270,9 +270,11 @@ async function runConnect(args = []) {
270
270
  const hasClaude = (0, utils_1.claudeCodeInstalled)();
271
271
  const hasCodex = (0, utils_1.codexInstalled)();
272
272
  const hasCursor = (0, utils_1.cursorInstalled)();
273
+ const hasOpenCode = (0, utils_1.openCodeInstalled)();
273
274
  (0, shared_1.recordInstallEvent)(config, { step: "detect", target: "claude-code", ok: hasClaude });
274
275
  (0, shared_1.recordInstallEvent)(config, { step: "detect", target: "codex", ok: hasCodex });
275
276
  (0, shared_1.recordInstallEvent)(config, { step: "detect", target: "cursor", ok: hasCursor });
277
+ (0, shared_1.recordInstallEvent)(config, { step: "detect", target: "opencode", ok: hasOpenCode });
276
278
  const installed = [];
277
279
  // Machine-readable target keys reported back to the backend via
278
280
  // /api/cli/markInstalled so the portal's "Connected" state reflects
@@ -311,6 +313,10 @@ async function runConnect(args = []) {
311
313
  runInstall("cursor-hook", utils_1.installCursorHook, "Cursor sessionEnd (capture + auto-backup) hooks (~/.cursor/hooks.json)", "cursor-hook");
312
314
  runInstall("cursor-instructions", utils_1.installCursorInstructions, "Cursor agent instructions (~/.cursor/rules/codiedev.mdc)", "cursor-instructions");
313
315
  }
316
+ // Always stage the adapter. It is dormant until OpenCode starts, which lets
317
+ // a user connect Claude Code today and install the Codie harness tomorrow
318
+ // without needing a second setup ritual.
319
+ runInstall("opencode-capture-adapter", utils_1.installOpenCodeCapturePlugin, "OpenCode session capture adapter (~/.config/opencode/plugins/codiedev.js)", "opencode-capture");
314
320
  // Tell the backend which targets we wired up so the portal's onboarding
315
321
  // card flips to "Connected" only when something actually installed —
316
322
  // not on bare token validation. Best-effort: a network failure here
@@ -333,8 +339,8 @@ async function runConnect(args = []) {
333
339
  });
334
340
  }
335
341
  }
336
- if (!hasClaude && !hasCodex && !hasCursor) {
337
- console.warn("\nNo Claude Code, Codex, or Cursor install detected.");
342
+ if (!hasClaude && !hasCodex && !hasCursor && !hasOpenCode) {
343
+ console.warn("\nNo existing AI client detected; the Codie/OpenCode adapter is ready for a later install.");
338
344
  console.warn("If you just installed Claude Code, launch it once (so ~/.claude is created),");
339
345
  console.warn("then re-run `npx codiedev connect` to wire up the capture hook.");
340
346
  }
package/dist/hook.js CHANGED
@@ -46,36 +46,6 @@ const limitEvent_1 = require("./limitEvent");
46
46
  const version_1 = require("./version");
47
47
  const captureLog_1 = require("./captureLog");
48
48
  const captureExclusions_1 = require("./captureExclusions");
49
- function getJson(url, bearerToken) {
50
- return new Promise((resolve, reject) => {
51
- const parsed = new URL(url);
52
- const options = {
53
- hostname: parsed.hostname,
54
- port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
55
- path: parsed.pathname + parsed.search,
56
- method: "GET",
57
- headers: {
58
- Authorization: `Bearer ${bearerToken}`,
59
- "X-Codiedev-Cli-Version": version_1.CLI_VERSION,
60
- },
61
- };
62
- const lib = parsed.protocol === "https:" ? https : http;
63
- const req = lib.request(options, (res) => {
64
- let body = "";
65
- res.on("data", (c) => (body += c));
66
- res.on("end", () => {
67
- try {
68
- resolve(JSON.parse(body));
69
- }
70
- catch {
71
- reject(new Error("bad json"));
72
- }
73
- });
74
- });
75
- req.on("error", reject);
76
- req.end();
77
- });
78
- }
79
49
  // Pure builder for the discovery additionalContext. Returns null when there are
80
50
  // no matches. The suggested `codiedev pull` command carries the EXACT top key
81
51
  // (e.g. "pr-push.md") rather than a "<key>" placeholder — otherwise agents
@@ -106,7 +76,10 @@ async function runDiscover(prompt) {
106
76
  if (!config)
107
77
  process.exit(0);
108
78
  try {
109
- const res = await getJson(`${config.backendUrl}/api/cli/skill-discovery?q=${encodeURIComponent(prompt)}`, config.token);
79
+ // POST body, never a query param: raw prompts in GET query strings land
80
+ // in server/proxy access logs (SIG-16). The server keeps accepting legacy
81
+ // GETs from older CLIs during the transition.
82
+ const res = await postJsonForResponse(`${config.backendUrl}/api/cli/skill-discovery`, { q: prompt }, config.token);
110
83
  const skills = res?.skills ?? [];
111
84
  const additionalContext = buildDiscoveryContext(skills);
112
85
  if (additionalContext) {
@@ -177,7 +150,7 @@ async function postSessionSkip(config, sessionId, reason) {
177
150
  // so the backend can later compute captured ÷ started. Deliberately tiny — no
178
151
  // transcript read, no git — so it never slows session startup. Best-effort +
179
152
  // silent (SessionStart hook output is ignored by Claude Code).
180
- async function runSessionStart(hookInput) {
153
+ async function runSessionStart(hookInput, source = "claude-code") {
181
154
  const config = (0, utils_1.readConfig)();
182
155
  if (!config)
183
156
  process.exit(0);
@@ -186,7 +159,7 @@ async function runSessionStart(hookInput) {
186
159
  await postJson(`${config.backendUrl}/api/cli/sessionStart`, {
187
160
  sessionId: hookInput.session_id,
188
161
  startedAt: Date.now(),
189
- source: "claude-code",
162
+ source,
190
163
  }, config.token, PING_TIMEOUT_MS);
191
164
  }
192
165
  catch {
@@ -340,6 +313,10 @@ function resolveMode(arg) {
340
313
  return "codex";
341
314
  if (arg === "capture-cursor")
342
315
  return "cursor";
316
+ if (arg === "capture-opencode" || arg === "capture-opencode-final")
317
+ return "opencode";
318
+ if (arg === "capture-desktop" || arg === "capture-desktop-final")
319
+ return "desktop";
343
320
  return "claude-code";
344
321
  }
345
322
  async function main() {
@@ -466,8 +443,8 @@ async function main() {
466
443
  return;
467
444
  }
468
445
  // SessionStart: post the capture-coverage denominator ping. Tiny + fast.
469
- if (process.argv[2] === "session-start") {
470
- await runSessionStart(hookInput);
446
+ if (process.argv[2] === "session-start" || process.argv[2] === "session-start-opencode") {
447
+ await runSessionStart(hookInput, process.argv[2] === "session-start-opencode" ? "opencode" : "claude-code");
471
448
  return;
472
449
  }
473
450
  // Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets
@@ -610,6 +587,11 @@ async function runCaptureFinish(input) {
610
587
  }
611
588
  async function runCaptureFinishInner(input, config, session_id, transcript_path, cwd, workspace_roots) {
612
589
  const mode = resolveMode(input.mode);
590
+ // Desktop transcripts (opencode SDK {info,parts}[] dumps) are byte-identical
591
+ // to opencode's, so every parser below EXCEPT parseUsage treats desktop
592
+ // exactly as opencode via this mapping. parseUsage is the one exception —
593
+ // see the rawTranscriptContent note below.
594
+ const parserMode = mode === "desktop" ? "opencode" : mode;
613
595
  const workspaceRoot = workspace_roots && workspace_roots.length > 0
614
596
  ? workspace_roots[0]
615
597
  : cwd || process.cwd();
@@ -632,10 +614,19 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
632
614
  await postSessionSkip(config, session_id, reason);
633
615
  return;
634
616
  }
617
+ // Snapshot the RAW transcript before normalization below flattens it to
618
+ // Claude Code's newline-JSONL shape — that flattening drops info.cost /
619
+ // info.providerID, which desktop's parseUsage (below) needs and opencode's
620
+ // doesn't. Every other mode leaves transcriptContent untouched here, so this
621
+ // is a no-op for them.
622
+ const rawTranscriptContent = transcriptContent;
623
+ if (parserMode === "opencode") {
624
+ transcriptContent = (0, utils_1.normalizeOpenCodeTranscript)(transcriptContent);
625
+ }
635
626
  // Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
636
627
  // need the git remote of the workspace.
637
628
  let remoteUrl = null;
638
- if (mode === "codex") {
629
+ if (parserMode === "codex") {
639
630
  remoteUrl = (0, utils_1.extractCodexRepoUrl)(transcriptContent);
640
631
  }
641
632
  // The worktrees the session actually wrote to — resolved up front because
@@ -656,9 +647,9 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
656
647
  // Best-effort local match — backend re-resolves from `remoteUrl` against the
657
648
  // company's current repo list, so a stale/empty local cache doesn't matter.
658
649
  const matchedRepo = remoteUrl ? (0, utils_1.matchRepo)(remoteUrl, config.repos) : null;
659
- const stats = mode === "codex"
650
+ const stats = parserMode === "codex"
660
651
  ? (0, utils_1.parseCodexStats)(transcriptContent)
661
- : mode === "cursor"
652
+ : parserMode === "cursor"
662
653
  ? { messageCount: 0, toolCallCount: 0 }
663
654
  : (0, utils_1.parseClaudeCodeStats)(transcriptContent);
664
655
  const transcriptBase64 = Buffer.from(transcriptContent, "utf8").toString("base64");
@@ -688,10 +679,13 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
688
679
  const headShas = [...new Set([...keys.headShas, ...authoredShas])];
689
680
  const headSha = keys.headSha ?? headShas[0] ?? null;
690
681
  // Cost/velocity/steering signals parsed straight from the transcript we're
691
- // already holding — no extra hooks, no extra git/process calls.
692
- const usage = (0, utils_1.parseUsage)(transcriptContent, mode);
682
+ // already holding — no extra hooks, no extra git/process calls. parseUsage
683
+ // keeps the literal mode (it has its own desktop parser) and reads the RAW
684
+ // pre-normalization content for desktop; parseSteering routes through
685
+ // parserMode like the parsers above.
686
+ const usage = (0, utils_1.parseUsage)(mode === "desktop" ? rawTranscriptContent : transcriptContent, mode);
693
687
  const sessionStartedAt = (0, utils_1.parseSessionStart)(transcriptContent);
694
- const steering = (0, utils_1.parseSteering)(transcriptContent, mode);
688
+ const steering = (0, utils_1.parseSteering)(transcriptContent, parserMode);
695
689
  const payload = (0, utils_1.buildCapturePayload)({
696
690
  sessionId: session_id,
697
691
  transcriptBase64,
@@ -709,6 +703,8 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
709
703
  inputTokens: usage.inputTokens,
710
704
  outputTokens: usage.outputTokens,
711
705
  cacheTokens: usage.cacheTokens,
706
+ cacheReadTokens: usage.cacheReadTokens,
707
+ cacheCreationTokens: usage.cacheCreationTokens,
712
708
  model: usage.model,
713
709
  sessionStartedAt,
714
710
  interruptCount: steering.interrupts,
@@ -716,6 +712,8 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
716
712
  // Carry the final flag into the HTTP body — the server uses it to decide
717
713
  // whether to close the session lease (true) or renew it (false/absent).
718
714
  final: input.final,
715
+ // Desktop-only measured spend (undefined for every other source).
716
+ costUsd: usage.costUsd,
719
717
  });
720
718
  // Throttle decision: final captures go immediately; non-final (per-turn Stop)
721
719
  // are deferred if we uploaded this session recently, to avoid per-turn churn.
@@ -4,9 +4,10 @@
4
4
  // __tests__/versionCheck.test.ts; this module does the IO (registry fetch +
5
5
  // disk cache) and is exercised by manual smoke.
6
6
  //
7
- // Cache lives at ~/.codiedev/.update-cache.json. 24h TTL — long enough that
8
- // the registry isn't hit on every command, short enough that newly-published
9
- // versions surface the day after release.
7
+ // Cache lives at <codiedevStateDir()>/.update-cache.json (~/.codiedev by
8
+ // default; ~/.codiedev-desktop under CODIEDEV_CONFIG_DIR). 24h TTL — long
9
+ // enough that the registry isn't hit on every command, short enough that
10
+ // newly-published versions surface the day after release.
10
11
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
12
  if (k2 === undefined) k2 = k;
12
13
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -44,10 +45,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
44
45
  exports.maybePrintUpgradeNudge = maybePrintUpgradeNudge;
45
46
  const fs = __importStar(require("fs"));
46
47
  const path = __importStar(require("path"));
47
- const os = __importStar(require("os"));
48
48
  const version_1 = require("./version");
49
49
  const versionCheck_1 = require("./versionCheck");
50
- const CACHE_PATH = path.join(os.homedir(), ".codiedev", ".update-cache.json");
50
+ const configDir_1 = require("./configDir");
51
+ const CACHE_PATH = path.join((0, configDir_1.codiedevStateDir)(), ".update-cache.json");
51
52
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
52
53
  const REGISTRY_URL = "https://registry.npmjs.org/codiedev/latest";
53
54
  const FETCH_TIMEOUT_MS = 1500;
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import * as fs from "fs";
2
2
  import { spawn as nodeSpawn } from "child_process";
3
3
  import type { CaptureLogEntry } from "./captureLog";
4
+ import { codiedevStateDir } from "./configDir";
5
+ export { codiedevStateDir };
4
6
  export interface CodiedevConfig {
5
7
  token: string;
6
8
  companyId: string;
@@ -16,6 +18,7 @@ export interface CodiedevConfig {
16
18
  deviceName?: string;
17
19
  captureIgnoredPaths?: string[];
18
20
  }
21
+ export declare const CONFIG_PATH: string;
19
22
  export declare function readConfig(): CodiedevConfig | null;
20
23
  export declare function writeConfig(config: CodiedevConfig): void;
21
24
  /** Exported for permission-focused tests; normal callers use writeConfig. */
@@ -64,11 +67,14 @@ export interface CapturePayloadInput {
64
67
  inputTokens?: number;
65
68
  outputTokens?: number;
66
69
  cacheTokens?: number;
70
+ cacheReadTokens?: number;
71
+ cacheCreationTokens?: number;
67
72
  model?: string | null;
68
73
  sessionStartedAt?: number | null;
69
74
  interruptCount?: number;
70
75
  rejectionCount?: number;
71
76
  final?: boolean;
77
+ costUsd?: number;
72
78
  }
73
79
  export declare function buildCapturePayload(input: CapturePayloadInput): Record<string, unknown>;
74
80
  export declare function hashToken(token: string): string;
@@ -77,6 +83,11 @@ export declare function resetStableNodeCacheForTests(): void;
77
83
  export declare function claudeCodeInstalled(): boolean;
78
84
  export declare function codexInstalled(): boolean;
79
85
  export declare function cursorInstalled(): boolean;
86
+ export declare function openCodeInstalled(): boolean;
87
+ /** Install a dormant global OpenCode adapter. Installing it unconditionally
88
+ * keeps `codiedev connect` as the only setup step even when Codie/OpenCode is
89
+ * installed later. OpenCode loads this file from its documented plugin dir. */
90
+ export declare function installOpenCodeCapturePlugin(): void;
80
91
  /**
81
92
  * VS Code with the GitHub Copilot Chat extension installed. The extension
82
93
  * directory pattern is `~/.vscode/extensions/github.copilot-chat-*`. We treat
@@ -158,16 +169,26 @@ export interface SteeringStats {
158
169
  }
159
170
  export declare function parseClaudeSteering(content: string): SteeringStats;
160
171
  export declare function parseCodexSteering(content: string): SteeringStats;
161
- export declare function parseSteering(content: string, mode: "claude-code" | "codex" | "cursor"): SteeringStats;
172
+ export declare function parseSteering(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode"): SteeringStats;
162
173
  export interface UsageStats {
163
174
  inputTokens: number;
164
175
  outputTokens: number;
176
+ /** Legacy fold of read + creation — kept for older servers. */
165
177
  cacheTokens: number;
178
+ cacheReadTokens: number;
179
+ cacheCreationTokens: number;
166
180
  model: string | null;
181
+ /** Engine-reported spend (opencode SDK `info.cost`). Desktop mode only. */
182
+ costUsd?: number;
167
183
  }
168
- export declare function parseUsage(content: string, mode: "claude-code" | "codex" | "cursor"): UsageStats;
184
+ export declare function parseUsage(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "desktop"): UsageStats;
169
185
  export declare function parseSessionStart(content: string): number | null;
170
186
  export declare function parseClaudeCodeStats(content: string): ParsedStats;
187
+ /** Convert OpenCode's SDK message array (`{info, parts}[]`) into the same
188
+ * newline-delimited shape used by Claude Code. The rest of capture—privacy
189
+ * exclusions, git keys, token parsing, scrubbing, storage, and merge joins—can
190
+ * therefore stay one shared pipeline instead of becoming a parallel product. */
191
+ export declare function normalizeOpenCodeTranscript(content: string): string;
171
192
  export declare function parseCodexStats(content: string): ParsedStats;
172
193
  export declare function extractCodexRepoUrl(content: string): string | null;
173
194
  export interface CaptureUploadJob {
@@ -299,4 +320,3 @@ interface DeferredUploadDeps extends UploadDeps {
299
320
  }
300
321
  export declare function runDeferredUpload(jobFile: string, delayMs: number, deps: DeferredUploadDeps): Promise<void>;
301
322
  export declare function transcriptReadSkipReason(err: unknown): string;
302
- export {};