@yul-labs/agent-relay 0.1.1 → 0.1.3

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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import fs3, { promises, constants, statSync, chmodSync } from 'fs';
2
- import path7 from 'path';
2
+ import path8 from 'path';
3
3
  import { z } from 'zod';
4
4
  import { randomUUID } from 'crypto';
5
5
  import { execa } from 'execa';
@@ -92,7 +92,7 @@ function resolveApprovalMode(defaults, adapter) {
92
92
  return adapter?.approvalPolicy ?? defaults.approvalPolicy ?? (defaults.requireApprovalOnRiskyActions ? "gated" : "auto");
93
93
  }
94
94
  function resolveSandbox(defaults, adapter) {
95
- return adapter?.sandbox ?? defaults.sandbox ?? "workspace-write";
95
+ return adapter?.sandbox ?? defaults.sandbox ?? "danger-full-access";
96
96
  }
97
97
  var hooksSchema = z.object({
98
98
  /** Shell command run just before the agent starts. */
@@ -119,6 +119,14 @@ var deciderSchema = z.object({
119
119
  apiKey: z.string().optional(),
120
120
  maxTokens: z.number().int().positive().optional()
121
121
  }).strict();
122
+ var pricingRuleSchema = z.object({
123
+ /** Regex (case-insensitive) matched against the model id; first match wins. */
124
+ match: z.string().min(1),
125
+ input: z.number().nonnegative(),
126
+ output: z.number().nonnegative(),
127
+ cacheWrite: z.number().nonnegative().optional(),
128
+ cacheRead: z.number().nonnegative().optional()
129
+ }).strict();
122
130
  var configSchema = z.object({
123
131
  defaultAdapter: z.string().min(1),
124
132
  sessionsDir: z.string().min(1).default(".agent-relay/sessions"),
@@ -127,6 +135,8 @@ var configSchema = z.object({
127
135
  adapters: z.record(adapterConfigSchema),
128
136
  /** Which decider answers interactive prompts. */
129
137
  decider: deciderSchema.optional(),
138
+ /** Override the built-in per-model token pricing (USD per 1M tokens). */
139
+ pricing: z.array(pricingRuleSchema).optional(),
130
140
  /** Optional shell-command lifecycle hooks. */
131
141
  hooks: hooksSchema.optional()
132
142
  }).strict().superRefine((cfg, ctx) => {
@@ -147,9 +157,10 @@ function createDefaultConfig() {
147
157
  maxTurns: 20,
148
158
  timeoutMs: 18e5,
149
159
  idleTimeoutMs: 3e5,
150
- // Autonomous by default: agents run unattended without asking.
160
+ // Autonomous by default: agents run unattended without asking, with full
161
+ // bypass so they can act freely (tighten `sandbox` for guarded runs).
151
162
  approvalPolicy: "auto",
152
- sandbox: "workspace-write",
163
+ sandbox: "danger-full-access",
153
164
  requireApprovalOnRiskyActions: false
154
165
  },
155
166
  // Interactive (PTY) is the default mode: the agent runs in its real TUI and
@@ -187,7 +198,7 @@ ${detail}`,
187
198
  return result.data;
188
199
  }
189
200
  function configPath(rootDir) {
190
- return path7.resolve(rootDir, CONFIG_FILENAME);
201
+ return path8.resolve(rootDir, CONFIG_FILENAME);
191
202
  }
192
203
  async function loadConfig(rootDir) {
193
204
  const file = configPath(rootDir);
@@ -227,10 +238,31 @@ function stringifyConfig(config) {
227
238
  }
228
239
  async function saveConfig(rootDir, config) {
229
240
  const file = configPath(rootDir);
230
- await promises.mkdir(path7.dirname(file), { recursive: true });
241
+ await promises.mkdir(path8.dirname(file), { recursive: true });
231
242
  await promises.writeFile(file, stringifyConfig(config), "utf8");
232
243
  return file;
233
244
  }
245
+
246
+ // src/core/pricing.ts
247
+ var DEFAULT_PRICING = [
248
+ { match: /opus/i, pricing: { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
249
+ { match: /sonnet/i, pricing: { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 } },
250
+ { match: /haiku/i, pricing: { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 } }
251
+ ];
252
+ function pricingForModel(model, overrides = []) {
253
+ if (!model) return void 0;
254
+ for (const rule of [...overrides, ...DEFAULT_PRICING]) {
255
+ if (rule.match.test(model)) return rule.pricing;
256
+ }
257
+ return void 0;
258
+ }
259
+ function computeCostUsd(usage, overrides = []) {
260
+ const p = pricingForModel(usage.model, overrides);
261
+ if (!p) return null;
262
+ const M = 1e6;
263
+ const cost = ((usage.inputTokens ?? 0) * p.input + (usage.outputTokens ?? 0) * p.output + (usage.cacheCreationTokens ?? 0) * (p.cacheWrite ?? p.input) + (usage.cachedInputTokens ?? 0) * (p.cacheRead ?? 0)) / M;
264
+ return Math.round(cost * 1e6) / 1e6;
265
+ }
234
266
  var SessionManager = class {
235
267
  constructor(sessionsDir) {
236
268
  this.sessionsDir = sessionsDir;
@@ -238,7 +270,7 @@ var SessionManager = class {
238
270
  sessionsDir;
239
271
  /** Absolute path to a session's JSON metadata file. */
240
272
  filePath(sessionId) {
241
- return path7.join(this.sessionsDir, `${sessionId}.json`);
273
+ return path8.join(this.sessionsDir, `${sessionId}.json`);
242
274
  }
243
275
  /** Build a fresh session record in the `created` state (not yet persisted). */
244
276
  create(input) {
@@ -305,7 +337,7 @@ var SessionManager = class {
305
337
  if (!entry.endsWith(".json")) continue;
306
338
  try {
307
339
  const raw = await promises.readFile(
308
- path7.join(this.sessionsDir, entry),
340
+ path8.join(this.sessionsDir, entry),
309
341
  "utf8"
310
342
  );
311
343
  metas.push(JSON.parse(raw));
@@ -345,7 +377,7 @@ var RunLogger = class {
345
377
  }
346
378
  /** Ensure the log directory exists and write the run header. */
347
379
  start(header) {
348
- fs3.mkdirSync(path7.dirname(this.logFile), { recursive: true });
380
+ fs3.mkdirSync(path8.dirname(this.logFile), { recursive: true });
349
381
  const lines = [
350
382
  "================ agent-relay run ================",
351
383
  `sessionId : ${header.sessionId}`,
@@ -684,6 +716,7 @@ var CommandDecider = class {
684
716
  }
685
717
  }
686
718
  };
719
+ var MIN_API_MAX_TOKENS = 512;
687
720
  var ApiDecider = class {
688
721
  constructor(opts) {
689
722
  this.opts = opts;
@@ -713,7 +746,7 @@ var ApiDecider = class {
713
746
  body: JSON.stringify({
714
747
  model: this.opts.model ?? "default",
715
748
  messages: [{ role: "user", content: render(req) }],
716
- max_tokens: this.opts.maxTokens ?? 2048,
749
+ max_tokens: Math.max(this.opts.maxTokens ?? 2048, MIN_API_MAX_TOKENS),
717
750
  temperature: this.opts.temperature ?? 0,
718
751
  stream: false
719
752
  }),
@@ -807,12 +840,12 @@ async function runAgent(options) {
807
840
  }
808
841
  const adapter = options.resolveAdapter(adapterName, config);
809
842
  const detector = options.detector ?? new CompositeCompletionDetector();
810
- const rootDir = path7.resolve(options.rootDir);
811
- const cwd = path7.resolve(options.cwd ?? rootDir);
812
- const sessionsDir = path7.resolve(rootDir, config.sessionsDir);
813
- const logsDir = path7.resolve(rootDir, config.logsDir);
843
+ const rootDir = path8.resolve(options.rootDir);
844
+ const cwd = path8.resolve(options.cwd ?? rootDir);
845
+ const sessionsDir = path8.resolve(rootDir, config.sessionsDir);
846
+ const logsDir = path8.resolve(rootDir, config.logsDir);
814
847
  const sessionId = options.sessionId ?? randomUUID();
815
- const logFile = path7.join(logsDir, `${sessionId}.log`);
848
+ const logFile = path8.join(logsDir, `${sessionId}.log`);
816
849
  const sessions = new SessionManager(sessionsDir);
817
850
  const startedAt = iso();
818
851
  const session = sessions.create({
@@ -1030,6 +1063,28 @@ async function runAgent(options) {
1030
1063
  }
1031
1064
  externalSignal?.removeEventListener("abort", onExternalAbort);
1032
1065
  }
1066
+ if (result?.meta && typeof result.meta === "object") {
1067
+ const usage = result.meta.usage;
1068
+ if (usage && (usage.inputTokens != null || usage.outputTokens != null)) {
1069
+ const overrides = (config.pricing ?? []).map((r) => ({
1070
+ match: new RegExp(r.match, "i"),
1071
+ pricing: {
1072
+ input: r.input,
1073
+ output: r.output,
1074
+ cacheWrite: r.cacheWrite,
1075
+ cacheRead: r.cacheRead
1076
+ }
1077
+ }));
1078
+ usage.costUsd = computeCostUsd(usage, overrides);
1079
+ if (usage.costUsd === null) {
1080
+ onEvent({
1081
+ type: "status",
1082
+ timestamp: iso(),
1083
+ text: `usage: no price for model ${usage.model ? `"${usage.model}"` : "(unknown)"} \u2192 costUsd=null (set config.pricing to override)`
1084
+ });
1085
+ }
1086
+ }
1087
+ }
1033
1088
  const status = detector.detect({ result, events, abortReason, error: runError }) ?? "failed";
1034
1089
  const endedAt = iso();
1035
1090
  const error = runError && !result?.error ? { message: runError.message, code: "ADAPTER_THREW" } : result?.error ?? (abortReason === "timeout" || abortReason === "idle" ? {
@@ -1234,8 +1289,8 @@ async function isExecutableFile(file) {
1234
1289
  async function which(command) {
1235
1290
  if (!command) return null;
1236
1291
  const exts = pathExtensions();
1237
- if (command.includes(path7.sep) || command.includes("/")) {
1238
- const base = path7.resolve(command);
1292
+ if (command.includes(path8.sep) || command.includes("/")) {
1293
+ const base = path8.resolve(command);
1239
1294
  for (const ext of exts) {
1240
1295
  const candidate = base + ext;
1241
1296
  if (await isExecutableFile(candidate)) return candidate;
@@ -1243,11 +1298,11 @@ async function which(command) {
1243
1298
  return null;
1244
1299
  }
1245
1300
  const pathEnv = process.env.PATH ?? process.env.Path ?? "";
1246
- const dirs = pathEnv.split(path7.delimiter).filter(Boolean);
1247
- const fallbacks = process.platform === "win32" ? [] : ["/usr/local/bin", "/opt/homebrew/bin", path7.join(os.homedir(), ".local/bin")];
1301
+ const dirs = pathEnv.split(path8.delimiter).filter(Boolean);
1302
+ const fallbacks = process.platform === "win32" ? [] : ["/usr/local/bin", "/opt/homebrew/bin", path8.join(os.homedir(), ".local/bin")];
1248
1303
  for (const dir of [...dirs, ...fallbacks]) {
1249
1304
  for (const ext of exts) {
1250
- const candidate = path7.join(dir, command + ext);
1305
+ const candidate = path8.join(dir, command + ext);
1251
1306
  if (await isExecutableFile(candidate)) return candidate;
1252
1307
  }
1253
1308
  }
@@ -1373,11 +1428,11 @@ function ensurePtyHelperExecutable() {
1373
1428
  if (process.platform === "win32") return;
1374
1429
  try {
1375
1430
  const require2 = createRequire(import.meta.url);
1376
- const root = path7.dirname(require2.resolve("node-pty/package.json"));
1431
+ const root = path8.dirname(require2.resolve("node-pty/package.json"));
1377
1432
  const candidates = [
1378
- path7.join(root, "build", "Release", "spawn-helper"),
1379
- path7.join(root, "build", "Debug", "spawn-helper"),
1380
- path7.join(
1433
+ path8.join(root, "build", "Release", "spawn-helper"),
1434
+ path8.join(root, "build", "Debug", "spawn-helper"),
1435
+ path8.join(
1381
1436
  root,
1382
1437
  "prebuilds",
1383
1438
  `${process.platform}-${process.arch}`,
@@ -1539,6 +1594,7 @@ function runPtySession(opts, ctx) {
1539
1594
  let handling = false;
1540
1595
  let quitting = false;
1541
1596
  let settled = false;
1597
+ let usage;
1542
1598
  let finished = false;
1543
1599
  let disposeData;
1544
1600
  let disposeExit;
@@ -1555,6 +1611,7 @@ function runPtySession(opts, ctx) {
1555
1611
  const triggerQuit = (reason) => {
1556
1612
  if (finished || quitting) return;
1557
1613
  quitting = true;
1614
+ settled = true;
1558
1615
  if (completionTimer) clearTimeout(completionTimer);
1559
1616
  completionTimer = void 0;
1560
1617
  emit("status", `task appears complete (${reason})`);
@@ -1634,6 +1691,10 @@ function runPtySession(opts, ctx) {
1634
1691
  };
1635
1692
  const onSettle = async () => {
1636
1693
  if (finished || handling || quitting) return;
1694
+ if (opts.scrapeUsage) {
1695
+ const u = opts.scrapeUsage(cleanTerminalText(buffer));
1696
+ if (u) usage = { ...usage, ...u };
1697
+ }
1637
1698
  if (pendingComment !== void 0) {
1638
1699
  const comment = pendingComment;
1639
1700
  pendingComment = void 0;
@@ -1770,7 +1831,14 @@ function runPtySession(opts, ctx) {
1770
1831
  exitCode,
1771
1832
  error,
1772
1833
  sessionRef: void 0,
1773
- meta: { interactions, settled }
1834
+ // `completedCleanly` is an unambiguous success flag (exit 0, not aborted/
1835
+ // error) — use it instead of `settled` for "did the run finish well?".
1836
+ meta: {
1837
+ interactions,
1838
+ settled,
1839
+ completedCleanly: success,
1840
+ ...usage ? { usage } : {}
1841
+ }
1774
1842
  });
1775
1843
  });
1776
1844
  if (ctx.signal.aborted) onAbort();
@@ -1870,8 +1938,17 @@ var InteractivePtyAdapter = class {
1870
1938
  else args.push(input.prompt);
1871
1939
  return this.spawn(args, initialInput, input, ctx);
1872
1940
  }
1873
- spawn(args, initialInput, input, ctx, extra) {
1941
+ async spawn(args, initialInput, input, ctx, extra) {
1874
1942
  const decider = ctx.decider ?? new RuleDecider();
1943
+ try {
1944
+ const resolved = await which(this.cfg.command);
1945
+ ctx.onEvent({
1946
+ type: "status",
1947
+ timestamp: (this.cfg.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
1948
+ text: resolved ? `resolved ${this.cfg.command} \u2192 ${resolved}` : `${this.cfg.command} not found on PATH; relying on spawn-time resolution`
1949
+ });
1950
+ } catch {
1951
+ }
1875
1952
  return runPtySession(
1876
1953
  {
1877
1954
  command: this.cfg.command,
@@ -1884,6 +1961,7 @@ var InteractivePtyAdapter = class {
1884
1961
  completionPattern: this.cfg.completionPattern,
1885
1962
  completionIdleMs: input.completionIdleMs ?? this.cfg.completionIdleMs,
1886
1963
  workingPattern: this.cfg.workingPattern,
1964
+ scrapeUsage: this.cfg.scrapeUsage,
1887
1965
  quitKeys: this.cfg.quitKeys,
1888
1966
  setup: extra?.setup,
1889
1967
  promptAfterSetup: extra?.promptAfterSetup,
@@ -1899,8 +1977,113 @@ var InteractivePtyAdapter = class {
1899
1977
  );
1900
1978
  }
1901
1979
  };
1980
+ async function realish(p) {
1981
+ try {
1982
+ return await promises.realpath(p);
1983
+ } catch {
1984
+ return p;
1985
+ }
1986
+ }
1987
+ function projectDirCandidates(absCwd) {
1988
+ return [absCwd.replace(/[/.]/g, "-"), absCwd.replace(/\//g, "-")];
1989
+ }
1990
+ async function resolveProjectDir(root, cwd) {
1991
+ const abs = await realish(cwd);
1992
+ for (const name of projectDirCandidates(abs)) {
1993
+ const dir = path8.join(root, name);
1994
+ try {
1995
+ const st = await promises.stat(dir);
1996
+ if (st.isDirectory()) return dir;
1997
+ } catch {
1998
+ }
1999
+ }
2000
+ return void 0;
2001
+ }
2002
+ async function findLatestClaudeTranscript(opts) {
2003
+ const root = opts.projectsDir ?? path8.join(os.homedir(), ".claude", "projects");
2004
+ const since = opts.sinceMs ?? 0;
2005
+ const dir = await resolveProjectDir(root, opts.cwd);
2006
+ if (!dir) return void 0;
2007
+ let files;
2008
+ try {
2009
+ files = await promises.readdir(dir);
2010
+ } catch {
2011
+ return void 0;
2012
+ }
2013
+ const candidates = [];
2014
+ for (const f of files) {
2015
+ if (!f.endsWith(".jsonl")) continue;
2016
+ const file = path8.join(dir, f);
2017
+ try {
2018
+ const st = await promises.stat(file);
2019
+ if (st.mtimeMs + 2e3 < since) continue;
2020
+ candidates.push({ file, mtimeMs: st.mtimeMs });
2021
+ } catch {
2022
+ }
2023
+ }
2024
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
2025
+ return candidates[0]?.file;
2026
+ }
2027
+ async function sumClaudeUsage(file) {
2028
+ let text;
2029
+ try {
2030
+ text = await promises.readFile(file, "utf8");
2031
+ } catch {
2032
+ return void 0;
2033
+ }
2034
+ let input = 0;
2035
+ let output = 0;
2036
+ let cacheCreate = 0;
2037
+ let cacheRead = 0;
2038
+ let model;
2039
+ let any = false;
2040
+ for (const line of text.split("\n")) {
2041
+ if (!line.trim()) continue;
2042
+ let rec;
2043
+ try {
2044
+ rec = JSON.parse(line);
2045
+ } catch {
2046
+ continue;
2047
+ }
2048
+ const u = rec.message?.usage;
2049
+ if (!u || typeof u !== "object") continue;
2050
+ any = true;
2051
+ input += u.input_tokens ?? 0;
2052
+ output += u.output_tokens ?? 0;
2053
+ cacheCreate += u.cache_creation_input_tokens ?? 0;
2054
+ cacheRead += u.cache_read_input_tokens ?? 0;
2055
+ if (!model && typeof rec.message?.model === "string")
2056
+ model = rec.message.model;
2057
+ }
2058
+ if (!any) return void 0;
2059
+ return {
2060
+ source: "transcript",
2061
+ model,
2062
+ inputTokens: input,
2063
+ outputTokens: output,
2064
+ cacheCreationTokens: cacheCreate,
2065
+ cachedInputTokens: cacheRead,
2066
+ totalTokens: input + output + cacheCreate + cacheRead
2067
+ };
2068
+ }
2069
+ async function findLatestClaudeUsage(opts) {
2070
+ const file = await findLatestClaudeTranscript(opts);
2071
+ if (!file) return void 0;
2072
+ return sumClaudeUsage(file);
2073
+ }
1902
2074
 
1903
2075
  // src/adapters/interactive/claude-interactive.ts
2076
+ function scrapeClaudeStatusLine(text) {
2077
+ const u = { source: "status-line" };
2078
+ const ctx = text.match(/context[^\n]*?(\d{1,3})\s*%/i);
2079
+ if (ctx) {
2080
+ u.contextPercent = Number(ctx[1]);
2081
+ u.raw = ctx[0].trim().replace(/\s+/g, " ");
2082
+ }
2083
+ const cost = text.match(/session\s*\$\s*([\d.]+)/i);
2084
+ if (cost) u.subscriptionSessionCostUsd = Number(cost[1]);
2085
+ return u.contextPercent !== void 0 || u.subscriptionSessionCostUsd !== void 0 ? u : void 0;
2086
+ }
1904
2087
  var DEFINITION2 = {
1905
2088
  name: "claude",
1906
2089
  type: "claude",
@@ -1909,6 +2092,9 @@ var DEFINITION2 = {
1909
2092
  supportsResume: true
1910
2093
  };
1911
2094
  var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends InteractivePtyAdapter {
2095
+ clock;
2096
+ /** Override the projects root (~/.claude/projects) for tests. */
2097
+ projectsDir;
1912
2098
  constructor(opts = {}) {
1913
2099
  super({
1914
2100
  definition: DEFINITION2,
@@ -1925,14 +2111,14 @@ var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends Interacti
1925
2111
  if (input.approvalMode === "readonly")
1926
2112
  return [...head, "--permission-mode", "plan"];
1927
2113
  if (input.approvalMode === "gated")
1928
- return [...head, "--permission-mode", "acceptEdits"];
2114
+ return [...head, "--permission-mode", "default"];
1929
2115
  return [...head, "--dangerously-skip-permissions"];
1930
2116
  },
1931
2117
  // Resume the most recent conversation in the cwd and send the follow-up
1932
2118
  // prompt. (`--continue` picks the latest session; the native id is not
1933
2119
  // captured in PTY mode, so a specific `--resume <id>` is not used.)
1934
2120
  resumeArgs: (input) => {
1935
- const mode = input.approvalMode === "readonly" ? ["--permission-mode", "plan"] : input.approvalMode === "gated" ? ["--permission-mode", "acceptEdits"] : ["--dangerously-skip-permissions"];
2121
+ const mode = input.approvalMode === "readonly" ? ["--permission-mode", "plan"] : input.approvalMode === "gated" ? ["--permission-mode", "default"] : ["--dangerously-skip-permissions"];
1936
2122
  return ["--continue", "--effort", "xhigh", ...mode];
1937
2123
  },
1938
2124
  // Unattended ultracode: once Claude is idle (past the trust dialog), type
@@ -1946,6 +2132,10 @@ var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends Interacti
1946
2132
  approvalPattern: /(allow|grant|permission|approve|trust|proceed|do you want)[^\n]{0,60}\?|\[y\/n\]|\(y\/n\)/i
1947
2133
  },
1948
2134
  keymap: new ArrowKeymap(),
2135
+ // SUPPLEMENT only: grab context% / session $ from the status line when it
2136
+ // is enabled. The authoritative token counts come from the transcript in
2137
+ // run() below and overwrite these on the same meta.usage object.
2138
+ scrapeUsage: scrapeClaudeStatusLine,
1949
2139
  // Claude's TUI stays open after a task; quit once it's been idle a while.
1950
2140
  completionIdleMs: 8e3,
1951
2141
  // While Claude is ACTIVELY working it shows "(esc to interrupt)" — that is
@@ -1959,6 +2149,32 @@ var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends Interacti
1959
2149
  workingPattern: /interrupt/i,
1960
2150
  quitKeys: ""
1961
2151
  });
2152
+ this.clock = opts.now ?? (() => /* @__PURE__ */ new Date());
2153
+ this.projectsDir = opts.projectsDir;
2154
+ }
2155
+ /**
2156
+ * Run Claude, then read AUTHORITATIVE token usage from its session transcript
2157
+ * (~/.claude/projects/<cwd>/<id>.jsonl) and surface it as `meta.usage`. This is
2158
+ * device-independent — it works regardless of whether the user has a usage
2159
+ * status line — and overwrites the best-effort status-line scrape's token
2160
+ * figures while keeping its context%/cost extras. Best-effort: if no transcript
2161
+ * is found, the status-line usage (if any) is left as-is.
2162
+ */
2163
+ async run(input, ctx) {
2164
+ const startedAt = this.clock().getTime();
2165
+ const result = await super.run(input, ctx);
2166
+ if (result.error) return result;
2167
+ const transcript = await findLatestClaudeUsage({
2168
+ cwd: input.cwd,
2169
+ sinceMs: startedAt,
2170
+ projectsDir: this.projectsDir
2171
+ });
2172
+ if (!transcript) return result;
2173
+ const prior = result.meta?.usage ?? {};
2174
+ return {
2175
+ ...result,
2176
+ meta: { ...result.meta, usage: { ...prior, ...transcript } }
2177
+ };
1962
2178
  }
1963
2179
  static fromConfig(config) {
1964
2180
  return new _ClaudeInteractiveAdapter({
@@ -1968,7 +2184,7 @@ var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends Interacti
1968
2184
  }
1969
2185
  };
1970
2186
  var ROLLOUT_UUID_RE = /-([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\.jsonl$/;
1971
- async function realish(p) {
2187
+ async function realish2(p) {
1972
2188
  try {
1973
2189
  return await promises.realpath(p);
1974
2190
  } catch {
@@ -1984,7 +2200,7 @@ async function listRollouts(root) {
1984
2200
  return out;
1985
2201
  }
1986
2202
  for (const y of years) {
1987
- const yDir = path7.join(root, y);
2203
+ const yDir = path8.join(root, y);
1988
2204
  let months;
1989
2205
  try {
1990
2206
  months = await promises.readdir(yDir);
@@ -1992,7 +2208,7 @@ async function listRollouts(root) {
1992
2208
  continue;
1993
2209
  }
1994
2210
  for (const m of months) {
1995
- const mDir = path7.join(yDir, m);
2211
+ const mDir = path8.join(yDir, m);
1996
2212
  let days;
1997
2213
  try {
1998
2214
  days = await promises.readdir(mDir);
@@ -2000,7 +2216,7 @@ async function listRollouts(root) {
2000
2216
  continue;
2001
2217
  }
2002
2218
  for (const d of days) {
2003
- const dDir = path7.join(mDir, d);
2219
+ const dDir = path8.join(mDir, d);
2004
2220
  let files;
2005
2221
  try {
2006
2222
  files = await promises.readdir(dDir);
@@ -2009,7 +2225,7 @@ async function listRollouts(root) {
2009
2225
  }
2010
2226
  for (const f of files) {
2011
2227
  if (!f.startsWith("rollout-") || !f.endsWith(".jsonl")) continue;
2012
- const file = path7.join(dDir, f);
2228
+ const file = path8.join(dDir, f);
2013
2229
  try {
2014
2230
  const st = await promises.stat(file);
2015
2231
  out.push({ file, mtimeMs: st.mtimeMs });
@@ -2042,25 +2258,55 @@ async function readSessionMeta(file) {
2042
2258
  }
2043
2259
  }
2044
2260
  function uuidFromRolloutFilename(file) {
2045
- const m = ROLLOUT_UUID_RE.exec(path7.basename(file));
2261
+ const m = ROLLOUT_UUID_RE.exec(path8.basename(file));
2046
2262
  return m?.[1];
2047
2263
  }
2048
- async function findLatestCodexSessionId(opts) {
2049
- const root = opts.sessionsDir ?? path7.join(os.homedir(), ".codex", "sessions");
2264
+ async function findLatestCodexRollout(opts) {
2265
+ const root = opts.sessionsDir ?? path8.join(os.homedir(), ".codex", "sessions");
2050
2266
  const since = opts.sinceMs ?? 0;
2051
- const targetCwd = await realish(opts.cwd);
2267
+ const targetCwd = await realish2(opts.cwd);
2052
2268
  const rollouts = await listRollouts(root);
2053
2269
  for (const { file, mtimeMs } of rollouts) {
2054
2270
  if (mtimeMs + 2e3 < since) continue;
2055
2271
  const meta = await readSessionMeta(file);
2056
2272
  if (!meta?.cwd) continue;
2057
- const metaCwd = await realish(meta.cwd);
2273
+ const metaCwd = await realish2(meta.cwd);
2058
2274
  if (metaCwd !== targetCwd) continue;
2059
2275
  const id = meta.id ?? uuidFromRolloutFilename(file);
2060
- if (id) return id;
2276
+ return { file, id };
2061
2277
  }
2062
2278
  return void 0;
2063
2279
  }
2280
+ async function sumCodexUsage(file) {
2281
+ let text;
2282
+ try {
2283
+ text = await promises.readFile(file, "utf8");
2284
+ } catch {
2285
+ return void 0;
2286
+ }
2287
+ let total;
2288
+ for (const line of text.split("\n")) {
2289
+ if (!line.includes("token_count")) continue;
2290
+ let rec;
2291
+ try {
2292
+ rec = JSON.parse(line);
2293
+ } catch {
2294
+ continue;
2295
+ }
2296
+ if (rec.payload?.type !== "token_count") continue;
2297
+ const t = rec.payload.info?.total_token_usage;
2298
+ if (t && typeof t === "object") total = t;
2299
+ }
2300
+ if (!total) return void 0;
2301
+ return {
2302
+ source: "transcript",
2303
+ inputTokens: total.input_tokens,
2304
+ cachedInputTokens: total.cached_input_tokens,
2305
+ outputTokens: total.output_tokens,
2306
+ reasoningTokens: total.reasoning_output_tokens,
2307
+ totalTokens: total.total_tokens
2308
+ };
2309
+ }
2064
2310
 
2065
2311
  // src/adapters/interactive/codex-interactive.ts
2066
2312
  var DEFINITION3 = {
@@ -2082,7 +2328,7 @@ var CodexInteractiveAdapter = class _CodexInteractiveAdapter extends Interactive
2082
2328
  now: opts.now,
2083
2329
  installHint: "Install the Codex CLI (`npm i -g @openai/codex`), run `codex login`, and ensure `codex` is on your PATH.",
2084
2330
  preArgs: (input) => {
2085
- const sandbox = input.approvalMode === "readonly" ? "read-only" : input.sandbox ?? "workspace-write";
2331
+ const sandbox = input.approvalMode === "readonly" ? "read-only" : input.sandbox ?? "danger-full-access";
2086
2332
  const approval = input.approvalMode === "gated" ? "on-request" : "never";
2087
2333
  return ["-s", sandbox, "-a", approval];
2088
2334
  },
@@ -2118,29 +2364,28 @@ var CodexInteractiveAdapter = class _CodexInteractiveAdapter extends Interactive
2118
2364
  this.sessionsDir = opts.sessionsDir;
2119
2365
  }
2120
2366
  /**
2121
- * Run Codex, then capture its NATIVE session id (the rollout UUID) for this
2122
- * cwd and attach it to the result's `sessionRef` so the runner persists it and
2123
- * a later resume can use `codex resume <id> "<prompt>"`. Capture is best-effort:
2124
- * if no rollout matches (or any I/O fails) the result is returned unchanged, so
2125
- * the run still resumes via the `--last` fallback.
2367
+ * Run Codex, then read its rollout for this cwd to capture (a) the NATIVE
2368
+ * session id (the rollout UUID) for `sessionRef` so a later resume can use
2369
+ * `codex resume <id> "<prompt>"`, and (b) authoritative token usage for
2370
+ * `meta.usage` (device-independent from Codex's own log, not the TUI). Both
2371
+ * are best-effort: if no rollout matches (or any I/O fails) the result is
2372
+ * returned unchanged, so the run still resumes via the `--last` fallback.
2126
2373
  */
2127
2374
  async run(input, ctx) {
2128
2375
  const startedAt = this.clock().getTime();
2129
2376
  const result = await super.run(input, ctx);
2130
2377
  if (result.error) return result;
2131
- const nativeSessionId = await findLatestCodexSessionId({
2378
+ const rollout = await findLatestCodexRollout({
2132
2379
  cwd: input.cwd,
2133
2380
  sinceMs: startedAt,
2134
2381
  sessionsDir: this.sessionsDir
2135
2382
  });
2136
- if (!nativeSessionId) return result;
2383
+ if (!rollout) return result;
2384
+ const usage = await sumCodexUsage(rollout.file);
2137
2385
  return {
2138
2386
  ...result,
2139
- sessionRef: {
2140
- adapter: this.definition.name,
2141
- nativeSessionId,
2142
- resumable: true
2143
- }
2387
+ sessionRef: rollout.id ? { adapter: this.definition.name, nativeSessionId: rollout.id, resumable: true } : result.sessionRef,
2388
+ meta: usage ? { ...result.meta, usage } : result.meta
2144
2389
  };
2145
2390
  }
2146
2391
  static fromConfig(config) {
@@ -2204,7 +2449,7 @@ async function dirExists(p) {
2204
2449
  }
2205
2450
  }
2206
2451
  async function runInit(options) {
2207
- const rootDir = path7.resolve(options.rootDir);
2452
+ const rootDir = path8.resolve(options.rootDir);
2208
2453
  const cfgPath = configPath(rootDir);
2209
2454
  let configCreated = false;
2210
2455
  let configExists = false;
@@ -2219,8 +2464,8 @@ async function runInit(options) {
2219
2464
  configCreated = true;
2220
2465
  }
2221
2466
  const config = await loadConfig(rootDir);
2222
- const sessionsDir = path7.resolve(rootDir, config.sessionsDir);
2223
- const logsDir = path7.resolve(rootDir, config.logsDir);
2467
+ const sessionsDir = path8.resolve(rootDir, config.sessionsDir);
2468
+ const logsDir = path8.resolve(rootDir, config.logsDir);
2224
2469
  const createdDirs = [];
2225
2470
  for (const dir of [sessionsDir, logsDir]) {
2226
2471
  if (!await dirExists(dir)) {
@@ -2313,11 +2558,11 @@ async function commandCheck(name, command, installHint) {
2313
2558
  };
2314
2559
  }
2315
2560
  async function runDoctor(options) {
2316
- const rootDir = path7.resolve(options.rootDir);
2561
+ const rootDir = path8.resolve(options.rootDir);
2317
2562
  const checks = [];
2318
2563
  checks.push(checkNode());
2319
- let sessionsDir = path7.resolve(rootDir, ".agent-relay/sessions");
2320
- let logsDir = path7.resolve(rootDir, ".agent-relay/logs");
2564
+ let sessionsDir = path8.resolve(rootDir, ".agent-relay/sessions");
2565
+ let logsDir = path8.resolve(rootDir, ".agent-relay/logs");
2321
2566
  try {
2322
2567
  const config = await loadConfig(rootDir);
2323
2568
  checks.push({
@@ -2325,8 +2570,8 @@ async function runDoctor(options) {
2325
2570
  level: "ok",
2326
2571
  detail: `Valid config (defaultAdapter: ${config.defaultAdapter})`
2327
2572
  });
2328
- sessionsDir = path7.resolve(rootDir, config.sessionsDir);
2329
- logsDir = path7.resolve(rootDir, config.logsDir);
2573
+ sessionsDir = path8.resolve(rootDir, config.sessionsDir);
2574
+ logsDir = path8.resolve(rootDir, config.logsDir);
2330
2575
  } catch (err) {
2331
2576
  const message = err instanceof Error ? err.message : String(err);
2332
2577
  const missing = message.includes("not found");
@@ -2395,7 +2640,7 @@ async function resolvePrompt(options) {
2395
2640
  );
2396
2641
  }
2397
2642
  if (options.promptFile) {
2398
- const file = path7.resolve(options.rootDir, options.promptFile);
2643
+ const file = path8.resolve(options.rootDir, options.promptFile);
2399
2644
  try {
2400
2645
  const text = await promises.readFile(file, "utf8");
2401
2646
  if (!text.trim()) {
@@ -2422,7 +2667,7 @@ async function resolvePrompt(options) {
2422
2667
  );
2423
2668
  }
2424
2669
  async function runCommand(options) {
2425
- const rootDir = path7.resolve(options.rootDir);
2670
+ const rootDir = path8.resolve(options.rootDir);
2426
2671
  const config = options.config ?? await loadConfigOrDefault(rootDir, options.onDefaultConfig);
2427
2672
  const prompt = await resolvePrompt({
2428
2673
  prompt: options.prompt,
@@ -2454,9 +2699,9 @@ async function runCommand(options) {
2454
2699
  });
2455
2700
  }
2456
2701
  async function resumeCommand(options) {
2457
- const rootDir = path7.resolve(options.rootDir);
2702
+ const rootDir = path8.resolve(options.rootDir);
2458
2703
  const config = options.config ?? await loadConfigOrDefault(rootDir, options.onDefaultConfig);
2459
- const sessionsDir = path7.resolve(rootDir, config.sessionsDir);
2704
+ const sessionsDir = path8.resolve(rootDir, config.sessionsDir);
2460
2705
  const sessions = new SessionManager(sessionsDir);
2461
2706
  const session = await sessions.load(options.sessionId);
2462
2707
  const resolveAdapter = options.resolveAdapter ?? createAdapterFactory();
@@ -2507,9 +2752,9 @@ async function resumeCommand(options) {
2507
2752
  return { session, resumable, resumed: true, outcome };
2508
2753
  }
2509
2754
  async function resolveManager(opts) {
2510
- const rootDir = path7.resolve(opts.rootDir);
2755
+ const rootDir = path8.resolve(opts.rootDir);
2511
2756
  const config = await loadConfigOrDefault(rootDir, opts.onDefaultConfig);
2512
- const sessionsDir = path7.resolve(rootDir, config.sessionsDir);
2757
+ const sessionsDir = path8.resolve(rootDir, config.sessionsDir);
2513
2758
  const sessions = new SessionManager(sessionsDir);
2514
2759
  return { sessions, metas: await sessions.list() };
2515
2760
  }
@@ -2568,4 +2813,4 @@ async function pruneSessions(opts) {
2568
2813
  };
2569
2814
  }
2570
2815
 
2571
- export { AdapterRegistry, AgentRelayError, AlwaysApproveDecider, ApiDecider, BUILTIN_ADAPTER_DEFINITIONS, CONFIG_FILENAME, ClaudeInteractiveAdapter, CodexInteractiveAdapter, CommandDecider, CompositeCompletionDetector, ConfigError, DEFAULT_DENY_PATTERNS, DefaultCompletionDetector, DefaultKeymap, FakeAgentAdapter, FunctionDecider, InteractivePtyAdapter, OutputPatternDetector, PromptDetector, RuleDecider, RunLogger, SessionManager, SessionNotFoundError, UnknownAdapterError, adapterConfigSchema, approvalPolicySchema, cleanTerminalText, configPath, configSchema, createAdapterFactory, createDecider, createDefaultConfig, deciderConfigFromFlags, deciderSchema, defaultRegistry, defaultsSchema, hooksSchema, listAdapters, listSessions, loadConfig, loadConfigOrDefault, parseCheckbox, parseConfig, parseDecisionReply, pruneSessions, renderDecisionPrompt, resolveApprovalMode, resolvePrompt, resolveSandbox, resumeCommand, runAgent, runCommand, runDoctor, runInit, runPtySession, runShellHook, sandboxSchema, saveConfig, stringifyConfig, stripAnsi, tailLines };
2816
+ export { AdapterRegistry, AgentRelayError, AlwaysApproveDecider, ApiDecider, BUILTIN_ADAPTER_DEFINITIONS, CONFIG_FILENAME, ClaudeInteractiveAdapter, CodexInteractiveAdapter, CommandDecider, CompositeCompletionDetector, ConfigError, DEFAULT_DENY_PATTERNS, DEFAULT_PRICING, DefaultCompletionDetector, DefaultKeymap, FakeAgentAdapter, FunctionDecider, InteractivePtyAdapter, OutputPatternDetector, PromptDetector, RuleDecider, RunLogger, SessionManager, SessionNotFoundError, UnknownAdapterError, adapterConfigSchema, approvalPolicySchema, cleanTerminalText, computeCostUsd, configPath, configSchema, createAdapterFactory, createDecider, createDefaultConfig, deciderConfigFromFlags, deciderSchema, defaultRegistry, defaultsSchema, hooksSchema, listAdapters, listSessions, loadConfig, loadConfigOrDefault, parseCheckbox, parseConfig, parseDecisionReply, pricingForModel, pruneSessions, renderDecisionPrompt, resolveApprovalMode, resolvePrompt, resolveSandbox, resumeCommand, runAgent, runCommand, runDoctor, runInit, runPtySession, runShellHook, sandboxSchema, saveConfig, stringifyConfig, stripAnsi, tailLines };