@yul-labs/agent-relay 0.1.1 → 0.1.2

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';
@@ -187,7 +187,7 @@ ${detail}`,
187
187
  return result.data;
188
188
  }
189
189
  function configPath(rootDir) {
190
- return path7.resolve(rootDir, CONFIG_FILENAME);
190
+ return path8.resolve(rootDir, CONFIG_FILENAME);
191
191
  }
192
192
  async function loadConfig(rootDir) {
193
193
  const file = configPath(rootDir);
@@ -227,7 +227,7 @@ function stringifyConfig(config) {
227
227
  }
228
228
  async function saveConfig(rootDir, config) {
229
229
  const file = configPath(rootDir);
230
- await promises.mkdir(path7.dirname(file), { recursive: true });
230
+ await promises.mkdir(path8.dirname(file), { recursive: true });
231
231
  await promises.writeFile(file, stringifyConfig(config), "utf8");
232
232
  return file;
233
233
  }
@@ -238,7 +238,7 @@ var SessionManager = class {
238
238
  sessionsDir;
239
239
  /** Absolute path to a session's JSON metadata file. */
240
240
  filePath(sessionId) {
241
- return path7.join(this.sessionsDir, `${sessionId}.json`);
241
+ return path8.join(this.sessionsDir, `${sessionId}.json`);
242
242
  }
243
243
  /** Build a fresh session record in the `created` state (not yet persisted). */
244
244
  create(input) {
@@ -305,7 +305,7 @@ var SessionManager = class {
305
305
  if (!entry.endsWith(".json")) continue;
306
306
  try {
307
307
  const raw = await promises.readFile(
308
- path7.join(this.sessionsDir, entry),
308
+ path8.join(this.sessionsDir, entry),
309
309
  "utf8"
310
310
  );
311
311
  metas.push(JSON.parse(raw));
@@ -345,7 +345,7 @@ var RunLogger = class {
345
345
  }
346
346
  /** Ensure the log directory exists and write the run header. */
347
347
  start(header) {
348
- fs3.mkdirSync(path7.dirname(this.logFile), { recursive: true });
348
+ fs3.mkdirSync(path8.dirname(this.logFile), { recursive: true });
349
349
  const lines = [
350
350
  "================ agent-relay run ================",
351
351
  `sessionId : ${header.sessionId}`,
@@ -684,6 +684,7 @@ var CommandDecider = class {
684
684
  }
685
685
  }
686
686
  };
687
+ var MIN_API_MAX_TOKENS = 512;
687
688
  var ApiDecider = class {
688
689
  constructor(opts) {
689
690
  this.opts = opts;
@@ -713,7 +714,7 @@ var ApiDecider = class {
713
714
  body: JSON.stringify({
714
715
  model: this.opts.model ?? "default",
715
716
  messages: [{ role: "user", content: render(req) }],
716
- max_tokens: this.opts.maxTokens ?? 2048,
717
+ max_tokens: Math.max(this.opts.maxTokens ?? 2048, MIN_API_MAX_TOKENS),
717
718
  temperature: this.opts.temperature ?? 0,
718
719
  stream: false
719
720
  }),
@@ -807,12 +808,12 @@ async function runAgent(options) {
807
808
  }
808
809
  const adapter = options.resolveAdapter(adapterName, config);
809
810
  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);
811
+ const rootDir = path8.resolve(options.rootDir);
812
+ const cwd = path8.resolve(options.cwd ?? rootDir);
813
+ const sessionsDir = path8.resolve(rootDir, config.sessionsDir);
814
+ const logsDir = path8.resolve(rootDir, config.logsDir);
814
815
  const sessionId = options.sessionId ?? randomUUID();
815
- const logFile = path7.join(logsDir, `${sessionId}.log`);
816
+ const logFile = path8.join(logsDir, `${sessionId}.log`);
816
817
  const sessions = new SessionManager(sessionsDir);
817
818
  const startedAt = iso();
818
819
  const session = sessions.create({
@@ -1234,8 +1235,8 @@ async function isExecutableFile(file) {
1234
1235
  async function which(command) {
1235
1236
  if (!command) return null;
1236
1237
  const exts = pathExtensions();
1237
- if (command.includes(path7.sep) || command.includes("/")) {
1238
- const base = path7.resolve(command);
1238
+ if (command.includes(path8.sep) || command.includes("/")) {
1239
+ const base = path8.resolve(command);
1239
1240
  for (const ext of exts) {
1240
1241
  const candidate = base + ext;
1241
1242
  if (await isExecutableFile(candidate)) return candidate;
@@ -1243,11 +1244,11 @@ async function which(command) {
1243
1244
  return null;
1244
1245
  }
1245
1246
  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")];
1247
+ const dirs = pathEnv.split(path8.delimiter).filter(Boolean);
1248
+ const fallbacks = process.platform === "win32" ? [] : ["/usr/local/bin", "/opt/homebrew/bin", path8.join(os.homedir(), ".local/bin")];
1248
1249
  for (const dir of [...dirs, ...fallbacks]) {
1249
1250
  for (const ext of exts) {
1250
- const candidate = path7.join(dir, command + ext);
1251
+ const candidate = path8.join(dir, command + ext);
1251
1252
  if (await isExecutableFile(candidate)) return candidate;
1252
1253
  }
1253
1254
  }
@@ -1373,11 +1374,11 @@ function ensurePtyHelperExecutable() {
1373
1374
  if (process.platform === "win32") return;
1374
1375
  try {
1375
1376
  const require2 = createRequire(import.meta.url);
1376
- const root = path7.dirname(require2.resolve("node-pty/package.json"));
1377
+ const root = path8.dirname(require2.resolve("node-pty/package.json"));
1377
1378
  const candidates = [
1378
- path7.join(root, "build", "Release", "spawn-helper"),
1379
- path7.join(root, "build", "Debug", "spawn-helper"),
1380
- path7.join(
1379
+ path8.join(root, "build", "Release", "spawn-helper"),
1380
+ path8.join(root, "build", "Debug", "spawn-helper"),
1381
+ path8.join(
1381
1382
  root,
1382
1383
  "prebuilds",
1383
1384
  `${process.platform}-${process.arch}`,
@@ -1539,6 +1540,7 @@ function runPtySession(opts, ctx) {
1539
1540
  let handling = false;
1540
1541
  let quitting = false;
1541
1542
  let settled = false;
1543
+ let usage;
1542
1544
  let finished = false;
1543
1545
  let disposeData;
1544
1546
  let disposeExit;
@@ -1634,6 +1636,10 @@ function runPtySession(opts, ctx) {
1634
1636
  };
1635
1637
  const onSettle = async () => {
1636
1638
  if (finished || handling || quitting) return;
1639
+ if (opts.scrapeUsage) {
1640
+ const u = opts.scrapeUsage(cleanTerminalText(buffer));
1641
+ if (u) usage = { ...usage, ...u };
1642
+ }
1637
1643
  if (pendingComment !== void 0) {
1638
1644
  const comment = pendingComment;
1639
1645
  pendingComment = void 0;
@@ -1770,7 +1776,7 @@ function runPtySession(opts, ctx) {
1770
1776
  exitCode,
1771
1777
  error,
1772
1778
  sessionRef: void 0,
1773
- meta: { interactions, settled }
1779
+ meta: { interactions, settled, ...usage ? { usage } : {} }
1774
1780
  });
1775
1781
  });
1776
1782
  if (ctx.signal.aborted) onAbort();
@@ -1884,6 +1890,7 @@ var InteractivePtyAdapter = class {
1884
1890
  completionPattern: this.cfg.completionPattern,
1885
1891
  completionIdleMs: input.completionIdleMs ?? this.cfg.completionIdleMs,
1886
1892
  workingPattern: this.cfg.workingPattern,
1893
+ scrapeUsage: this.cfg.scrapeUsage,
1887
1894
  quitKeys: this.cfg.quitKeys,
1888
1895
  setup: extra?.setup,
1889
1896
  promptAfterSetup: extra?.promptAfterSetup,
@@ -1899,8 +1906,113 @@ var InteractivePtyAdapter = class {
1899
1906
  );
1900
1907
  }
1901
1908
  };
1909
+ async function realish(p) {
1910
+ try {
1911
+ return await promises.realpath(p);
1912
+ } catch {
1913
+ return p;
1914
+ }
1915
+ }
1916
+ function projectDirCandidates(absCwd) {
1917
+ return [absCwd.replace(/[/.]/g, "-"), absCwd.replace(/\//g, "-")];
1918
+ }
1919
+ async function resolveProjectDir(root, cwd) {
1920
+ const abs = await realish(cwd);
1921
+ for (const name of projectDirCandidates(abs)) {
1922
+ const dir = path8.join(root, name);
1923
+ try {
1924
+ const st = await promises.stat(dir);
1925
+ if (st.isDirectory()) return dir;
1926
+ } catch {
1927
+ }
1928
+ }
1929
+ return void 0;
1930
+ }
1931
+ async function findLatestClaudeTranscript(opts) {
1932
+ const root = opts.projectsDir ?? path8.join(os.homedir(), ".claude", "projects");
1933
+ const since = opts.sinceMs ?? 0;
1934
+ const dir = await resolveProjectDir(root, opts.cwd);
1935
+ if (!dir) return void 0;
1936
+ let files;
1937
+ try {
1938
+ files = await promises.readdir(dir);
1939
+ } catch {
1940
+ return void 0;
1941
+ }
1942
+ const candidates = [];
1943
+ for (const f of files) {
1944
+ if (!f.endsWith(".jsonl")) continue;
1945
+ const file = path8.join(dir, f);
1946
+ try {
1947
+ const st = await promises.stat(file);
1948
+ if (st.mtimeMs + 2e3 < since) continue;
1949
+ candidates.push({ file, mtimeMs: st.mtimeMs });
1950
+ } catch {
1951
+ }
1952
+ }
1953
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
1954
+ return candidates[0]?.file;
1955
+ }
1956
+ async function sumClaudeUsage(file) {
1957
+ let text;
1958
+ try {
1959
+ text = await promises.readFile(file, "utf8");
1960
+ } catch {
1961
+ return void 0;
1962
+ }
1963
+ let input = 0;
1964
+ let output = 0;
1965
+ let cacheCreate = 0;
1966
+ let cacheRead = 0;
1967
+ let model;
1968
+ let any = false;
1969
+ for (const line of text.split("\n")) {
1970
+ if (!line.trim()) continue;
1971
+ let rec;
1972
+ try {
1973
+ rec = JSON.parse(line);
1974
+ } catch {
1975
+ continue;
1976
+ }
1977
+ const u = rec.message?.usage;
1978
+ if (!u || typeof u !== "object") continue;
1979
+ any = true;
1980
+ input += u.input_tokens ?? 0;
1981
+ output += u.output_tokens ?? 0;
1982
+ cacheCreate += u.cache_creation_input_tokens ?? 0;
1983
+ cacheRead += u.cache_read_input_tokens ?? 0;
1984
+ if (!model && typeof rec.message?.model === "string")
1985
+ model = rec.message.model;
1986
+ }
1987
+ if (!any) return void 0;
1988
+ return {
1989
+ source: "transcript",
1990
+ model,
1991
+ inputTokens: input,
1992
+ outputTokens: output,
1993
+ cacheCreationTokens: cacheCreate,
1994
+ cachedInputTokens: cacheRead,
1995
+ totalTokens: input + output + cacheCreate + cacheRead
1996
+ };
1997
+ }
1998
+ async function findLatestClaudeUsage(opts) {
1999
+ const file = await findLatestClaudeTranscript(opts);
2000
+ if (!file) return void 0;
2001
+ return sumClaudeUsage(file);
2002
+ }
1902
2003
 
1903
2004
  // src/adapters/interactive/claude-interactive.ts
2005
+ function scrapeClaudeStatusLine(text) {
2006
+ const u = { source: "status-line" };
2007
+ const ctx = text.match(/context[^\n]*?(\d{1,3})\s*%/i);
2008
+ if (ctx) {
2009
+ u.contextPercent = Number(ctx[1]);
2010
+ u.raw = ctx[0].trim().replace(/\s+/g, " ");
2011
+ }
2012
+ const cost = text.match(/session\s*\$\s*([\d.]+)/i);
2013
+ if (cost) u.sessionCostUsd = Number(cost[1]);
2014
+ return u.contextPercent !== void 0 || u.sessionCostUsd !== void 0 ? u : void 0;
2015
+ }
1904
2016
  var DEFINITION2 = {
1905
2017
  name: "claude",
1906
2018
  type: "claude",
@@ -1909,6 +2021,9 @@ var DEFINITION2 = {
1909
2021
  supportsResume: true
1910
2022
  };
1911
2023
  var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends InteractivePtyAdapter {
2024
+ clock;
2025
+ /** Override the projects root (~/.claude/projects) for tests. */
2026
+ projectsDir;
1912
2027
  constructor(opts = {}) {
1913
2028
  super({
1914
2029
  definition: DEFINITION2,
@@ -1925,14 +2040,14 @@ var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends Interacti
1925
2040
  if (input.approvalMode === "readonly")
1926
2041
  return [...head, "--permission-mode", "plan"];
1927
2042
  if (input.approvalMode === "gated")
1928
- return [...head, "--permission-mode", "acceptEdits"];
2043
+ return [...head, "--permission-mode", "default"];
1929
2044
  return [...head, "--dangerously-skip-permissions"];
1930
2045
  },
1931
2046
  // Resume the most recent conversation in the cwd and send the follow-up
1932
2047
  // prompt. (`--continue` picks the latest session; the native id is not
1933
2048
  // captured in PTY mode, so a specific `--resume <id>` is not used.)
1934
2049
  resumeArgs: (input) => {
1935
- const mode = input.approvalMode === "readonly" ? ["--permission-mode", "plan"] : input.approvalMode === "gated" ? ["--permission-mode", "acceptEdits"] : ["--dangerously-skip-permissions"];
2050
+ const mode = input.approvalMode === "readonly" ? ["--permission-mode", "plan"] : input.approvalMode === "gated" ? ["--permission-mode", "default"] : ["--dangerously-skip-permissions"];
1936
2051
  return ["--continue", "--effort", "xhigh", ...mode];
1937
2052
  },
1938
2053
  // Unattended ultracode: once Claude is idle (past the trust dialog), type
@@ -1946,6 +2061,10 @@ var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends Interacti
1946
2061
  approvalPattern: /(allow|grant|permission|approve|trust|proceed|do you want)[^\n]{0,60}\?|\[y\/n\]|\(y\/n\)/i
1947
2062
  },
1948
2063
  keymap: new ArrowKeymap(),
2064
+ // SUPPLEMENT only: grab context% / session $ from the status line when it
2065
+ // is enabled. The authoritative token counts come from the transcript in
2066
+ // run() below and overwrite these on the same meta.usage object.
2067
+ scrapeUsage: scrapeClaudeStatusLine,
1949
2068
  // Claude's TUI stays open after a task; quit once it's been idle a while.
1950
2069
  completionIdleMs: 8e3,
1951
2070
  // While Claude is ACTIVELY working it shows "(esc to interrupt)" — that is
@@ -1959,6 +2078,32 @@ var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends Interacti
1959
2078
  workingPattern: /interrupt/i,
1960
2079
  quitKeys: ""
1961
2080
  });
2081
+ this.clock = opts.now ?? (() => /* @__PURE__ */ new Date());
2082
+ this.projectsDir = opts.projectsDir;
2083
+ }
2084
+ /**
2085
+ * Run Claude, then read AUTHORITATIVE token usage from its session transcript
2086
+ * (~/.claude/projects/<cwd>/<id>.jsonl) and surface it as `meta.usage`. This is
2087
+ * device-independent — it works regardless of whether the user has a usage
2088
+ * status line — and overwrites the best-effort status-line scrape's token
2089
+ * figures while keeping its context%/cost extras. Best-effort: if no transcript
2090
+ * is found, the status-line usage (if any) is left as-is.
2091
+ */
2092
+ async run(input, ctx) {
2093
+ const startedAt = this.clock().getTime();
2094
+ const result = await super.run(input, ctx);
2095
+ if (result.error) return result;
2096
+ const transcript = await findLatestClaudeUsage({
2097
+ cwd: input.cwd,
2098
+ sinceMs: startedAt,
2099
+ projectsDir: this.projectsDir
2100
+ });
2101
+ if (!transcript) return result;
2102
+ const prior = result.meta?.usage ?? {};
2103
+ return {
2104
+ ...result,
2105
+ meta: { ...result.meta, usage: { ...prior, ...transcript } }
2106
+ };
1962
2107
  }
1963
2108
  static fromConfig(config) {
1964
2109
  return new _ClaudeInteractiveAdapter({
@@ -1968,7 +2113,7 @@ var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends Interacti
1968
2113
  }
1969
2114
  };
1970
2115
  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) {
2116
+ async function realish2(p) {
1972
2117
  try {
1973
2118
  return await promises.realpath(p);
1974
2119
  } catch {
@@ -1984,7 +2129,7 @@ async function listRollouts(root) {
1984
2129
  return out;
1985
2130
  }
1986
2131
  for (const y of years) {
1987
- const yDir = path7.join(root, y);
2132
+ const yDir = path8.join(root, y);
1988
2133
  let months;
1989
2134
  try {
1990
2135
  months = await promises.readdir(yDir);
@@ -1992,7 +2137,7 @@ async function listRollouts(root) {
1992
2137
  continue;
1993
2138
  }
1994
2139
  for (const m of months) {
1995
- const mDir = path7.join(yDir, m);
2140
+ const mDir = path8.join(yDir, m);
1996
2141
  let days;
1997
2142
  try {
1998
2143
  days = await promises.readdir(mDir);
@@ -2000,7 +2145,7 @@ async function listRollouts(root) {
2000
2145
  continue;
2001
2146
  }
2002
2147
  for (const d of days) {
2003
- const dDir = path7.join(mDir, d);
2148
+ const dDir = path8.join(mDir, d);
2004
2149
  let files;
2005
2150
  try {
2006
2151
  files = await promises.readdir(dDir);
@@ -2009,7 +2154,7 @@ async function listRollouts(root) {
2009
2154
  }
2010
2155
  for (const f of files) {
2011
2156
  if (!f.startsWith("rollout-") || !f.endsWith(".jsonl")) continue;
2012
- const file = path7.join(dDir, f);
2157
+ const file = path8.join(dDir, f);
2013
2158
  try {
2014
2159
  const st = await promises.stat(file);
2015
2160
  out.push({ file, mtimeMs: st.mtimeMs });
@@ -2042,25 +2187,55 @@ async function readSessionMeta(file) {
2042
2187
  }
2043
2188
  }
2044
2189
  function uuidFromRolloutFilename(file) {
2045
- const m = ROLLOUT_UUID_RE.exec(path7.basename(file));
2190
+ const m = ROLLOUT_UUID_RE.exec(path8.basename(file));
2046
2191
  return m?.[1];
2047
2192
  }
2048
- async function findLatestCodexSessionId(opts) {
2049
- const root = opts.sessionsDir ?? path7.join(os.homedir(), ".codex", "sessions");
2193
+ async function findLatestCodexRollout(opts) {
2194
+ const root = opts.sessionsDir ?? path8.join(os.homedir(), ".codex", "sessions");
2050
2195
  const since = opts.sinceMs ?? 0;
2051
- const targetCwd = await realish(opts.cwd);
2196
+ const targetCwd = await realish2(opts.cwd);
2052
2197
  const rollouts = await listRollouts(root);
2053
2198
  for (const { file, mtimeMs } of rollouts) {
2054
2199
  if (mtimeMs + 2e3 < since) continue;
2055
2200
  const meta = await readSessionMeta(file);
2056
2201
  if (!meta?.cwd) continue;
2057
- const metaCwd = await realish(meta.cwd);
2202
+ const metaCwd = await realish2(meta.cwd);
2058
2203
  if (metaCwd !== targetCwd) continue;
2059
2204
  const id = meta.id ?? uuidFromRolloutFilename(file);
2060
- if (id) return id;
2205
+ return { file, id };
2061
2206
  }
2062
2207
  return void 0;
2063
2208
  }
2209
+ async function sumCodexUsage(file) {
2210
+ let text;
2211
+ try {
2212
+ text = await promises.readFile(file, "utf8");
2213
+ } catch {
2214
+ return void 0;
2215
+ }
2216
+ let total;
2217
+ for (const line of text.split("\n")) {
2218
+ if (!line.includes("token_count")) continue;
2219
+ let rec;
2220
+ try {
2221
+ rec = JSON.parse(line);
2222
+ } catch {
2223
+ continue;
2224
+ }
2225
+ if (rec.payload?.type !== "token_count") continue;
2226
+ const t = rec.payload.info?.total_token_usage;
2227
+ if (t && typeof t === "object") total = t;
2228
+ }
2229
+ if (!total) return void 0;
2230
+ return {
2231
+ source: "transcript",
2232
+ inputTokens: total.input_tokens,
2233
+ cachedInputTokens: total.cached_input_tokens,
2234
+ outputTokens: total.output_tokens,
2235
+ reasoningTokens: total.reasoning_output_tokens,
2236
+ totalTokens: total.total_tokens
2237
+ };
2238
+ }
2064
2239
 
2065
2240
  // src/adapters/interactive/codex-interactive.ts
2066
2241
  var DEFINITION3 = {
@@ -2118,29 +2293,28 @@ var CodexInteractiveAdapter = class _CodexInteractiveAdapter extends Interactive
2118
2293
  this.sessionsDir = opts.sessionsDir;
2119
2294
  }
2120
2295
  /**
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.
2296
+ * Run Codex, then read its rollout for this cwd to capture (a) the NATIVE
2297
+ * session id (the rollout UUID) for `sessionRef` so a later resume can use
2298
+ * `codex resume <id> "<prompt>"`, and (b) authoritative token usage for
2299
+ * `meta.usage` (device-independent from Codex's own log, not the TUI). Both
2300
+ * are best-effort: if no rollout matches (or any I/O fails) the result is
2301
+ * returned unchanged, so the run still resumes via the `--last` fallback.
2126
2302
  */
2127
2303
  async run(input, ctx) {
2128
2304
  const startedAt = this.clock().getTime();
2129
2305
  const result = await super.run(input, ctx);
2130
2306
  if (result.error) return result;
2131
- const nativeSessionId = await findLatestCodexSessionId({
2307
+ const rollout = await findLatestCodexRollout({
2132
2308
  cwd: input.cwd,
2133
2309
  sinceMs: startedAt,
2134
2310
  sessionsDir: this.sessionsDir
2135
2311
  });
2136
- if (!nativeSessionId) return result;
2312
+ if (!rollout) return result;
2313
+ const usage = await sumCodexUsage(rollout.file);
2137
2314
  return {
2138
2315
  ...result,
2139
- sessionRef: {
2140
- adapter: this.definition.name,
2141
- nativeSessionId,
2142
- resumable: true
2143
- }
2316
+ sessionRef: rollout.id ? { adapter: this.definition.name, nativeSessionId: rollout.id, resumable: true } : result.sessionRef,
2317
+ meta: usage ? { ...result.meta, usage } : result.meta
2144
2318
  };
2145
2319
  }
2146
2320
  static fromConfig(config) {
@@ -2204,7 +2378,7 @@ async function dirExists(p) {
2204
2378
  }
2205
2379
  }
2206
2380
  async function runInit(options) {
2207
- const rootDir = path7.resolve(options.rootDir);
2381
+ const rootDir = path8.resolve(options.rootDir);
2208
2382
  const cfgPath = configPath(rootDir);
2209
2383
  let configCreated = false;
2210
2384
  let configExists = false;
@@ -2219,8 +2393,8 @@ async function runInit(options) {
2219
2393
  configCreated = true;
2220
2394
  }
2221
2395
  const config = await loadConfig(rootDir);
2222
- const sessionsDir = path7.resolve(rootDir, config.sessionsDir);
2223
- const logsDir = path7.resolve(rootDir, config.logsDir);
2396
+ const sessionsDir = path8.resolve(rootDir, config.sessionsDir);
2397
+ const logsDir = path8.resolve(rootDir, config.logsDir);
2224
2398
  const createdDirs = [];
2225
2399
  for (const dir of [sessionsDir, logsDir]) {
2226
2400
  if (!await dirExists(dir)) {
@@ -2313,11 +2487,11 @@ async function commandCheck(name, command, installHint) {
2313
2487
  };
2314
2488
  }
2315
2489
  async function runDoctor(options) {
2316
- const rootDir = path7.resolve(options.rootDir);
2490
+ const rootDir = path8.resolve(options.rootDir);
2317
2491
  const checks = [];
2318
2492
  checks.push(checkNode());
2319
- let sessionsDir = path7.resolve(rootDir, ".agent-relay/sessions");
2320
- let logsDir = path7.resolve(rootDir, ".agent-relay/logs");
2493
+ let sessionsDir = path8.resolve(rootDir, ".agent-relay/sessions");
2494
+ let logsDir = path8.resolve(rootDir, ".agent-relay/logs");
2321
2495
  try {
2322
2496
  const config = await loadConfig(rootDir);
2323
2497
  checks.push({
@@ -2325,8 +2499,8 @@ async function runDoctor(options) {
2325
2499
  level: "ok",
2326
2500
  detail: `Valid config (defaultAdapter: ${config.defaultAdapter})`
2327
2501
  });
2328
- sessionsDir = path7.resolve(rootDir, config.sessionsDir);
2329
- logsDir = path7.resolve(rootDir, config.logsDir);
2502
+ sessionsDir = path8.resolve(rootDir, config.sessionsDir);
2503
+ logsDir = path8.resolve(rootDir, config.logsDir);
2330
2504
  } catch (err) {
2331
2505
  const message = err instanceof Error ? err.message : String(err);
2332
2506
  const missing = message.includes("not found");
@@ -2395,7 +2569,7 @@ async function resolvePrompt(options) {
2395
2569
  );
2396
2570
  }
2397
2571
  if (options.promptFile) {
2398
- const file = path7.resolve(options.rootDir, options.promptFile);
2572
+ const file = path8.resolve(options.rootDir, options.promptFile);
2399
2573
  try {
2400
2574
  const text = await promises.readFile(file, "utf8");
2401
2575
  if (!text.trim()) {
@@ -2422,7 +2596,7 @@ async function resolvePrompt(options) {
2422
2596
  );
2423
2597
  }
2424
2598
  async function runCommand(options) {
2425
- const rootDir = path7.resolve(options.rootDir);
2599
+ const rootDir = path8.resolve(options.rootDir);
2426
2600
  const config = options.config ?? await loadConfigOrDefault(rootDir, options.onDefaultConfig);
2427
2601
  const prompt = await resolvePrompt({
2428
2602
  prompt: options.prompt,
@@ -2454,9 +2628,9 @@ async function runCommand(options) {
2454
2628
  });
2455
2629
  }
2456
2630
  async function resumeCommand(options) {
2457
- const rootDir = path7.resolve(options.rootDir);
2631
+ const rootDir = path8.resolve(options.rootDir);
2458
2632
  const config = options.config ?? await loadConfigOrDefault(rootDir, options.onDefaultConfig);
2459
- const sessionsDir = path7.resolve(rootDir, config.sessionsDir);
2633
+ const sessionsDir = path8.resolve(rootDir, config.sessionsDir);
2460
2634
  const sessions = new SessionManager(sessionsDir);
2461
2635
  const session = await sessions.load(options.sessionId);
2462
2636
  const resolveAdapter = options.resolveAdapter ?? createAdapterFactory();
@@ -2507,9 +2681,9 @@ async function resumeCommand(options) {
2507
2681
  return { session, resumable, resumed: true, outcome };
2508
2682
  }
2509
2683
  async function resolveManager(opts) {
2510
- const rootDir = path7.resolve(opts.rootDir);
2684
+ const rootDir = path8.resolve(opts.rootDir);
2511
2685
  const config = await loadConfigOrDefault(rootDir, opts.onDefaultConfig);
2512
- const sessionsDir = path7.resolve(rootDir, config.sessionsDir);
2686
+ const sessionsDir = path8.resolve(rootDir, config.sessionsDir);
2513
2687
  const sessions = new SessionManager(sessionsDir);
2514
2688
  return { sessions, metas: await sessions.list() };
2515
2689
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yul-labs/agent-relay",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Vendor-agnostic LLM coding agent session orchestrator (Claude, Codex, ...)",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.25.0",