@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/README.md +28 -1
- package/dist/cli.js +241 -67
- package/dist/index.d.ts +80 -10
- package/dist/index.js +235 -61
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs3, { promises, constants, statSync, chmodSync } from 'fs';
|
|
2
|
-
import
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
811
|
-
const cwd =
|
|
812
|
-
const sessionsDir =
|
|
813
|
-
const 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 =
|
|
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(
|
|
1238
|
-
const base =
|
|
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(
|
|
1247
|
-
const fallbacks = process.platform === "win32" ? [] : ["/usr/local/bin", "/opt/homebrew/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 =
|
|
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 =
|
|
1377
|
+
const root = path8.dirname(require2.resolve("node-pty/package.json"));
|
|
1377
1378
|
const candidates = [
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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", "
|
|
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", "
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
2190
|
+
const m = ROLLOUT_UUID_RE.exec(path8.basename(file));
|
|
2046
2191
|
return m?.[1];
|
|
2047
2192
|
}
|
|
2048
|
-
async function
|
|
2049
|
-
const root = opts.sessionsDir ??
|
|
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
|
|
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
|
|
2202
|
+
const metaCwd = await realish2(meta.cwd);
|
|
2058
2203
|
if (metaCwd !== targetCwd) continue;
|
|
2059
2204
|
const id = meta.id ?? uuidFromRolloutFilename(file);
|
|
2060
|
-
|
|
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
|
|
2122
|
-
*
|
|
2123
|
-
*
|
|
2124
|
-
*
|
|
2125
|
-
*
|
|
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
|
|
2307
|
+
const rollout = await findLatestCodexRollout({
|
|
2132
2308
|
cwd: input.cwd,
|
|
2133
2309
|
sinceMs: startedAt,
|
|
2134
2310
|
sessionsDir: this.sessionsDir
|
|
2135
2311
|
});
|
|
2136
|
-
if (!
|
|
2312
|
+
if (!rollout) return result;
|
|
2313
|
+
const usage = await sumCodexUsage(rollout.file);
|
|
2137
2314
|
return {
|
|
2138
2315
|
...result,
|
|
2139
|
-
sessionRef: {
|
|
2140
|
-
|
|
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 =
|
|
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 =
|
|
2223
|
-
const 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 =
|
|
2490
|
+
const rootDir = path8.resolve(options.rootDir);
|
|
2317
2491
|
const checks = [];
|
|
2318
2492
|
checks.push(checkNode());
|
|
2319
|
-
let sessionsDir =
|
|
2320
|
-
let logsDir =
|
|
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 =
|
|
2329
|
-
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 =
|
|
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 =
|
|
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 =
|
|
2631
|
+
const rootDir = path8.resolve(options.rootDir);
|
|
2458
2632
|
const config = options.config ?? await loadConfigOrDefault(rootDir, options.onDefaultConfig);
|
|
2459
|
-
const 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 =
|
|
2684
|
+
const rootDir = path8.resolve(opts.rootDir);
|
|
2511
2685
|
const config = await loadConfigOrDefault(rootDir, opts.onDefaultConfig);
|
|
2512
|
-
const 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
|
}
|