@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/README.md +70 -3
- package/dist/cli.js +317 -72
- package/dist/index.d.ts +212 -16
- package/dist/index.js +312 -67
- 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';
|
|
@@ -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 ?? "
|
|
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: "
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
811
|
-
const cwd =
|
|
812
|
-
const sessionsDir =
|
|
813
|
-
const 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 =
|
|
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(
|
|
1238
|
-
const base =
|
|
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(
|
|
1247
|
-
const fallbacks = process.platform === "win32" ? [] : ["/usr/local/bin", "/opt/homebrew/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 =
|
|
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 =
|
|
1431
|
+
const root = path8.dirname(require2.resolve("node-pty/package.json"));
|
|
1377
1432
|
const candidates = [
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
-
|
|
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", "
|
|
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", "
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
2261
|
+
const m = ROLLOUT_UUID_RE.exec(path8.basename(file));
|
|
2046
2262
|
return m?.[1];
|
|
2047
2263
|
}
|
|
2048
|
-
async function
|
|
2049
|
-
const root = opts.sessionsDir ??
|
|
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
|
|
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
|
|
2273
|
+
const metaCwd = await realish2(meta.cwd);
|
|
2058
2274
|
if (metaCwd !== targetCwd) continue;
|
|
2059
2275
|
const id = meta.id ?? uuidFromRolloutFilename(file);
|
|
2060
|
-
|
|
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 ?? "
|
|
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
|
|
2122
|
-
*
|
|
2123
|
-
*
|
|
2124
|
-
*
|
|
2125
|
-
*
|
|
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
|
|
2378
|
+
const rollout = await findLatestCodexRollout({
|
|
2132
2379
|
cwd: input.cwd,
|
|
2133
2380
|
sinceMs: startedAt,
|
|
2134
2381
|
sessionsDir: this.sessionsDir
|
|
2135
2382
|
});
|
|
2136
|
-
if (!
|
|
2383
|
+
if (!rollout) return result;
|
|
2384
|
+
const usage = await sumCodexUsage(rollout.file);
|
|
2137
2385
|
return {
|
|
2138
2386
|
...result,
|
|
2139
|
-
sessionRef: {
|
|
2140
|
-
|
|
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 =
|
|
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 =
|
|
2223
|
-
const 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 =
|
|
2561
|
+
const rootDir = path8.resolve(options.rootDir);
|
|
2317
2562
|
const checks = [];
|
|
2318
2563
|
checks.push(checkNode());
|
|
2319
|
-
let sessionsDir =
|
|
2320
|
-
let logsDir =
|
|
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 =
|
|
2329
|
-
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 =
|
|
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 =
|
|
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 =
|
|
2702
|
+
const rootDir = path8.resolve(options.rootDir);
|
|
2458
2703
|
const config = options.config ?? await loadConfigOrDefault(rootDir, options.onDefaultConfig);
|
|
2459
|
-
const 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 =
|
|
2755
|
+
const rootDir = path8.resolve(opts.rootDir);
|
|
2511
2756
|
const config = await loadConfigOrDefault(rootDir, opts.onDefaultConfig);
|
|
2512
|
-
const 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 };
|