@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/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import fs7, { promises, constants, statSync, chmodSync } from 'fs';
|
|
4
|
+
import path6 from 'path';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
-
import { execa } from 'execa';
|
|
7
6
|
import os from 'os';
|
|
7
|
+
import { execa } from 'execa';
|
|
8
8
|
import * as nodePty from 'node-pty';
|
|
9
9
|
import { createRequire } from 'module';
|
|
10
10
|
import { randomUUID } from 'crypto';
|
|
@@ -90,7 +90,7 @@ function resolveApprovalMode(defaults, adapter) {
|
|
|
90
90
|
return adapter?.approvalPolicy ?? defaults.approvalPolicy ?? (defaults.requireApprovalOnRiskyActions ? "gated" : "auto");
|
|
91
91
|
}
|
|
92
92
|
function resolveSandbox(defaults, adapter) {
|
|
93
|
-
return adapter?.sandbox ?? defaults.sandbox ?? "
|
|
93
|
+
return adapter?.sandbox ?? defaults.sandbox ?? "danger-full-access";
|
|
94
94
|
}
|
|
95
95
|
var hooksSchema = z.object({
|
|
96
96
|
/** Shell command run just before the agent starts. */
|
|
@@ -117,6 +117,14 @@ var deciderSchema = z.object({
|
|
|
117
117
|
apiKey: z.string().optional(),
|
|
118
118
|
maxTokens: z.number().int().positive().optional()
|
|
119
119
|
}).strict();
|
|
120
|
+
var pricingRuleSchema = z.object({
|
|
121
|
+
/** Regex (case-insensitive) matched against the model id; first match wins. */
|
|
122
|
+
match: z.string().min(1),
|
|
123
|
+
input: z.number().nonnegative(),
|
|
124
|
+
output: z.number().nonnegative(),
|
|
125
|
+
cacheWrite: z.number().nonnegative().optional(),
|
|
126
|
+
cacheRead: z.number().nonnegative().optional()
|
|
127
|
+
}).strict();
|
|
120
128
|
var configSchema = z.object({
|
|
121
129
|
defaultAdapter: z.string().min(1),
|
|
122
130
|
sessionsDir: z.string().min(1).default(".agent-relay/sessions"),
|
|
@@ -125,6 +133,8 @@ var configSchema = z.object({
|
|
|
125
133
|
adapters: z.record(adapterConfigSchema),
|
|
126
134
|
/** Which decider answers interactive prompts. */
|
|
127
135
|
decider: deciderSchema.optional(),
|
|
136
|
+
/** Override the built-in per-model token pricing (USD per 1M tokens). */
|
|
137
|
+
pricing: z.array(pricingRuleSchema).optional(),
|
|
128
138
|
/** Optional shell-command lifecycle hooks. */
|
|
129
139
|
hooks: hooksSchema.optional()
|
|
130
140
|
}).strict().superRefine((cfg, ctx) => {
|
|
@@ -145,9 +155,10 @@ function createDefaultConfig() {
|
|
|
145
155
|
maxTurns: 20,
|
|
146
156
|
timeoutMs: 18e5,
|
|
147
157
|
idleTimeoutMs: 3e5,
|
|
148
|
-
// Autonomous by default: agents run unattended without asking
|
|
158
|
+
// Autonomous by default: agents run unattended without asking, with full
|
|
159
|
+
// bypass so they can act freely (tighten `sandbox` for guarded runs).
|
|
149
160
|
approvalPolicy: "auto",
|
|
150
|
-
sandbox: "
|
|
161
|
+
sandbox: "danger-full-access",
|
|
151
162
|
requireApprovalOnRiskyActions: false
|
|
152
163
|
},
|
|
153
164
|
// Interactive (PTY) is the default mode: the agent runs in its real TUI and
|
|
@@ -185,7 +196,7 @@ ${detail}`,
|
|
|
185
196
|
return result.data;
|
|
186
197
|
}
|
|
187
198
|
function configPath(rootDir) {
|
|
188
|
-
return
|
|
199
|
+
return path6.resolve(rootDir, CONFIG_FILENAME);
|
|
189
200
|
}
|
|
190
201
|
async function loadConfig(rootDir) {
|
|
191
202
|
const file = configPath(rootDir);
|
|
@@ -225,7 +236,7 @@ function stringifyConfig(config) {
|
|
|
225
236
|
}
|
|
226
237
|
async function saveConfig(rootDir, config) {
|
|
227
238
|
const file = configPath(rootDir);
|
|
228
|
-
await promises.mkdir(
|
|
239
|
+
await promises.mkdir(path6.dirname(file), { recursive: true });
|
|
229
240
|
await promises.writeFile(file, stringifyConfig(config), "utf8");
|
|
230
241
|
return file;
|
|
231
242
|
}
|
|
@@ -240,7 +251,7 @@ async function dirExists(p) {
|
|
|
240
251
|
}
|
|
241
252
|
}
|
|
242
253
|
async function runInit(options) {
|
|
243
|
-
const rootDir =
|
|
254
|
+
const rootDir = path6.resolve(options.rootDir);
|
|
244
255
|
const cfgPath = configPath(rootDir);
|
|
245
256
|
let configCreated = false;
|
|
246
257
|
let configExists = false;
|
|
@@ -255,8 +266,8 @@ async function runInit(options) {
|
|
|
255
266
|
configCreated = true;
|
|
256
267
|
}
|
|
257
268
|
const config = await loadConfig(rootDir);
|
|
258
|
-
const sessionsDir =
|
|
259
|
-
const logsDir =
|
|
269
|
+
const sessionsDir = path6.resolve(rootDir, config.sessionsDir);
|
|
270
|
+
const logsDir = path6.resolve(rootDir, config.logsDir);
|
|
260
271
|
const createdDirs = [];
|
|
261
272
|
for (const dir of [sessionsDir, logsDir]) {
|
|
262
273
|
if (!await dirExists(dir)) {
|
|
@@ -272,6 +283,100 @@ async function runInit(options) {
|
|
|
272
283
|
createdDirs
|
|
273
284
|
};
|
|
274
285
|
}
|
|
286
|
+
async function realish(p) {
|
|
287
|
+
try {
|
|
288
|
+
return await promises.realpath(p);
|
|
289
|
+
} catch {
|
|
290
|
+
return p;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function projectDirCandidates(absCwd) {
|
|
294
|
+
return [absCwd.replace(/[/.]/g, "-"), absCwd.replace(/\//g, "-")];
|
|
295
|
+
}
|
|
296
|
+
async function resolveProjectDir(root, cwd) {
|
|
297
|
+
const abs = await realish(cwd);
|
|
298
|
+
for (const name of projectDirCandidates(abs)) {
|
|
299
|
+
const dir = path6.join(root, name);
|
|
300
|
+
try {
|
|
301
|
+
const st = await promises.stat(dir);
|
|
302
|
+
if (st.isDirectory()) return dir;
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return void 0;
|
|
307
|
+
}
|
|
308
|
+
async function findLatestClaudeTranscript(opts) {
|
|
309
|
+
const root = opts.projectsDir ?? path6.join(os.homedir(), ".claude", "projects");
|
|
310
|
+
const since = opts.sinceMs ?? 0;
|
|
311
|
+
const dir = await resolveProjectDir(root, opts.cwd);
|
|
312
|
+
if (!dir) return void 0;
|
|
313
|
+
let files;
|
|
314
|
+
try {
|
|
315
|
+
files = await promises.readdir(dir);
|
|
316
|
+
} catch {
|
|
317
|
+
return void 0;
|
|
318
|
+
}
|
|
319
|
+
const candidates = [];
|
|
320
|
+
for (const f of files) {
|
|
321
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
322
|
+
const file = path6.join(dir, f);
|
|
323
|
+
try {
|
|
324
|
+
const st = await promises.stat(file);
|
|
325
|
+
if (st.mtimeMs + 2e3 < since) continue;
|
|
326
|
+
candidates.push({ file, mtimeMs: st.mtimeMs });
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
331
|
+
return candidates[0]?.file;
|
|
332
|
+
}
|
|
333
|
+
async function sumClaudeUsage(file) {
|
|
334
|
+
let text;
|
|
335
|
+
try {
|
|
336
|
+
text = await promises.readFile(file, "utf8");
|
|
337
|
+
} catch {
|
|
338
|
+
return void 0;
|
|
339
|
+
}
|
|
340
|
+
let input = 0;
|
|
341
|
+
let output = 0;
|
|
342
|
+
let cacheCreate = 0;
|
|
343
|
+
let cacheRead = 0;
|
|
344
|
+
let model;
|
|
345
|
+
let any = false;
|
|
346
|
+
for (const line of text.split("\n")) {
|
|
347
|
+
if (!line.trim()) continue;
|
|
348
|
+
let rec;
|
|
349
|
+
try {
|
|
350
|
+
rec = JSON.parse(line);
|
|
351
|
+
} catch {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const u = rec.message?.usage;
|
|
355
|
+
if (!u || typeof u !== "object") continue;
|
|
356
|
+
any = true;
|
|
357
|
+
input += u.input_tokens ?? 0;
|
|
358
|
+
output += u.output_tokens ?? 0;
|
|
359
|
+
cacheCreate += u.cache_creation_input_tokens ?? 0;
|
|
360
|
+
cacheRead += u.cache_read_input_tokens ?? 0;
|
|
361
|
+
if (!model && typeof rec.message?.model === "string")
|
|
362
|
+
model = rec.message.model;
|
|
363
|
+
}
|
|
364
|
+
if (!any) return void 0;
|
|
365
|
+
return {
|
|
366
|
+
source: "transcript",
|
|
367
|
+
model,
|
|
368
|
+
inputTokens: input,
|
|
369
|
+
outputTokens: output,
|
|
370
|
+
cacheCreationTokens: cacheCreate,
|
|
371
|
+
cachedInputTokens: cacheRead,
|
|
372
|
+
totalTokens: input + output + cacheCreate + cacheRead
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
async function findLatestClaudeUsage(opts) {
|
|
376
|
+
const file = await findLatestClaudeTranscript(opts);
|
|
377
|
+
if (!file) return void 0;
|
|
378
|
+
return sumClaudeUsage(file);
|
|
379
|
+
}
|
|
275
380
|
|
|
276
381
|
// src/core/util/json.ts
|
|
277
382
|
function safeJsonParse(text) {
|
|
@@ -493,6 +598,7 @@ var CommandDecider = class {
|
|
|
493
598
|
}
|
|
494
599
|
}
|
|
495
600
|
};
|
|
601
|
+
var MIN_API_MAX_TOKENS = 512;
|
|
496
602
|
var ApiDecider = class {
|
|
497
603
|
constructor(opts) {
|
|
498
604
|
this.opts = opts;
|
|
@@ -522,7 +628,7 @@ var ApiDecider = class {
|
|
|
522
628
|
body: JSON.stringify({
|
|
523
629
|
model: this.opts.model ?? "default",
|
|
524
630
|
messages: [{ role: "user", content: render(req) }],
|
|
525
|
-
max_tokens: this.opts.maxTokens ?? 2048,
|
|
631
|
+
max_tokens: Math.max(this.opts.maxTokens ?? 2048, MIN_API_MAX_TOKENS),
|
|
526
632
|
temperature: this.opts.temperature ?? 0,
|
|
527
633
|
stream: false
|
|
528
634
|
}),
|
|
@@ -594,8 +700,8 @@ async function isExecutableFile(file) {
|
|
|
594
700
|
async function which(command) {
|
|
595
701
|
if (!command) return null;
|
|
596
702
|
const exts = pathExtensions();
|
|
597
|
-
if (command.includes(
|
|
598
|
-
const base =
|
|
703
|
+
if (command.includes(path6.sep) || command.includes("/")) {
|
|
704
|
+
const base = path6.resolve(command);
|
|
599
705
|
for (const ext of exts) {
|
|
600
706
|
const candidate = base + ext;
|
|
601
707
|
if (await isExecutableFile(candidate)) return candidate;
|
|
@@ -603,11 +709,11 @@ async function which(command) {
|
|
|
603
709
|
return null;
|
|
604
710
|
}
|
|
605
711
|
const pathEnv = process.env.PATH ?? process.env.Path ?? "";
|
|
606
|
-
const dirs = pathEnv.split(
|
|
607
|
-
const fallbacks = process.platform === "win32" ? [] : ["/usr/local/bin", "/opt/homebrew/bin",
|
|
712
|
+
const dirs = pathEnv.split(path6.delimiter).filter(Boolean);
|
|
713
|
+
const fallbacks = process.platform === "win32" ? [] : ["/usr/local/bin", "/opt/homebrew/bin", path6.join(os.homedir(), ".local/bin")];
|
|
608
714
|
for (const dir of [...dirs, ...fallbacks]) {
|
|
609
715
|
for (const ext of exts) {
|
|
610
|
-
const candidate =
|
|
716
|
+
const candidate = path6.join(dir, command + ext);
|
|
611
717
|
if (await isExecutableFile(candidate)) return candidate;
|
|
612
718
|
}
|
|
613
719
|
}
|
|
@@ -733,11 +839,11 @@ function ensurePtyHelperExecutable() {
|
|
|
733
839
|
if (process.platform === "win32") return;
|
|
734
840
|
try {
|
|
735
841
|
const require2 = createRequire(import.meta.url);
|
|
736
|
-
const root =
|
|
842
|
+
const root = path6.dirname(require2.resolve("node-pty/package.json"));
|
|
737
843
|
const candidates = [
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
844
|
+
path6.join(root, "build", "Release", "spawn-helper"),
|
|
845
|
+
path6.join(root, "build", "Debug", "spawn-helper"),
|
|
846
|
+
path6.join(
|
|
741
847
|
root,
|
|
742
848
|
"prebuilds",
|
|
743
849
|
`${process.platform}-${process.arch}`,
|
|
@@ -899,6 +1005,7 @@ function runPtySession(opts, ctx) {
|
|
|
899
1005
|
let handling = false;
|
|
900
1006
|
let quitting = false;
|
|
901
1007
|
let settled = false;
|
|
1008
|
+
let usage;
|
|
902
1009
|
let finished = false;
|
|
903
1010
|
let disposeData;
|
|
904
1011
|
let disposeExit;
|
|
@@ -915,6 +1022,7 @@ function runPtySession(opts, ctx) {
|
|
|
915
1022
|
const triggerQuit = (reason) => {
|
|
916
1023
|
if (finished || quitting) return;
|
|
917
1024
|
quitting = true;
|
|
1025
|
+
settled = true;
|
|
918
1026
|
if (completionTimer) clearTimeout(completionTimer);
|
|
919
1027
|
completionTimer = void 0;
|
|
920
1028
|
emit("status", `task appears complete (${reason})`);
|
|
@@ -994,6 +1102,10 @@ function runPtySession(opts, ctx) {
|
|
|
994
1102
|
};
|
|
995
1103
|
const onSettle = async () => {
|
|
996
1104
|
if (finished || handling || quitting) return;
|
|
1105
|
+
if (opts.scrapeUsage) {
|
|
1106
|
+
const u = opts.scrapeUsage(cleanTerminalText(buffer));
|
|
1107
|
+
if (u) usage = { ...usage, ...u };
|
|
1108
|
+
}
|
|
997
1109
|
if (pendingComment !== void 0) {
|
|
998
1110
|
const comment = pendingComment;
|
|
999
1111
|
pendingComment = void 0;
|
|
@@ -1130,7 +1242,14 @@ function runPtySession(opts, ctx) {
|
|
|
1130
1242
|
exitCode,
|
|
1131
1243
|
error,
|
|
1132
1244
|
sessionRef: void 0,
|
|
1133
|
-
|
|
1245
|
+
// `completedCleanly` is an unambiguous success flag (exit 0, not aborted/
|
|
1246
|
+
// error) — use it instead of `settled` for "did the run finish well?".
|
|
1247
|
+
meta: {
|
|
1248
|
+
interactions,
|
|
1249
|
+
settled,
|
|
1250
|
+
completedCleanly: success,
|
|
1251
|
+
...usage ? { usage } : {}
|
|
1252
|
+
}
|
|
1134
1253
|
});
|
|
1135
1254
|
});
|
|
1136
1255
|
if (ctx.signal.aborted) onAbort();
|
|
@@ -1230,8 +1349,17 @@ var InteractivePtyAdapter = class {
|
|
|
1230
1349
|
else args.push(input.prompt);
|
|
1231
1350
|
return this.spawn(args, initialInput, input, ctx);
|
|
1232
1351
|
}
|
|
1233
|
-
spawn(args, initialInput, input, ctx, extra) {
|
|
1352
|
+
async spawn(args, initialInput, input, ctx, extra) {
|
|
1234
1353
|
const decider = ctx.decider ?? new RuleDecider();
|
|
1354
|
+
try {
|
|
1355
|
+
const resolved = await which(this.cfg.command);
|
|
1356
|
+
ctx.onEvent({
|
|
1357
|
+
type: "status",
|
|
1358
|
+
timestamp: (this.cfg.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
1359
|
+
text: resolved ? `resolved ${this.cfg.command} \u2192 ${resolved}` : `${this.cfg.command} not found on PATH; relying on spawn-time resolution`
|
|
1360
|
+
});
|
|
1361
|
+
} catch {
|
|
1362
|
+
}
|
|
1235
1363
|
return runPtySession(
|
|
1236
1364
|
{
|
|
1237
1365
|
command: this.cfg.command,
|
|
@@ -1244,6 +1372,7 @@ var InteractivePtyAdapter = class {
|
|
|
1244
1372
|
completionPattern: this.cfg.completionPattern,
|
|
1245
1373
|
completionIdleMs: input.completionIdleMs ?? this.cfg.completionIdleMs,
|
|
1246
1374
|
workingPattern: this.cfg.workingPattern,
|
|
1375
|
+
scrapeUsage: this.cfg.scrapeUsage,
|
|
1247
1376
|
quitKeys: this.cfg.quitKeys,
|
|
1248
1377
|
setup: extra?.setup,
|
|
1249
1378
|
promptAfterSetup: extra?.promptAfterSetup,
|
|
@@ -1261,6 +1390,17 @@ var InteractivePtyAdapter = class {
|
|
|
1261
1390
|
};
|
|
1262
1391
|
|
|
1263
1392
|
// src/adapters/interactive/claude-interactive.ts
|
|
1393
|
+
function scrapeClaudeStatusLine(text) {
|
|
1394
|
+
const u = { source: "status-line" };
|
|
1395
|
+
const ctx = text.match(/context[^\n]*?(\d{1,3})\s*%/i);
|
|
1396
|
+
if (ctx) {
|
|
1397
|
+
u.contextPercent = Number(ctx[1]);
|
|
1398
|
+
u.raw = ctx[0].trim().replace(/\s+/g, " ");
|
|
1399
|
+
}
|
|
1400
|
+
const cost = text.match(/session\s*\$\s*([\d.]+)/i);
|
|
1401
|
+
if (cost) u.subscriptionSessionCostUsd = Number(cost[1]);
|
|
1402
|
+
return u.contextPercent !== void 0 || u.subscriptionSessionCostUsd !== void 0 ? u : void 0;
|
|
1403
|
+
}
|
|
1264
1404
|
var DEFINITION = {
|
|
1265
1405
|
name: "claude",
|
|
1266
1406
|
type: "claude",
|
|
@@ -1269,6 +1409,9 @@ var DEFINITION = {
|
|
|
1269
1409
|
supportsResume: true
|
|
1270
1410
|
};
|
|
1271
1411
|
var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends InteractivePtyAdapter {
|
|
1412
|
+
clock;
|
|
1413
|
+
/** Override the projects root (~/.claude/projects) for tests. */
|
|
1414
|
+
projectsDir;
|
|
1272
1415
|
constructor(opts = {}) {
|
|
1273
1416
|
super({
|
|
1274
1417
|
definition: DEFINITION,
|
|
@@ -1285,14 +1428,14 @@ var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends Interacti
|
|
|
1285
1428
|
if (input.approvalMode === "readonly")
|
|
1286
1429
|
return [...head, "--permission-mode", "plan"];
|
|
1287
1430
|
if (input.approvalMode === "gated")
|
|
1288
|
-
return [...head, "--permission-mode", "
|
|
1431
|
+
return [...head, "--permission-mode", "default"];
|
|
1289
1432
|
return [...head, "--dangerously-skip-permissions"];
|
|
1290
1433
|
},
|
|
1291
1434
|
// Resume the most recent conversation in the cwd and send the follow-up
|
|
1292
1435
|
// prompt. (`--continue` picks the latest session; the native id is not
|
|
1293
1436
|
// captured in PTY mode, so a specific `--resume <id>` is not used.)
|
|
1294
1437
|
resumeArgs: (input) => {
|
|
1295
|
-
const mode = input.approvalMode === "readonly" ? ["--permission-mode", "plan"] : input.approvalMode === "gated" ? ["--permission-mode", "
|
|
1438
|
+
const mode = input.approvalMode === "readonly" ? ["--permission-mode", "plan"] : input.approvalMode === "gated" ? ["--permission-mode", "default"] : ["--dangerously-skip-permissions"];
|
|
1296
1439
|
return ["--continue", "--effort", "xhigh", ...mode];
|
|
1297
1440
|
},
|
|
1298
1441
|
// Unattended ultracode: once Claude is idle (past the trust dialog), type
|
|
@@ -1306,6 +1449,10 @@ var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends Interacti
|
|
|
1306
1449
|
approvalPattern: /(allow|grant|permission|approve|trust|proceed|do you want)[^\n]{0,60}\?|\[y\/n\]|\(y\/n\)/i
|
|
1307
1450
|
},
|
|
1308
1451
|
keymap: new ArrowKeymap(),
|
|
1452
|
+
// SUPPLEMENT only: grab context% / session $ from the status line when it
|
|
1453
|
+
// is enabled. The authoritative token counts come from the transcript in
|
|
1454
|
+
// run() below and overwrite these on the same meta.usage object.
|
|
1455
|
+
scrapeUsage: scrapeClaudeStatusLine,
|
|
1309
1456
|
// Claude's TUI stays open after a task; quit once it's been idle a while.
|
|
1310
1457
|
completionIdleMs: 8e3,
|
|
1311
1458
|
// While Claude is ACTIVELY working it shows "(esc to interrupt)" — that is
|
|
@@ -1319,6 +1466,32 @@ var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends Interacti
|
|
|
1319
1466
|
workingPattern: /interrupt/i,
|
|
1320
1467
|
quitKeys: ""
|
|
1321
1468
|
});
|
|
1469
|
+
this.clock = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
1470
|
+
this.projectsDir = opts.projectsDir;
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Run Claude, then read AUTHORITATIVE token usage from its session transcript
|
|
1474
|
+
* (~/.claude/projects/<cwd>/<id>.jsonl) and surface it as `meta.usage`. This is
|
|
1475
|
+
* device-independent — it works regardless of whether the user has a usage
|
|
1476
|
+
* status line — and overwrites the best-effort status-line scrape's token
|
|
1477
|
+
* figures while keeping its context%/cost extras. Best-effort: if no transcript
|
|
1478
|
+
* is found, the status-line usage (if any) is left as-is.
|
|
1479
|
+
*/
|
|
1480
|
+
async run(input, ctx) {
|
|
1481
|
+
const startedAt = this.clock().getTime();
|
|
1482
|
+
const result = await super.run(input, ctx);
|
|
1483
|
+
if (result.error) return result;
|
|
1484
|
+
const transcript = await findLatestClaudeUsage({
|
|
1485
|
+
cwd: input.cwd,
|
|
1486
|
+
sinceMs: startedAt,
|
|
1487
|
+
projectsDir: this.projectsDir
|
|
1488
|
+
});
|
|
1489
|
+
if (!transcript) return result;
|
|
1490
|
+
const prior = result.meta?.usage ?? {};
|
|
1491
|
+
return {
|
|
1492
|
+
...result,
|
|
1493
|
+
meta: { ...result.meta, usage: { ...prior, ...transcript } }
|
|
1494
|
+
};
|
|
1322
1495
|
}
|
|
1323
1496
|
static fromConfig(config) {
|
|
1324
1497
|
return new _ClaudeInteractiveAdapter({
|
|
@@ -1328,7 +1501,7 @@ var ClaudeInteractiveAdapter = class _ClaudeInteractiveAdapter extends Interacti
|
|
|
1328
1501
|
}
|
|
1329
1502
|
};
|
|
1330
1503
|
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$/;
|
|
1331
|
-
async function
|
|
1504
|
+
async function realish2(p) {
|
|
1332
1505
|
try {
|
|
1333
1506
|
return await promises.realpath(p);
|
|
1334
1507
|
} catch {
|
|
@@ -1344,7 +1517,7 @@ async function listRollouts(root) {
|
|
|
1344
1517
|
return out;
|
|
1345
1518
|
}
|
|
1346
1519
|
for (const y of years) {
|
|
1347
|
-
const yDir =
|
|
1520
|
+
const yDir = path6.join(root, y);
|
|
1348
1521
|
let months;
|
|
1349
1522
|
try {
|
|
1350
1523
|
months = await promises.readdir(yDir);
|
|
@@ -1352,7 +1525,7 @@ async function listRollouts(root) {
|
|
|
1352
1525
|
continue;
|
|
1353
1526
|
}
|
|
1354
1527
|
for (const m of months) {
|
|
1355
|
-
const mDir =
|
|
1528
|
+
const mDir = path6.join(yDir, m);
|
|
1356
1529
|
let days;
|
|
1357
1530
|
try {
|
|
1358
1531
|
days = await promises.readdir(mDir);
|
|
@@ -1360,7 +1533,7 @@ async function listRollouts(root) {
|
|
|
1360
1533
|
continue;
|
|
1361
1534
|
}
|
|
1362
1535
|
for (const d of days) {
|
|
1363
|
-
const dDir =
|
|
1536
|
+
const dDir = path6.join(mDir, d);
|
|
1364
1537
|
let files;
|
|
1365
1538
|
try {
|
|
1366
1539
|
files = await promises.readdir(dDir);
|
|
@@ -1369,7 +1542,7 @@ async function listRollouts(root) {
|
|
|
1369
1542
|
}
|
|
1370
1543
|
for (const f of files) {
|
|
1371
1544
|
if (!f.startsWith("rollout-") || !f.endsWith(".jsonl")) continue;
|
|
1372
|
-
const file =
|
|
1545
|
+
const file = path6.join(dDir, f);
|
|
1373
1546
|
try {
|
|
1374
1547
|
const st = await promises.stat(file);
|
|
1375
1548
|
out.push({ file, mtimeMs: st.mtimeMs });
|
|
@@ -1402,25 +1575,55 @@ async function readSessionMeta(file) {
|
|
|
1402
1575
|
}
|
|
1403
1576
|
}
|
|
1404
1577
|
function uuidFromRolloutFilename(file) {
|
|
1405
|
-
const m = ROLLOUT_UUID_RE.exec(
|
|
1578
|
+
const m = ROLLOUT_UUID_RE.exec(path6.basename(file));
|
|
1406
1579
|
return m?.[1];
|
|
1407
1580
|
}
|
|
1408
|
-
async function
|
|
1409
|
-
const root = opts.sessionsDir ??
|
|
1581
|
+
async function findLatestCodexRollout(opts) {
|
|
1582
|
+
const root = opts.sessionsDir ?? path6.join(os.homedir(), ".codex", "sessions");
|
|
1410
1583
|
const since = opts.sinceMs ?? 0;
|
|
1411
|
-
const targetCwd = await
|
|
1584
|
+
const targetCwd = await realish2(opts.cwd);
|
|
1412
1585
|
const rollouts = await listRollouts(root);
|
|
1413
1586
|
for (const { file, mtimeMs } of rollouts) {
|
|
1414
1587
|
if (mtimeMs + 2e3 < since) continue;
|
|
1415
1588
|
const meta = await readSessionMeta(file);
|
|
1416
1589
|
if (!meta?.cwd) continue;
|
|
1417
|
-
const metaCwd = await
|
|
1590
|
+
const metaCwd = await realish2(meta.cwd);
|
|
1418
1591
|
if (metaCwd !== targetCwd) continue;
|
|
1419
1592
|
const id = meta.id ?? uuidFromRolloutFilename(file);
|
|
1420
|
-
|
|
1593
|
+
return { file, id };
|
|
1421
1594
|
}
|
|
1422
1595
|
return void 0;
|
|
1423
1596
|
}
|
|
1597
|
+
async function sumCodexUsage(file) {
|
|
1598
|
+
let text;
|
|
1599
|
+
try {
|
|
1600
|
+
text = await promises.readFile(file, "utf8");
|
|
1601
|
+
} catch {
|
|
1602
|
+
return void 0;
|
|
1603
|
+
}
|
|
1604
|
+
let total;
|
|
1605
|
+
for (const line of text.split("\n")) {
|
|
1606
|
+
if (!line.includes("token_count")) continue;
|
|
1607
|
+
let rec;
|
|
1608
|
+
try {
|
|
1609
|
+
rec = JSON.parse(line);
|
|
1610
|
+
} catch {
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
if (rec.payload?.type !== "token_count") continue;
|
|
1614
|
+
const t = rec.payload.info?.total_token_usage;
|
|
1615
|
+
if (t && typeof t === "object") total = t;
|
|
1616
|
+
}
|
|
1617
|
+
if (!total) return void 0;
|
|
1618
|
+
return {
|
|
1619
|
+
source: "transcript",
|
|
1620
|
+
inputTokens: total.input_tokens,
|
|
1621
|
+
cachedInputTokens: total.cached_input_tokens,
|
|
1622
|
+
outputTokens: total.output_tokens,
|
|
1623
|
+
reasoningTokens: total.reasoning_output_tokens,
|
|
1624
|
+
totalTokens: total.total_tokens
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1424
1627
|
|
|
1425
1628
|
// src/adapters/interactive/codex-interactive.ts
|
|
1426
1629
|
var DEFINITION2 = {
|
|
@@ -1442,7 +1645,7 @@ var CodexInteractiveAdapter = class _CodexInteractiveAdapter extends Interactive
|
|
|
1442
1645
|
now: opts.now,
|
|
1443
1646
|
installHint: "Install the Codex CLI (`npm i -g @openai/codex`), run `codex login`, and ensure `codex` is on your PATH.",
|
|
1444
1647
|
preArgs: (input) => {
|
|
1445
|
-
const sandbox = input.approvalMode === "readonly" ? "read-only" : input.sandbox ?? "
|
|
1648
|
+
const sandbox = input.approvalMode === "readonly" ? "read-only" : input.sandbox ?? "danger-full-access";
|
|
1446
1649
|
const approval = input.approvalMode === "gated" ? "on-request" : "never";
|
|
1447
1650
|
return ["-s", sandbox, "-a", approval];
|
|
1448
1651
|
},
|
|
@@ -1478,29 +1681,28 @@ var CodexInteractiveAdapter = class _CodexInteractiveAdapter extends Interactive
|
|
|
1478
1681
|
this.sessionsDir = opts.sessionsDir;
|
|
1479
1682
|
}
|
|
1480
1683
|
/**
|
|
1481
|
-
* Run Codex, then
|
|
1482
|
-
*
|
|
1483
|
-
*
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1684
|
+
* Run Codex, then read its rollout for this cwd to capture (a) the NATIVE
|
|
1685
|
+
* session id (the rollout UUID) for `sessionRef` so a later resume can use
|
|
1686
|
+
* `codex resume <id> "<prompt>"`, and (b) authoritative token usage for
|
|
1687
|
+
* `meta.usage` (device-independent — from Codex's own log, not the TUI). Both
|
|
1688
|
+
* are best-effort: if no rollout matches (or any I/O fails) the result is
|
|
1689
|
+
* returned unchanged, so the run still resumes via the `--last` fallback.
|
|
1486
1690
|
*/
|
|
1487
1691
|
async run(input, ctx) {
|
|
1488
1692
|
const startedAt = this.clock().getTime();
|
|
1489
1693
|
const result = await super.run(input, ctx);
|
|
1490
1694
|
if (result.error) return result;
|
|
1491
|
-
const
|
|
1695
|
+
const rollout = await findLatestCodexRollout({
|
|
1492
1696
|
cwd: input.cwd,
|
|
1493
1697
|
sinceMs: startedAt,
|
|
1494
1698
|
sessionsDir: this.sessionsDir
|
|
1495
1699
|
});
|
|
1496
|
-
if (!
|
|
1700
|
+
if (!rollout) return result;
|
|
1701
|
+
const usage = await sumCodexUsage(rollout.file);
|
|
1497
1702
|
return {
|
|
1498
1703
|
...result,
|
|
1499
|
-
sessionRef: {
|
|
1500
|
-
|
|
1501
|
-
nativeSessionId,
|
|
1502
|
-
resumable: true
|
|
1503
|
-
}
|
|
1704
|
+
sessionRef: rollout.id ? { adapter: this.definition.name, nativeSessionId: rollout.id, resumable: true } : result.sessionRef,
|
|
1705
|
+
meta: usage ? { ...result.meta, usage } : result.meta
|
|
1504
1706
|
};
|
|
1505
1707
|
}
|
|
1506
1708
|
static fromConfig(config) {
|
|
@@ -1780,11 +1982,11 @@ async function commandCheck(name, command, installHint) {
|
|
|
1780
1982
|
};
|
|
1781
1983
|
}
|
|
1782
1984
|
async function runDoctor(options) {
|
|
1783
|
-
const rootDir =
|
|
1985
|
+
const rootDir = path6.resolve(options.rootDir);
|
|
1784
1986
|
const checks = [];
|
|
1785
1987
|
checks.push(checkNode());
|
|
1786
|
-
let sessionsDir =
|
|
1787
|
-
let logsDir =
|
|
1988
|
+
let sessionsDir = path6.resolve(rootDir, ".agent-relay/sessions");
|
|
1989
|
+
let logsDir = path6.resolve(rootDir, ".agent-relay/logs");
|
|
1788
1990
|
try {
|
|
1789
1991
|
const config = await loadConfig(rootDir);
|
|
1790
1992
|
checks.push({
|
|
@@ -1792,8 +1994,8 @@ async function runDoctor(options) {
|
|
|
1792
1994
|
level: "ok",
|
|
1793
1995
|
detail: `Valid config (defaultAdapter: ${config.defaultAdapter})`
|
|
1794
1996
|
});
|
|
1795
|
-
sessionsDir =
|
|
1796
|
-
logsDir =
|
|
1997
|
+
sessionsDir = path6.resolve(rootDir, config.sessionsDir);
|
|
1998
|
+
logsDir = path6.resolve(rootDir, config.logsDir);
|
|
1797
1999
|
} catch (err) {
|
|
1798
2000
|
const message = err instanceof Error ? err.message : String(err);
|
|
1799
2001
|
const missing = message.includes("not found");
|
|
@@ -1909,7 +2111,7 @@ var RunLogger = class {
|
|
|
1909
2111
|
if (this.opts.maxBytes && this.bytes >= this.opts.maxBytes) {
|
|
1910
2112
|
if (!this.truncated) {
|
|
1911
2113
|
this.truncated = true;
|
|
1912
|
-
|
|
2114
|
+
fs7.appendFileSync(
|
|
1913
2115
|
this.logFile,
|
|
1914
2116
|
`... [log truncated at ${this.opts.maxBytes} bytes \u2014 raise --max-log-bytes for more]
|
|
1915
2117
|
`
|
|
@@ -1917,12 +2119,12 @@ var RunLogger = class {
|
|
|
1917
2119
|
}
|
|
1918
2120
|
return;
|
|
1919
2121
|
}
|
|
1920
|
-
|
|
2122
|
+
fs7.appendFileSync(this.logFile, text);
|
|
1921
2123
|
this.bytes += Buffer.byteLength(text);
|
|
1922
2124
|
}
|
|
1923
2125
|
/** Ensure the log directory exists and write the run header. */
|
|
1924
2126
|
start(header) {
|
|
1925
|
-
|
|
2127
|
+
fs7.mkdirSync(path6.dirname(this.logFile), { recursive: true });
|
|
1926
2128
|
const lines = [
|
|
1927
2129
|
"================ agent-relay run ================",
|
|
1928
2130
|
`sessionId : ${header.sessionId}`,
|
|
@@ -1976,6 +2178,27 @@ var RunLogger = class {
|
|
|
1976
2178
|
this.append(lines.join("\n"));
|
|
1977
2179
|
}
|
|
1978
2180
|
};
|
|
2181
|
+
|
|
2182
|
+
// src/core/pricing.ts
|
|
2183
|
+
var DEFAULT_PRICING = [
|
|
2184
|
+
{ match: /opus/i, pricing: { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
|
|
2185
|
+
{ match: /sonnet/i, pricing: { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 } },
|
|
2186
|
+
{ match: /haiku/i, pricing: { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 } }
|
|
2187
|
+
];
|
|
2188
|
+
function pricingForModel(model, overrides = []) {
|
|
2189
|
+
if (!model) return void 0;
|
|
2190
|
+
for (const rule of [...overrides, ...DEFAULT_PRICING]) {
|
|
2191
|
+
if (rule.match.test(model)) return rule.pricing;
|
|
2192
|
+
}
|
|
2193
|
+
return void 0;
|
|
2194
|
+
}
|
|
2195
|
+
function computeCostUsd(usage, overrides = []) {
|
|
2196
|
+
const p = pricingForModel(usage.model, overrides);
|
|
2197
|
+
if (!p) return null;
|
|
2198
|
+
const M = 1e6;
|
|
2199
|
+
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;
|
|
2200
|
+
return Math.round(cost * 1e6) / 1e6;
|
|
2201
|
+
}
|
|
1979
2202
|
var SessionManager = class {
|
|
1980
2203
|
constructor(sessionsDir) {
|
|
1981
2204
|
this.sessionsDir = sessionsDir;
|
|
@@ -1983,7 +2206,7 @@ var SessionManager = class {
|
|
|
1983
2206
|
sessionsDir;
|
|
1984
2207
|
/** Absolute path to a session's JSON metadata file. */
|
|
1985
2208
|
filePath(sessionId) {
|
|
1986
|
-
return
|
|
2209
|
+
return path6.join(this.sessionsDir, `${sessionId}.json`);
|
|
1987
2210
|
}
|
|
1988
2211
|
/** Build a fresh session record in the `created` state (not yet persisted). */
|
|
1989
2212
|
create(input) {
|
|
@@ -2050,7 +2273,7 @@ var SessionManager = class {
|
|
|
2050
2273
|
if (!entry.endsWith(".json")) continue;
|
|
2051
2274
|
try {
|
|
2052
2275
|
const raw = await promises.readFile(
|
|
2053
|
-
|
|
2276
|
+
path6.join(this.sessionsDir, entry),
|
|
2054
2277
|
"utf8"
|
|
2055
2278
|
);
|
|
2056
2279
|
metas.push(JSON.parse(raw));
|
|
@@ -2074,12 +2297,12 @@ async function runAgent(options) {
|
|
|
2074
2297
|
}
|
|
2075
2298
|
const adapter = options.resolveAdapter(adapterName, config);
|
|
2076
2299
|
const detector = options.detector ?? new CompositeCompletionDetector();
|
|
2077
|
-
const rootDir =
|
|
2078
|
-
const cwd =
|
|
2079
|
-
const sessionsDir =
|
|
2080
|
-
const logsDir =
|
|
2300
|
+
const rootDir = path6.resolve(options.rootDir);
|
|
2301
|
+
const cwd = path6.resolve(options.cwd ?? rootDir);
|
|
2302
|
+
const sessionsDir = path6.resolve(rootDir, config.sessionsDir);
|
|
2303
|
+
const logsDir = path6.resolve(rootDir, config.logsDir);
|
|
2081
2304
|
const sessionId = options.sessionId ?? randomUUID();
|
|
2082
|
-
const logFile =
|
|
2305
|
+
const logFile = path6.join(logsDir, `${sessionId}.log`);
|
|
2083
2306
|
const sessions = new SessionManager(sessionsDir);
|
|
2084
2307
|
const startedAt = iso();
|
|
2085
2308
|
const session = sessions.create({
|
|
@@ -2297,6 +2520,28 @@ async function runAgent(options) {
|
|
|
2297
2520
|
}
|
|
2298
2521
|
externalSignal?.removeEventListener("abort", onExternalAbort);
|
|
2299
2522
|
}
|
|
2523
|
+
if (result?.meta && typeof result.meta === "object") {
|
|
2524
|
+
const usage = result.meta.usage;
|
|
2525
|
+
if (usage && (usage.inputTokens != null || usage.outputTokens != null)) {
|
|
2526
|
+
const overrides = (config.pricing ?? []).map((r) => ({
|
|
2527
|
+
match: new RegExp(r.match, "i"),
|
|
2528
|
+
pricing: {
|
|
2529
|
+
input: r.input,
|
|
2530
|
+
output: r.output,
|
|
2531
|
+
cacheWrite: r.cacheWrite,
|
|
2532
|
+
cacheRead: r.cacheRead
|
|
2533
|
+
}
|
|
2534
|
+
}));
|
|
2535
|
+
usage.costUsd = computeCostUsd(usage, overrides);
|
|
2536
|
+
if (usage.costUsd === null) {
|
|
2537
|
+
onEvent({
|
|
2538
|
+
type: "status",
|
|
2539
|
+
timestamp: iso(),
|
|
2540
|
+
text: `usage: no price for model ${usage.model ? `"${usage.model}"` : "(unknown)"} \u2192 costUsd=null (set config.pricing to override)`
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2300
2545
|
const status = detector.detect({ result, events, abortReason, error: runError }) ?? "failed";
|
|
2301
2546
|
const endedAt = iso();
|
|
2302
2547
|
const error = runError && !result?.error ? { message: runError.message, code: "ADAPTER_THREW" } : result?.error ?? (abortReason === "timeout" || abortReason === "idle" ? {
|
|
@@ -2375,7 +2620,7 @@ async function resolvePrompt(options) {
|
|
|
2375
2620
|
);
|
|
2376
2621
|
}
|
|
2377
2622
|
if (options.promptFile) {
|
|
2378
|
-
const file =
|
|
2623
|
+
const file = path6.resolve(options.rootDir, options.promptFile);
|
|
2379
2624
|
try {
|
|
2380
2625
|
const text = await promises.readFile(file, "utf8");
|
|
2381
2626
|
if (!text.trim()) {
|
|
@@ -2402,7 +2647,7 @@ async function resolvePrompt(options) {
|
|
|
2402
2647
|
);
|
|
2403
2648
|
}
|
|
2404
2649
|
async function runCommand(options) {
|
|
2405
|
-
const rootDir =
|
|
2650
|
+
const rootDir = path6.resolve(options.rootDir);
|
|
2406
2651
|
const config = options.config ?? await loadConfigOrDefault(rootDir, options.onDefaultConfig);
|
|
2407
2652
|
const prompt = await resolvePrompt({
|
|
2408
2653
|
prompt: options.prompt,
|
|
@@ -2434,9 +2679,9 @@ async function runCommand(options) {
|
|
|
2434
2679
|
});
|
|
2435
2680
|
}
|
|
2436
2681
|
async function resumeCommand(options) {
|
|
2437
|
-
const rootDir =
|
|
2682
|
+
const rootDir = path6.resolve(options.rootDir);
|
|
2438
2683
|
const config = options.config ?? await loadConfigOrDefault(rootDir, options.onDefaultConfig);
|
|
2439
|
-
const sessionsDir =
|
|
2684
|
+
const sessionsDir = path6.resolve(rootDir, config.sessionsDir);
|
|
2440
2685
|
const sessions = new SessionManager(sessionsDir);
|
|
2441
2686
|
const session = await sessions.load(options.sessionId);
|
|
2442
2687
|
const resolveAdapter = options.resolveAdapter ?? createAdapterFactory();
|
|
@@ -2487,9 +2732,9 @@ async function resumeCommand(options) {
|
|
|
2487
2732
|
return { session, resumable, resumed: true, outcome };
|
|
2488
2733
|
}
|
|
2489
2734
|
async function resolveManager(opts) {
|
|
2490
|
-
const rootDir =
|
|
2735
|
+
const rootDir = path6.resolve(opts.rootDir);
|
|
2491
2736
|
const config = await loadConfigOrDefault(rootDir, opts.onDefaultConfig);
|
|
2492
|
-
const sessionsDir =
|
|
2737
|
+
const sessionsDir = path6.resolve(rootDir, config.sessionsDir);
|
|
2493
2738
|
const sessions = new SessionManager(sessionsDir);
|
|
2494
2739
|
return { sessions, metas: await sessions.list() };
|
|
2495
2740
|
}
|
|
@@ -2810,7 +3055,7 @@ program.command("run").description("Run a prompt through an agent adapter").opti
|
|
|
2810
3055
|
"api decider: OpenAI-compatible chat-completions URL"
|
|
2811
3056
|
).option("--decider-model <model>", "api decider: model name").option("--decider-key <key>", "api decider: bearer API key").option(
|
|
2812
3057
|
"--decider-max-tokens <n>",
|
|
2813
|
-
"api decider: max tokens",
|
|
3058
|
+
"api decider: max tokens (default 2048; floor 512 \u2014 reasoning models need headroom)",
|
|
2814
3059
|
positiveIntArg("--decider-max-tokens")
|
|
2815
3060
|
).option(
|
|
2816
3061
|
"--decider-timeout-ms <n>",
|
|
@@ -2882,7 +3127,7 @@ program.command("resume").description("Inspect a prior session and optionally se
|
|
|
2882
3127
|
"Decider override: rule | always-approve | command | api"
|
|
2883
3128
|
).option("--decider-command <cmdline>", "command decider: full command line").option("--decider-url <url>", "api decider: chat-completions URL").option("--decider-model <model>", "api decider: model name").option("--decider-key <key>", "api decider: bearer API key").option(
|
|
2884
3129
|
"--decider-max-tokens <n>",
|
|
2885
|
-
"api decider: max tokens",
|
|
3130
|
+
"api decider: max tokens (default 2048; floor 512 \u2014 reasoning models need headroom)",
|
|
2886
3131
|
positiveIntArg("--decider-max-tokens")
|
|
2887
3132
|
).option(
|
|
2888
3133
|
"--decider-timeout-ms <n>",
|