@dsiloed/silo-link 1.11.0 → 1.12.0

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,280 @@
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { BaseTmuxLauncher } from './base-tmux-launcher.js';
5
+ import { locateCodexTranscript, parseCodexTranscript, codexCostBasis, } from './transcript-usage.js';
6
+ // Env var the codex MCP config reads the dsiloed bearer token from. Codex's
7
+ // `bearer_token_env_var` mechanism keeps the JWT OUT of config.toml on disk.
8
+ const DSILOED_TOKEN_ENV = 'SILOLINK_DSILOED_TOKEN';
9
+ /**
10
+ * Codex CLI (OpenAI) implementation of the AgentLauncher interface.
11
+ *
12
+ * Mirrors GeminiLauncher's tmux-based lifecycle with Codex-specific bits:
13
+ * - Command: `codex` (override via config.codex_command)
14
+ * - Auto-approval: `--dangerously-bypass-approvals-and-sandbox` so a headless
15
+ * tmux session never wedges on an approval/sandbox prompt (override via
16
+ * config.codex_args).
17
+ * - MCP: codex's NATIVE streamable-HTTP MCP support — `[mcp_servers.*]` tables
18
+ * with `url` (+ `bearer_token_env_var` for the dsiloed JWT) in
19
+ * `$CODEX_HOME/config.toml`. No `npx mcp-remote` bridge needed (unlike
20
+ * Claude/Gemini). We point CODEX_HOME at a per-env dir (so each daemon identity
21
+ * owns its own MCP config, the same isolation Claude/Gemini get via staging
22
+ * cwd) and copy the user's `auth.json` into it so the codex login carries over.
23
+ * - Instructions file: `AGENTS.md` (codex's analogue of CLAUDE.md / GEMINI.md).
24
+ * - MCP tool names: codex prefixes as `<server>__<tool>` (e.g.
25
+ * `silolink__remote_register`, `silolink_server__conversation_chat_tool`).
26
+ * - Usage: parsed from the rollout under ~/.codex/sessions (see transcript-usage).
27
+ *
28
+ * Validated against codex-cli 0.141.0:
29
+ * ✓ `--dangerously-bypass-approvals-and-sandbox` ✓ `codex resume <id>`
30
+ * ✓ `[mcp_servers.*]` url + bearer_token_env_var ✓ CODEX_HOME respected
31
+ * ✓ `<server>__<tool>` MCP tool naming (default; non_prefixed_mcp_tool_names off)
32
+ * Flags/command stay overridable via config for forward-compat across versions;
33
+ * the base launcher's pane watcher surfaces `error: unknown option …` on drift.
34
+ */
35
+ export class CodexLauncher extends BaseTmuxLauncher {
36
+ constructor(config, sessionManager, dsiloedClient, messageQueue, workspaceManager) {
37
+ super(config, sessionManager, dsiloedClient, messageQueue, workspaceManager);
38
+ }
39
+ // --- Abstract implementation ---
40
+ getCommand() {
41
+ return this.config.codex_command || 'codex';
42
+ }
43
+ // Empty — codex's arg ORDER matters (the `resume` subcommand must come first),
44
+ // so buildLaunchPrompt() owns the full arg list incl. the auto-approval flags.
45
+ getArgs() {
46
+ return [];
47
+ }
48
+ // codex-cli 0.141.0: validated flag (skips approvals + sandbox for headless run).
49
+ codexFlags() {
50
+ return this.config.codex_args ?? ['--dangerously-bypass-approvals-and-sandbox'];
51
+ }
52
+ // Best-effort match for the codex TUI's ready state. If a codex version prints
53
+ // none of these the base launcher still sends the prompt after its ready
54
+ // timeout, so a miss degrades to a small delay rather than a wedge.
55
+ getReadyIndicator() {
56
+ return ['Codex', 'codex', 'send a message', 'To get started', 'esc to interrupt', '▌'];
57
+ }
58
+ formatPrompt(prompt) {
59
+ return this.adaptPromptForCodex(prompt);
60
+ }
61
+ async setupMcpConfig(cwd, toolAllowlist) {
62
+ if (!this.config.jwt) {
63
+ throw new Error('No JWT in config. Run "silolink config" to log in.');
64
+ }
65
+ // The dsiloed MCP URL pointing at the same server SiloLink connects to.
66
+ let dsiloedUrl = `${this.config.host}/api/v1/mcp?categories=data,admin,conversation,utility`;
67
+ if (toolAllowlist && toolAllowlist.length > 0) {
68
+ const url = new URL(`${this.config.host}/api/v1/mcp`);
69
+ url.searchParams.set('tools', toolAllowlist.join(','));
70
+ dsiloedUrl = url.toString();
71
+ }
72
+ const codexHome = this.codexHome();
73
+ const configPath = join(codexHome, 'config.toml');
74
+ try {
75
+ mkdirSync(codexHome, { recursive: true });
76
+ this.ensureCodexAuth(codexHome);
77
+ // SiloLink OWNS this per-env config.toml — regenerate the MCP block each
78
+ // launch. Preserve any non-mcp_servers lines the user added, but drop prior
79
+ // [mcp_servers.*] tables so stale config can't linger. Codex talks to both
80
+ // servers over native streamable HTTP (no mcp-remote); the dsiloed JWT is
81
+ // read from an env var (DSILOED_TOKEN_ENV), never written to disk.
82
+ const preserved = existsSync(configPath)
83
+ ? stripMcpServers(readFileSync(configPath, 'utf-8'))
84
+ : '';
85
+ const mcpToml = [
86
+ '# Managed by SiloLink — MCP servers (regenerated each launch).',
87
+ '[mcp_servers.silolink]',
88
+ `url = ${tomlStr(`http://localhost:${this.config.mcp_port}/mcp`)}`,
89
+ '',
90
+ '[mcp_servers.silolink_server]',
91
+ `url = ${tomlStr(dsiloedUrl)}`,
92
+ `bearer_token_env_var = ${tomlStr(DSILOED_TOKEN_ENV)}`,
93
+ '',
94
+ ].join('\n');
95
+ const body = preserved.trim() ? `${preserved.trimEnd()}\n\n${mcpToml}` : mcpToml;
96
+ writeFileSync(configPath, body);
97
+ console.log(`[CodexLauncher] Configured MCP servers in ${configPath} (CODEX_HOME=${codexHome}, host: ${this.config.host}, cwd: ${cwd})`);
98
+ }
99
+ catch (err) {
100
+ console.error(`[CodexLauncher] Failed to configure ${configPath}: ${err}`);
101
+ }
102
+ }
103
+ checkEnabled() {
104
+ return !!this.config.projects_path;
105
+ }
106
+ getWorkingDirectory() {
107
+ // Mirror Claude/Gemini: prefer the per-env staging dir as the default cwd.
108
+ const cwd = this.config.staging_path || this.config.projects_path || process.cwd();
109
+ if (this.config.staging_path && !existsSync(cwd)) {
110
+ try {
111
+ mkdirSync(cwd, { recursive: true });
112
+ console.log(`[CodexLauncher] Created staging dir ${cwd}`);
113
+ }
114
+ catch (err) {
115
+ console.warn(`[CodexLauncher] Failed to create staging dir ${cwd}: ${err}`);
116
+ }
117
+ }
118
+ return cwd;
119
+ }
120
+ isAutoRespawnConfigured() {
121
+ return !!this.config.codex_auto_respawn;
122
+ }
123
+ getIdleTimeoutMs() {
124
+ return this.config.codex_idle_timeout_ms || 30000;
125
+ }
126
+ getIdleKillAfterMs() {
127
+ return this.config.codex_idle_kill_after_ms ?? 0;
128
+ }
129
+ // Codex's TUI treats a multi-line paste as interactive input; send prompts in
130
+ // bracketed-paste mode like Gemini to avoid tripping shell/slash handling.
131
+ usesBracketedPaste() {
132
+ return true;
133
+ }
134
+ getSessionPrompt() {
135
+ return this.config.codex_session_prompt;
136
+ }
137
+ getAgentEnv() {
138
+ return {
139
+ SILOLINK_MCP_URL: `http://localhost:${this.config.mcp_port}/mcp`,
140
+ SILOLINK_AGENT_PROVIDER: 'codex',
141
+ // Point codex at the per-env home so it reads OUR config.toml + the
142
+ // carried-over auth.json (and writes refreshed tokens there).
143
+ CODEX_HOME: this.codexHome(),
144
+ // The dsiloed MCP server's bearer token, referenced by config.toml's
145
+ // bearer_token_env_var — keeps the JWT out of the on-disk config.
146
+ [DSILOED_TOKEN_ENV]: this.config.jwt,
147
+ };
148
+ }
149
+ getTmuxPrefix() {
150
+ return 'silolink-codex';
151
+ }
152
+ getLogTag() {
153
+ return '[CodexLauncher]';
154
+ }
155
+ getCostMetadataKey() {
156
+ return 'codex_session_cost_usd';
157
+ }
158
+ // Token-level usage from the codex rollout on disk, located by the launch cwd
159
+ // (-> matching session_meta) + the resumed session id. cost_basis reflects the
160
+ // live codex auth mode (API key = real $, ChatGPT sign-in = imputed).
161
+ extractTranscriptUsage(cwd) {
162
+ const transcriptPath = locateCodexTranscript(cwd, this.lastSessionId);
163
+ if (!transcriptPath)
164
+ return null;
165
+ const usage = parseCodexTranscript(readFileSync(transcriptPath, 'utf-8'));
166
+ if (usage.length === 0)
167
+ return null;
168
+ return { cli_type: 'codex', cost_basis: codexCostBasis(), usage };
169
+ }
170
+ buildLaunchPrompt(reason, conversationId, agentContext, workspaceContext) {
171
+ const flags = this.codexFlags();
172
+ // codex-cli 0.141.0: `codex resume <SESSION_ID>` (UUID) accepts the sandbox/
173
+ // approval flags after it. Disabled by default (codex_resume_enabled) — a
174
+ // fresh session reconstructs context via remote_load_context, also fine.
175
+ const canResume = this.config.codex_resume_enabled && this.lastSessionId &&
176
+ (reason === 'process-exit' || reason === 'inbound-message');
177
+ if (canResume) {
178
+ return {
179
+ prompt: 'Continue polling SiloLink for messages.' + workspaceContext,
180
+ extraArgs: ['resume', this.lastSessionId, ...flags],
181
+ };
182
+ }
183
+ if (reason === 'process-exit' || reason === 'inbound-message') {
184
+ const agentIdParam = this.pendingLlmAgentId ? `, "llm_agent_id": ${this.pendingLlmAgentId}` : '';
185
+ this.pendingLlmAgentId = null;
186
+ const registerParams = conversationId
187
+ ? `{ "conversation_id": ${conversationId}${agentIdParam} }`
188
+ : `{${agentIdParam ? agentIdParam.substring(2) : ''}}`;
189
+ const triggerLine = this.pendingTriggerMessage
190
+ ? `Pending user message (address after load_context): "${this.pendingTriggerMessage}"`
191
+ : '';
192
+ this.pendingTriggerMessage = null;
193
+ return {
194
+ prompt: [
195
+ `Fresh session (no resume). Call silolink__remote_register with ${registerParams}, then follow the SiloLink section in AGENTS.md (call silolink__remote_load_context once, then enter the poll loop).`,
196
+ triggerLine,
197
+ agentContext,
198
+ workspaceContext,
199
+ ].filter(Boolean).join('\n'),
200
+ extraArgs: [...flags],
201
+ };
202
+ }
203
+ return {
204
+ prompt: (this.config.codex_session_prompt || this.config.claude_session_prompt ||
205
+ 'Register with SiloLink and enter the poll loop.') + agentContext + workspaceContext,
206
+ extraArgs: [...flags],
207
+ };
208
+ }
209
+ // --- Codex-specific ---
210
+ /**
211
+ * Per-env CODEX_HOME. When a staging_path is configured (per-env identity
212
+ * isolation), nest the codex home under it so each daemon owns its own
213
+ * config.toml + JWT; otherwise fall back to the user's default ~/.codex
214
+ * (single-identity machines).
215
+ */
216
+ codexHome() {
217
+ if (this.config.staging_path)
218
+ return join(this.config.staging_path, '.codex-home');
219
+ return process.env.CODEX_HOME || join(homedir(), '.codex');
220
+ }
221
+ /**
222
+ * Copy the user's codex auth (~/.codex/auth.json) into the per-env CODEX_HOME
223
+ * so the login carries over. No-op when not isolating (codexHome === ~/.codex)
224
+ * or when the source/target are the same. Failure-tolerant — codex will simply
225
+ * prompt for login if auth can't be copied.
226
+ */
227
+ ensureCodexAuth(codexHome) {
228
+ const defaultHome = join(homedir(), '.codex');
229
+ if (codexHome === defaultHome)
230
+ return;
231
+ const src = join(defaultHome, 'auth.json');
232
+ const dest = join(codexHome, 'auth.json');
233
+ try {
234
+ if (existsSync(src) && !existsSync(dest)) {
235
+ copyFileSync(src, dest);
236
+ console.log(`[CodexLauncher] Copied codex auth into ${dest}`);
237
+ }
238
+ }
239
+ catch (err) {
240
+ console.warn(`[CodexLauncher] Could not copy codex auth to ${dest}: ${err}`);
241
+ }
242
+ }
243
+ /**
244
+ * Adapt a prompt originally written for Claude to codex conventions. Codex
245
+ * names MCP tools `<server>__<tool>`, so strip the Claude `mcp__` prefix:
246
+ * mcp__silolink__remote_* -> silolink__remote_*
247
+ * mcp__silolink_server__<tool> -> silolink_server__<tool>
248
+ * and point at codex's instructions file (AGENTS.md).
249
+ */
250
+ adaptPromptForCodex(prompt) {
251
+ return prompt
252
+ .replace(/mcp__silolink/g, 'silolink')
253
+ .replace(/CLAUDE\.md/g, 'AGENTS.md');
254
+ }
255
+ }
256
+ /** Render a JS string as a TOML basic (double-quoted) string. */
257
+ function tomlStr(s) {
258
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
259
+ }
260
+ /**
261
+ * Remove every `[mcp_servers.*]` table (header line through the line before the
262
+ * next top-level `[` table or EOF) from a config.toml body, leaving all other
263
+ * content intact. Lets SiloLink own the MCP block without clobbering unrelated
264
+ * user config. Best-effort line scanner (no full TOML parse).
265
+ */
266
+ function stripMcpServers(toml) {
267
+ const lines = toml.split('\n');
268
+ const out = [];
269
+ let skipping = false;
270
+ for (const line of lines) {
271
+ const header = line.trim();
272
+ if (header.startsWith('[')) {
273
+ skipping = header.startsWith('[mcp_servers.') || header === '[mcp_servers]';
274
+ }
275
+ if (!skipping)
276
+ out.push(line);
277
+ }
278
+ return out.join('\n');
279
+ }
280
+ //# sourceMappingURL=codex-launcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-launcher.js","sourceRoot":"","sources":["../../src/core/codex-launcher.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@ import type { MessageQueue } from './message-queue.js';
4
4
  import type { DSiloedClient } from '../api/dsiloed-client.js';
5
5
  import type { WorkspaceManager } from './workspace-manager.js';
6
6
  import type { AgentLauncher } from './agent-launcher.js';
7
- export type AgentProvider = 'claude' | 'gemini' | 'openai';
7
+ export type AgentProvider = 'claude' | 'gemini' | 'openai' | 'codex';
8
8
  /**
9
9
  * Creates the appropriate AgentLauncher based on provider configuration.
10
10
  * Falls back to ClaudeLauncher if no provider is specified.
@@ -1 +1 @@
1
- {"version":3,"file":"launcher-factory.d.ts","sourceRoot":"","sources":["../../src/core/launcher-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAKzD,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE3D;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,aAAa,GAAG,SAAS,EACnC,MAAM,EAAE,cAAc,EACtB,cAAc,EAAE,cAAc,EAC9B,aAAa,CAAC,EAAE,aAAa,EAC7B,YAAY,CAAC,EAAE,YAAY,EAC3B,gBAAgB,CAAC,EAAE,gBAAgB,GAClC,aAAa,CAkBf"}
1
+ {"version":3,"file":"launcher-factory.d.ts","sourceRoot":"","sources":["../../src/core/launcher-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAMzD,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAErE;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,aAAa,GAAG,SAAS,EACnC,MAAM,EAAE,cAAc,EACtB,cAAc,EAAE,cAAc,EAC9B,aAAa,CAAC,EAAE,aAAa,EAC7B,YAAY,CAAC,EAAE,YAAY,EAC3B,gBAAgB,CAAC,EAAE,gBAAgB,GAClC,aAAa,CAkBf"}
@@ -1,6 +1,6 @@
1
1
  import { ClaudeLauncher } from './claude-launcher.js';
2
2
  import { GeminiLauncher } from './gemini-launcher.js';
3
- import { OpenAILauncher } from './openai-launcher.js';
3
+ import { CodexLauncher } from './codex-launcher.js';
4
4
  /**
5
5
  * Creates the appropriate AgentLauncher based on provider configuration.
6
6
  * Falls back to ClaudeLauncher if no provider is specified.
@@ -12,9 +12,9 @@ export function createLauncher(provider, config, sessionManager, dsiloedClient,
12
12
  return new ClaudeLauncher(config, sessionManager, dsiloedClient, messageQueue, workspaceManager);
13
13
  case 'gemini':
14
14
  return new GeminiLauncher(config, sessionManager, dsiloedClient, messageQueue, workspaceManager);
15
- case 'openai':
16
- console.log('[LauncherFactory] OpenAI provider selected (stub — not yet implemented)');
17
- return new OpenAILauncher();
15
+ case 'codex':
16
+ case 'openai': // alias
17
+ return new CodexLauncher(config, sessionManager, dsiloedClient, messageQueue, workspaceManager);
18
18
  default:
19
19
  console.warn(`[LauncherFactory] Unknown provider "${resolvedProvider}", falling back to Claude`);
20
20
  return new ClaudeLauncher(config, sessionManager, dsiloedClient, messageQueue, workspaceManager);
@@ -1 +1 @@
1
- {"version":3,"file":"launcher-factory.js","sourceRoot":"","sources":["../../src/core/launcher-factory.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAItD;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAmC,EACnC,MAAsB,EACtB,cAA8B,EAC9B,aAA6B,EAC7B,YAA2B,EAC3B,gBAAmC;IAEnC,MAAM,gBAAgB,GAAG,QAAQ,IAAI,MAAM,CAAC,cAAc,IAAI,QAAQ,CAAC;IAEvE,QAAQ,gBAAgB,EAAE,CAAC;QACzB,KAAK,QAAQ;YACX,OAAO,IAAI,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAAC;QAEnG,KAAK,QAAQ;YACX,OAAO,IAAI,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAAC;QAEnG,KAAK,QAAQ;YACX,OAAO,CAAC,GAAG,CAAC,yEAAyE,CAAC,CAAC;YACvF,OAAO,IAAI,cAAc,EAAE,CAAC;QAE9B;YACE,OAAO,CAAC,IAAI,CAAC,uCAAuC,gBAAgB,2BAA2B,CAAC,CAAC;YACjG,OAAO,IAAI,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAAC;IACrG,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"launcher-factory.js","sourceRoot":"","sources":["../../src/core/launcher-factory.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAKpD;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAmC,EACnC,MAAsB,EACtB,cAA8B,EAC9B,aAA6B,EAC7B,YAA2B,EAC3B,gBAAmC;IAEnC,MAAM,gBAAgB,GAAG,QAAQ,IAAI,MAAM,CAAC,cAAc,IAAI,QAAQ,CAAC;IAEvE,QAAQ,gBAAgB,EAAE,CAAC;QACzB,KAAK,QAAQ;YACX,OAAO,IAAI,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAAC;QAEnG,KAAK,QAAQ;YACX,OAAO,IAAI,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAAC;QAEnG,KAAK,OAAO,CAAC;QACb,KAAK,QAAQ,EAAE,QAAQ;YACrB,OAAO,IAAI,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAAC;QAElG;YACE,OAAO,CAAC,IAAI,CAAC,uCAAuC,gBAAgB,2BAA2B,CAAC,CAAC;YACjG,OAAO,IAAI,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAAC;IACrG,CAAC;AACH,CAAC"}
@@ -0,0 +1,51 @@
1
+ import type { SiloLinkConfig } from '../types/index.js';
2
+ import { type UsageEntry } from './transcript-usage.js';
3
+ export type CliType = 'claude' | 'gemini' | 'codex';
4
+ export interface CollectOptions {
5
+ cli: CliType;
6
+ /** Explicit transcript path (e.g. from a Claude Stop hook). Skips the locator. */
7
+ transcriptPath?: string | null;
8
+ /** The CLI's own session id, for locating the transcript + idempotent dedupe. */
9
+ sessionId?: string | null;
10
+ /** Working directory the session ran in (used by the locators). */
11
+ cwd?: string;
12
+ }
13
+ export interface CollectedUsage {
14
+ cli: CliType;
15
+ cost_basis: 'real' | 'imputed';
16
+ cli_session_id: string | null;
17
+ usage: UsageEntry[];
18
+ transcriptPath: string | null;
19
+ }
20
+ /**
21
+ * Locate + parse a coding-agent transcript into the per-model usage summary the
22
+ * report_coding_usage endpoint expects. Pure (no network) so it's unit-testable.
23
+ */
24
+ export declare function collectUsage(opts: CollectOptions): CollectedUsage;
25
+ export interface ReportResult {
26
+ posted: boolean;
27
+ status?: number;
28
+ recorded?: number;
29
+ skipped?: unknown[];
30
+ agent_session_id?: number;
31
+ message?: string;
32
+ collected: CollectedUsage;
33
+ }
34
+ /**
35
+ * Collect usage for a coding session and POST it to model_api's
36
+ * report_coding_usage endpoint, attributed to the configured user. Returns the
37
+ * collected usage even when nothing is posted (empty usage / dry run) so callers
38
+ * can log what happened. Never throws on HTTP failure — returns posted: false.
39
+ */
40
+ export declare function reportUsage(config: SiloLinkConfig, opts: CollectOptions & {
41
+ sessionName?: string;
42
+ dryRun?: boolean;
43
+ getToken: () => string;
44
+ }): Promise<ReportResult>;
45
+ /**
46
+ * Read a coding-agent hook's JSON payload from stdin (when invoked as a Stop /
47
+ * SessionEnd hook). Returns {} when there's no piped input or it isn't JSON.
48
+ * Claude Code passes { session_id, transcript_path, cwd, ... } on stdin.
49
+ */
50
+ export declare function readHookStdin(): Promise<Record<string, unknown>>;
51
+ //# sourceMappingURL=report-usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-usage.d.ts","sourceRoot":"","sources":["../../src/core/report-usage.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EACL,KAAK,UAAU,EAIhB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEpD,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,OAAO,CAAC;IACb,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,mEAAmE;IACnE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,OAAO,CAAC;IACb,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAoBD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,CAoBjE;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,cAAc,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,cAAc,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,MAAM,CAAA;CAAE,GACxF,OAAO,CAAC,YAAY,CAAC,CA4CvB;AAOD;;;;GAIG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAWtE"}
@@ -0,0 +1,112 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { basename } from 'node:path';
3
+ import { parseClaudeTranscript, parseGeminiTranscript, parseCodexTranscript, locateClaudeTranscript, locateGeminiTranscript, locateCodexTranscript, claudeCostBasis, geminiCostBasis, codexCostBasis, } from './transcript-usage.js';
4
+ const PARSERS = {
5
+ claude: parseClaudeTranscript,
6
+ gemini: parseGeminiTranscript,
7
+ codex: parseCodexTranscript,
8
+ };
9
+ const LOCATORS = {
10
+ claude: locateClaudeTranscript,
11
+ gemini: locateGeminiTranscript,
12
+ codex: locateCodexTranscript,
13
+ };
14
+ const COST_BASIS = {
15
+ claude: claudeCostBasis,
16
+ gemini: geminiCostBasis,
17
+ codex: codexCostBasis,
18
+ };
19
+ /**
20
+ * Locate + parse a coding-agent transcript into the per-model usage summary the
21
+ * report_coding_usage endpoint expects. Pure (no network) so it's unit-testable.
22
+ */
23
+ export function collectUsage(opts) {
24
+ const cli = opts.cli;
25
+ const cwd = opts.cwd || process.cwd();
26
+ const sessionId = opts.sessionId ?? null;
27
+ const transcriptPath = opts.transcriptPath && existsSync(opts.transcriptPath)
28
+ ? opts.transcriptPath
29
+ : LOCATORS[cli](cwd, sessionId);
30
+ const usage = transcriptPath && existsSync(transcriptPath)
31
+ ? PARSERS[cli](readFileSync(transcriptPath, 'utf8'))
32
+ : [];
33
+ return {
34
+ cli,
35
+ cost_basis: COST_BASIS[cli](),
36
+ cli_session_id: sessionId,
37
+ usage,
38
+ transcriptPath,
39
+ };
40
+ }
41
+ /**
42
+ * Collect usage for a coding session and POST it to model_api's
43
+ * report_coding_usage endpoint, attributed to the configured user. Returns the
44
+ * collected usage even when nothing is posted (empty usage / dry run) so callers
45
+ * can log what happened. Never throws on HTTP failure — returns posted: false.
46
+ */
47
+ export async function reportUsage(config, opts) {
48
+ const collected = collectUsage(opts);
49
+ if (collected.usage.length === 0) {
50
+ return { posted: false, message: 'No usage found in transcript', collected };
51
+ }
52
+ if (opts.dryRun) {
53
+ return { posted: false, message: 'Dry run — not posted', collected };
54
+ }
55
+ const body = {
56
+ cli_type: collected.cli,
57
+ cost_basis: collected.cost_basis,
58
+ cli_session_id: collected.cli_session_id,
59
+ session_name: opts.sessionName || defaultSessionName(opts.cli, opts.cwd || process.cwd()),
60
+ usage: collected.usage,
61
+ };
62
+ try {
63
+ const res = await fetch(`${config.host}/api/v1/silolink/report_coding_usage`, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ 'Authorization': `Bearer ${opts.getToken()}`,
68
+ 'Tenant-Id': config.tenant_id,
69
+ },
70
+ body: JSON.stringify(body),
71
+ });
72
+ const data = (await res.json().catch(() => ({})));
73
+ return {
74
+ posted: !!data.success,
75
+ status: res.status,
76
+ recorded: data.recorded,
77
+ skipped: data.skipped,
78
+ agent_session_id: data.agent_session_id,
79
+ message: data.message,
80
+ collected,
81
+ };
82
+ }
83
+ catch (err) {
84
+ return { posted: false, message: `POST failed: ${err.message}`, collected };
85
+ }
86
+ }
87
+ function defaultSessionName(cli, cwd) {
88
+ const label = { claude: 'Claude Code', gemini: 'Gemini', codex: 'Codex' }[cli];
89
+ return `${label} — ${basename(cwd) || cwd}`;
90
+ }
91
+ /**
92
+ * Read a coding-agent hook's JSON payload from stdin (when invoked as a Stop /
93
+ * SessionEnd hook). Returns {} when there's no piped input or it isn't JSON.
94
+ * Claude Code passes { session_id, transcript_path, cwd, ... } on stdin.
95
+ */
96
+ export async function readHookStdin() {
97
+ if (process.stdin.isTTY)
98
+ return {};
99
+ try {
100
+ const chunks = [];
101
+ for await (const chunk of process.stdin)
102
+ chunks.push(chunk);
103
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
104
+ if (!raw)
105
+ return {};
106
+ return JSON.parse(raw);
107
+ }
108
+ catch {
109
+ return {};
110
+ }
111
+ }
112
+ //# sourceMappingURL=report-usage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-usage.js","sourceRoot":"","sources":["../../src/core/report-usage.ts"],"names":[],"mappings":"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"}
@@ -55,6 +55,42 @@ export declare function claudeCostBasis(): 'real' | 'imputed';
55
55
  * the slug when the session id isn't known.
56
56
  */
57
57
  export declare function locateGeminiTranscript(cwd: string, sessionId: string | null): string | null;
58
+ /**
59
+ * Parse a Codex CLI rollout transcript (JSONL) into per-model token totals.
60
+ *
61
+ * Codex writes one rollout file per session under ~/.codex/sessions/YYYY/MM/DD/.
62
+ * Each line is `{ timestamp, type, payload }`. Token usage arrives in
63
+ * `token_count` event payloads carrying `info.total_token_usage` — a CUMULATIVE
64
+ * running total for the whole session (NOT a per-turn delta). So we take the
65
+ * LAST total_token_usage we see rather than summing (summing cumulative totals
66
+ * would massively overcount). The model is read from the session_meta /
67
+ * turn_context line(s); the most recent wins.
68
+ *
69
+ * Codex `TokenUsage` keys map straight onto model_api's OpenAI pricing
70
+ * convention (input_tokens is cache-INCLUSIVE, output_tokens is
71
+ * reasoning-INCLUSIVE — model_api subtracts the subsets itself), so we forward
72
+ * the raw cumulative values:
73
+ * input_tokens <- input_tokens (incl. cached)
74
+ * cache_read_input_tokens<- cached_input_tokens
75
+ * output_tokens <- output_tokens (incl. reasoning)
76
+ * reasoning_tokens <- reasoning_output_tokens
77
+ */
78
+ export declare function parseCodexTranscript(content: string): UsageEntry[];
79
+ /**
80
+ * Locate a Codex rollout transcript. Rollouts live under
81
+ * ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl. When sessionId is
82
+ * known, match it in the filename or the session_meta header; otherwise pick the
83
+ * newest rollout whose session_meta cwd matches, falling back to the newest
84
+ * rollout overall.
85
+ */
86
+ export declare function locateCodexTranscript(cwd: string, sessionId: string | null): string | null;
87
+ /**
88
+ * Codex billing basis: API key (env OPENAI_API_KEY, or an api-key entry in
89
+ * ~/.codex/auth.json) => real metered dollars; "Sign in with ChatGPT" (OAuth
90
+ * tokens, no api key) => subscription/imputed. Defaults to 'imputed' when the
91
+ * auth config can't be read.
92
+ */
93
+ export declare function codexCostBasis(): 'real' | 'imputed';
58
94
  /**
59
95
  * Real metered billing vs subscription-imputed, read from the live Gemini auth
60
96
  * config: an explicit API key means real dollars; OAuth (personal account)
@@ -1 +1 @@
1
- {"version":3,"file":"transcript-usage.d.ts","sourceRoot":"","sources":["../../src/core/transcript-usage.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,uBAAuB,EAAE,MAAM,CAAC;IAChC,gBAAgB,EAAE,MAAM,CAAC;IACzB,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAgCnE;AA4BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAgCnE;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAe3F;AAED,mFAAmF;AACnF,wBAAgB,eAAe,IAAI,MAAM,GAAG,SAAS,CAEpD;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAmC3F;AAUD;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,GAAG,SAAS,CAOpD"}
1
+ {"version":3,"file":"transcript-usage.d.ts","sourceRoot":"","sources":["../../src/core/transcript-usage.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,uBAAuB,EAAE,MAAM,CAAC;IAChC,gBAAgB,EAAE,MAAM,CAAC;IACzB,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAgCnE;AA4BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAgCnE;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAe3F;AAED,mFAAmF;AACnF,wBAAgB,eAAe,IAAI,MAAM,GAAG,SAAS,CAEpD;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAmC3F;AAMD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAqClE;AA2CD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAuC1F;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,MAAM,GAAG,SAAS,CAgBnD;AAUD;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,GAAG,SAAS,CAOpD"}