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/hook.js CHANGED
@@ -45,36 +45,7 @@ const utils_1 = require("./utils");
45
45
  const limitEvent_1 = require("./limitEvent");
46
46
  const version_1 = require("./version");
47
47
  const captureLog_1 = require("./captureLog");
48
- function getJson(url, bearerToken) {
49
- return new Promise((resolve, reject) => {
50
- const parsed = new URL(url);
51
- const options = {
52
- hostname: parsed.hostname,
53
- port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
54
- path: parsed.pathname + parsed.search,
55
- method: "GET",
56
- headers: {
57
- Authorization: `Bearer ${bearerToken}`,
58
- "X-Codiedev-Cli-Version": version_1.CLI_VERSION,
59
- },
60
- };
61
- const lib = parsed.protocol === "https:" ? https : http;
62
- const req = lib.request(options, (res) => {
63
- let body = "";
64
- res.on("data", (c) => (body += c));
65
- res.on("end", () => {
66
- try {
67
- resolve(JSON.parse(body));
68
- }
69
- catch {
70
- reject(new Error("bad json"));
71
- }
72
- });
73
- });
74
- req.on("error", reject);
75
- req.end();
76
- });
77
- }
48
+ const captureExclusions_1 = require("./captureExclusions");
78
49
  // Pure builder for the discovery additionalContext. Returns null when there are
79
50
  // no matches. The suggested `codiedev pull` command carries the EXACT top key
80
51
  // (e.g. "pr-push.md") rather than a "<key>" placeholder — otherwise agents
@@ -105,7 +76,10 @@ async function runDiscover(prompt) {
105
76
  if (!config)
106
77
  process.exit(0);
107
78
  try {
108
- 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);
109
83
  const skills = res?.skills ?? [];
110
84
  const additionalContext = buildDiscoveryContext(skills);
111
85
  if (additionalContext) {
@@ -176,7 +150,7 @@ async function postSessionSkip(config, sessionId, reason) {
176
150
  // so the backend can later compute captured ÷ started. Deliberately tiny — no
177
151
  // transcript read, no git — so it never slows session startup. Best-effort +
178
152
  // silent (SessionStart hook output is ignored by Claude Code).
179
- async function runSessionStart(hookInput) {
153
+ async function runSessionStart(hookInput, source = "claude-code") {
180
154
  const config = (0, utils_1.readConfig)();
181
155
  if (!config)
182
156
  process.exit(0);
@@ -185,7 +159,7 @@ async function runSessionStart(hookInput) {
185
159
  await postJson(`${config.backendUrl}/api/cli/sessionStart`, {
186
160
  sessionId: hookInput.session_id,
187
161
  startedAt: Date.now(),
188
- source: "claude-code",
162
+ source,
189
163
  }, config.token, PING_TIMEOUT_MS);
190
164
  }
191
165
  catch {
@@ -339,6 +313,10 @@ function resolveMode(arg) {
339
313
  return "codex";
340
314
  if (arg === "capture-cursor")
341
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";
342
320
  return "claude-code";
343
321
  }
344
322
  async function main() {
@@ -368,6 +346,12 @@ async function main() {
368
346
  // Write per-session upload state so the throttle can defer later
369
347
  // non-final uploads within the same window.
370
348
  writeSessionState: utils_1.writeSessionUploadState,
349
+ // Re-read local config in the uploader so adding an exclusion after a
350
+ // job was queued still prevents its transcript from leaving the device.
351
+ shouldSkipJob: (job) => {
352
+ const config = (0, utils_1.readConfig)();
353
+ return (0, captureExclusions_1.shouldSkipCaptureUploadJob)(job, config);
354
+ },
371
355
  };
372
356
  }
373
357
  // Detached uploader child (spawned by the capture path below). The job —
@@ -441,6 +425,13 @@ async function main() {
441
425
  catch {
442
426
  process.exit(0);
443
427
  }
428
+ // Local privacy boundary: every stdin-driven hook mode passes through this
429
+ // check BEFORE prompt discovery, limit-event tails, SessionStart pings,
430
+ // backup, self-heal, transcript reads, or detached dispatch. Ignored means
431
+ // no Codiedev request at all for this session.
432
+ if ((0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput)) {
433
+ process.exit(0);
434
+ }
444
435
  // UserPromptSubmit discovery path — separate from session capture.
445
436
  if (process.argv[2] === "discover") {
446
437
  await runDiscover(hookInput.prompt);
@@ -452,8 +443,8 @@ async function main() {
452
443
  return;
453
444
  }
454
445
  // SessionStart: post the capture-coverage denominator ping. Tiny + fast.
455
- if (process.argv[2] === "session-start") {
456
- 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");
457
448
  return;
458
449
  }
459
450
  // Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets
@@ -563,6 +554,17 @@ async function runCaptureFinish(input) {
563
554
  const config = (0, utils_1.readConfig)();
564
555
  if (!config)
565
556
  return;
557
+ // Re-check in the detached child: the user may have added an exclusion
558
+ // after the parent hook dispatched this job but before it started.
559
+ if ((0, captureExclusions_1.shouldIgnoreCaptureInput)(config, input)) {
560
+ (0, captureLog_1.appendCaptureLog)({
561
+ stage: "capture-finish",
562
+ sessionId: input.session_id,
563
+ outcome: "skip",
564
+ detail: "ignored-path",
565
+ });
566
+ return;
567
+ }
566
568
  const { session_id, transcript_path, cwd, workspace_roots } = input;
567
569
  if (!transcript_path) {
568
570
  await postSessionSkip(config, session_id, "no-transcript-path");
@@ -585,9 +587,15 @@ async function runCaptureFinish(input) {
585
587
  }
586
588
  async function runCaptureFinishInner(input, config, session_id, transcript_path, cwd, workspace_roots) {
587
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;
588
595
  const workspaceRoot = workspace_roots && workspace_roots.length > 0
589
596
  ? workspace_roots[0]
590
597
  : cwd || process.cwd();
598
+ const scopePaths = (0, captureExclusions_1.captureScopePaths)({ cwd, workspace_roots }, workspaceRoot);
591
599
  let transcriptContent;
592
600
  try {
593
601
  transcriptContent = fs.readFileSync(transcript_path, "utf8");
@@ -606,10 +614,19 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
606
614
  await postSessionSkip(config, session_id, reason);
607
615
  return;
608
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
+ }
609
626
  // Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
610
627
  // need the git remote of the workspace.
611
628
  let remoteUrl = null;
612
- if (mode === "codex") {
629
+ if (parserMode === "codex") {
613
630
  remoteUrl = (0, utils_1.extractCodexRepoUrl)(transcriptContent);
614
631
  }
615
632
  // The worktrees the session actually wrote to — resolved up front because
@@ -630,9 +647,9 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
630
647
  // Best-effort local match — backend re-resolves from `remoteUrl` against the
631
648
  // company's current repo list, so a stale/empty local cache doesn't matter.
632
649
  const matchedRepo = remoteUrl ? (0, utils_1.matchRepo)(remoteUrl, config.repos) : null;
633
- const stats = mode === "codex"
650
+ const stats = parserMode === "codex"
634
651
  ? (0, utils_1.parseCodexStats)(transcriptContent)
635
- : mode === "cursor"
652
+ : parserMode === "cursor"
636
653
  ? { messageCount: 0, toolCallCount: 0 }
637
654
  : (0, utils_1.parseClaudeCodeStats)(transcriptContent);
638
655
  const transcriptBase64 = Buffer.from(transcriptContent, "utf8").toString("base64");
@@ -662,10 +679,13 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
662
679
  const headShas = [...new Set([...keys.headShas, ...authoredShas])];
663
680
  const headSha = keys.headSha ?? headShas[0] ?? null;
664
681
  // Cost/velocity/steering signals parsed straight from the transcript we're
665
- // already holding — no extra hooks, no extra git/process calls.
666
- 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);
667
687
  const sessionStartedAt = (0, utils_1.parseSessionStart)(transcriptContent);
668
- const steering = (0, utils_1.parseSteering)(transcriptContent, mode);
688
+ const steering = (0, utils_1.parseSteering)(transcriptContent, parserMode);
669
689
  const payload = (0, utils_1.buildCapturePayload)({
670
690
  sessionId: session_id,
671
691
  transcriptBase64,
@@ -683,6 +703,8 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
683
703
  inputTokens: usage.inputTokens,
684
704
  outputTokens: usage.outputTokens,
685
705
  cacheTokens: usage.cacheTokens,
706
+ cacheReadTokens: usage.cacheReadTokens,
707
+ cacheCreationTokens: usage.cacheCreationTokens,
686
708
  model: usage.model,
687
709
  sessionStartedAt,
688
710
  interruptCount: steering.interrupts,
@@ -690,6 +712,8 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
690
712
  // Carry the final flag into the HTTP body — the server uses it to decide
691
713
  // whether to close the session lease (true) or renew it (false/absent).
692
714
  final: input.final,
715
+ // Desktop-only measured spend (undefined for every other source).
716
+ costUsd: usage.costUsd,
693
717
  });
694
718
  // Throttle decision: final captures go immediately; non-final (per-turn Stop)
695
719
  // are deferred if we uploaded this session recently, to avoid per-turn churn.
@@ -718,7 +742,12 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
718
742
  const hookScriptPath = process.argv[1];
719
743
  const tmpFile = path.join(os.tmpdir(), `codiedev-capture-${session_id ?? process.pid}.json`);
720
744
  if (decision.mode === "immediate") {
721
- (0, utils_1.dispatchCaptureDetached)({ url: `${config.backendUrl}/api/captureSession`, token: config.token, body: payload }, hookScriptPath, { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile, openSync: fs.openSync, closeSync: fs.closeSync });
745
+ (0, utils_1.dispatchCaptureDetached)({
746
+ url: `${config.backendUrl}/api/captureSession`,
747
+ token: config.token,
748
+ body: payload,
749
+ scopePaths,
750
+ }, hookScriptPath, { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile, openSync: fs.openSync, closeSync: fs.closeSync });
722
751
  // Retry any earlier capture jobs that failed to upload (transient network
723
752
  // blips), excluding the one we just dispatched. Best-effort, detached.
724
753
  // Skip the sweep on deferred path — the deferred child will handle uploading.
@@ -754,7 +783,12 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
754
783
  // Deferred: write the job file (newer turns overwrite = supersede), then
755
784
  // spawn the deferred-upload child which will sleep, claim, and upload.
756
785
  // Same stdio/fd-hygiene contract as dispatchCaptureDetached.
757
- (0, utils_1.dispatchDeferredUpload)({ url: `${config.backendUrl}/api/captureSession`, token: config.token, body: payload }, hookScriptPath, { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile, openSync: fs.openSync, closeSync: fs.closeSync }, decision.delayMs);
786
+ (0, utils_1.dispatchDeferredUpload)({
787
+ url: `${config.backendUrl}/api/captureSession`,
788
+ token: config.token,
789
+ body: payload,
790
+ scopePaths,
791
+ }, hookScriptPath, { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile, openSync: fs.openSync, closeSync: fs.closeSync }, decision.delayMs);
758
792
  }
759
793
  }
760
794
  catch (err) {
@@ -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;
@@ -12,9 +14,15 @@ export interface CodiedevConfig {
12
14
  }>;
13
15
  lastSynced: string;
14
16
  backendUrl: string;
17
+ deviceId?: string;
18
+ deviceName?: string;
19
+ captureIgnoredPaths?: string[];
15
20
  }
21
+ export declare const CONFIG_PATH: string;
16
22
  export declare function readConfig(): CodiedevConfig | null;
17
23
  export declare function writeConfig(config: CodiedevConfig): void;
24
+ /** Exported for permission-focused tests; normal callers use writeConfig. */
25
+ export declare function writeConfigAtPath(config: CodiedevConfig, configDir: string, configPath: string): void;
18
26
  export declare function parseGitRemoteFromConfig(configText: string): string | null;
19
27
  export declare function readGitRemoteFromConfig(startDir: string): string | null;
20
28
  export declare function getGitRemoteUrl(cwd: string): string | null;
@@ -59,11 +67,14 @@ export interface CapturePayloadInput {
59
67
  inputTokens?: number;
60
68
  outputTokens?: number;
61
69
  cacheTokens?: number;
70
+ cacheReadTokens?: number;
71
+ cacheCreationTokens?: number;
62
72
  model?: string | null;
63
73
  sessionStartedAt?: number | null;
64
74
  interruptCount?: number;
65
75
  rejectionCount?: number;
66
76
  final?: boolean;
77
+ costUsd?: number;
67
78
  }
68
79
  export declare function buildCapturePayload(input: CapturePayloadInput): Record<string, unknown>;
69
80
  export declare function hashToken(token: string): string;
@@ -72,6 +83,11 @@ export declare function resetStableNodeCacheForTests(): void;
72
83
  export declare function claudeCodeInstalled(): boolean;
73
84
  export declare function codexInstalled(): boolean;
74
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;
75
91
  /**
76
92
  * VS Code with the GitHub Copilot Chat extension installed. The extension
77
93
  * directory pattern is `~/.vscode/extensions/github.copilot-chat-*`. We treat
@@ -153,16 +169,26 @@ export interface SteeringStats {
153
169
  }
154
170
  export declare function parseClaudeSteering(content: string): SteeringStats;
155
171
  export declare function parseCodexSteering(content: string): SteeringStats;
156
- 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;
157
173
  export interface UsageStats {
158
174
  inputTokens: number;
159
175
  outputTokens: number;
176
+ /** Legacy fold of read + creation — kept for older servers. */
160
177
  cacheTokens: number;
178
+ cacheReadTokens: number;
179
+ cacheCreationTokens: number;
161
180
  model: string | null;
181
+ /** Engine-reported spend (opencode SDK `info.cost`). Desktop mode only. */
182
+ costUsd?: number;
162
183
  }
163
- 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;
164
185
  export declare function parseSessionStart(content: string): number | null;
165
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;
166
192
  export declare function parseCodexStats(content: string): ParsedStats;
167
193
  export declare function extractCodexRepoUrl(content: string): string | null;
168
194
  export interface CaptureUploadJob {
@@ -171,6 +197,7 @@ export interface CaptureUploadJob {
171
197
  body: Record<string, unknown>;
172
198
  attempts?: number;
173
199
  queuedAt?: number;
200
+ scopePaths?: string[];
174
201
  }
175
202
  export declare const MAX_UPLOAD_ATTEMPTS = 5;
176
203
  export declare const UPLOAD_TTL_MS: number;
@@ -262,6 +289,7 @@ interface UploadDeps {
262
289
  recordSuccess?: (job: CaptureUploadJob) => void;
263
290
  log?: (entry: CaptureLogEntry) => void;
264
291
  writeSessionState?: (sessionId: string, state: SessionUploadState) => void;
292
+ shouldSkipJob?: (job: CaptureUploadJob) => boolean;
265
293
  }
266
294
  export interface LastCaptureRecord {
267
295
  sessionId: string | undefined;
@@ -292,4 +320,3 @@ interface DeferredUploadDeps extends UploadDeps {
292
320
  }
293
321
  export declare function runDeferredUpload(jobFile: string, delayMs: number, deps: DeferredUploadDeps): Promise<void>;
294
322
  export declare function transcriptReadSkipReason(err: unknown): string;
295
- export {};