@phnx-labs/agents-cli 1.22.26 → 1.22.27
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/CHANGELOG.md +244 -0
- package/README.md +18 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +17 -16
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +47 -1
- package/dist/commands/sessions.js +345 -37
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +3 -3
- package/dist/index.js +16 -20
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +27 -1
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/exec.bench.d.ts +1 -0
- package/dist/lib/exec.bench.js +186 -0
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +10 -6
- package/dist/lib/hosts/dispatch.js +7 -3
- package/dist/lib/hosts/passthrough.d.ts +22 -0
- package/dist/lib/hosts/passthrough.js +14 -8
- package/dist/lib/hosts/progress.d.ts +2 -1
- package/dist/lib/hosts/progress.js +4 -3
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +4 -2
- package/dist/lib/hosts/ready.js +24 -8
- package/dist/lib/hosts/reconcile.d.ts +1 -1
- package/dist/lib/hosts/reconcile.js +16 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +90 -34
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +3 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +2 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/models.d.ts +30 -1
- package/dist/lib/models.js +42 -144
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/redact.js +8 -3
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.js +3 -2
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/db.d.ts +19 -2
- package/dist/lib/session/db.js +54 -5
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/lib/models.js
CHANGED
|
@@ -112,51 +112,8 @@ export function locateModelSource(agent, version) {
|
|
|
112
112
|
}
|
|
113
113
|
return null;
|
|
114
114
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// 0.42+ inlines the same constants into a minified chunk under @google/gemini-cli/bundle/.
|
|
118
|
-
const modelsJs = path.join(versionDir, 'node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'config', 'models.js');
|
|
119
|
-
if (fs.existsSync(modelsJs))
|
|
120
|
-
return { path: modelsJs, kind: 'js' };
|
|
121
|
-
const bundleDir = path.join(versionDir, 'node_modules', '@google', 'gemini-cli', 'bundle');
|
|
122
|
-
if (fs.existsSync(bundleDir)) {
|
|
123
|
-
try {
|
|
124
|
-
const entries = fs.readdirSync(bundleDir);
|
|
125
|
-
// The model constants live in a single chunk; pick the first .js file
|
|
126
|
-
// whose contents contain VALID_GEMINI_MODELS. Skip subdirectories.
|
|
127
|
-
for (const name of entries) {
|
|
128
|
-
if (!name.endsWith('.js'))
|
|
129
|
-
continue;
|
|
130
|
-
const full = path.join(bundleDir, name);
|
|
131
|
-
let stat;
|
|
132
|
-
try {
|
|
133
|
-
stat = fs.statSync(full);
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (!stat.isFile())
|
|
139
|
-
continue;
|
|
140
|
-
// Most chunks just re-export the constants; only the defining chunk
|
|
141
|
-
// actually has `var VALID_GEMINI_MODELS = ... new Set(`. Match that
|
|
142
|
-
// shape so we pick the chunk that the extractor can parse.
|
|
143
|
-
let body;
|
|
144
|
-
try {
|
|
145
|
-
body = fs.readFileSync(full, 'utf-8');
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if (/var\s+VALID_GEMINI_MODELS\s*=[^\n]*new\s+Set/.test(body) &&
|
|
151
|
-
/var\s+DEFAULT_GEMINI_MODEL\s*=/.test(body)) {
|
|
152
|
-
return { path: full, kind: 'js' };
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
catch { /* unreadable bundle dir */ }
|
|
157
|
-
}
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
115
|
+
// gemini is hard-deprecated (no launch path left to validate a model
|
|
116
|
+
// against), so its bundle is deliberately not parsed for a catalog here.
|
|
160
117
|
if (agent === 'opencode') {
|
|
161
118
|
// The `opencode` shim under node_modules/.bin dispatches to a platform-
|
|
162
119
|
// specific native binary. We don't parse the 100MB binary; we let the CLI
|
|
@@ -345,6 +302,45 @@ export function dropBareLegacyIds(ids) {
|
|
|
345
302
|
return !(bareMajor && ids.some((o) => o !== id && o.startsWith(`${id}-`)));
|
|
346
303
|
});
|
|
347
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Scan raw binary/bundle text for canonical Claude model ids, then drop bare
|
|
307
|
+
* legacy prefixes (issue #1892). Two independent guards keep non-model strings
|
|
308
|
+
* out of the catalog:
|
|
309
|
+
*
|
|
310
|
+
* - **Word-boundary anchors on the id regex.** The id must not be glued to a
|
|
311
|
+
* surrounding identifier character, and must not be the truncated prefix of a
|
|
312
|
+
* longer *version* token. `(?<![A-Za-z0-9_])` rejects a glued prefix;
|
|
313
|
+
* `(?![A-Za-z0-9])` rejects a glued alnum suffix; `(?!\.\d)` rejects a
|
|
314
|
+
* dotted-version continuation — so the bare-major prefix of the binary's own
|
|
315
|
+
* "Typo in model ID" troubleshooting string `claude-sonnet-4.6` is not
|
|
316
|
+
* scraped as `claude-sonnet-4`, while a real id followed by an unrelated `.`
|
|
317
|
+
* suffix (`claude-fable-5.md`) still matches. Dash-separated segments only:
|
|
318
|
+
* the dotted form never appears in a genuine id.
|
|
319
|
+
*
|
|
320
|
+
* The id body is captured inside a lookahead (`(?=(...))\1`) so the greedy
|
|
321
|
+
* `-\d+` run matches **atomically**: without it, a suffix-glued token like
|
|
322
|
+
* `claude-opus-4-1x` would fail the trailing anchor on the full match, then
|
|
323
|
+
* backtrack a segment and re-emit the bare `claude-opus-4` — the exact 404-able
|
|
324
|
+
* id this scan exists to suppress (two packed strings can end up glued with no
|
|
325
|
+
* separator in the extracted binary text). The atomic match fails outright
|
|
326
|
+
* instead of degrading to the bare form.
|
|
327
|
+
* - **`dropBareLegacyIds`.** A standalone `.includes("claude-opus-4")` prefix
|
|
328
|
+
* check is a fully delimited string the anchors cannot tell apart from a real
|
|
329
|
+
* bare id, so a bare `claude-<family>-<major>` is dropped only when a
|
|
330
|
+
* more-specific sibling (`claude-opus-4-8`) is also present; a genuinely bare
|
|
331
|
+
* current id with no sibling (`claude-sonnet-5`) is kept.
|
|
332
|
+
*/
|
|
333
|
+
export function scanClaudeCatalogIds(text) {
|
|
334
|
+
const idRe = /(?<![A-Za-z0-9_])(?=(claude-(?:opus|sonnet|haiku|fable|mythos)-\d+(?:-\d+)*(?:-(?:fast|v\d+))?))\1(?![A-Za-z0-9])(?!\.\d)/g;
|
|
335
|
+
const scanned = new Set();
|
|
336
|
+
let sm;
|
|
337
|
+
while ((sm = idRe.exec(text)) !== null) {
|
|
338
|
+
scanned.add(sm[0]);
|
|
339
|
+
if (sm.index === idRe.lastIndex)
|
|
340
|
+
idRe.lastIndex++;
|
|
341
|
+
}
|
|
342
|
+
return dropBareLegacyIds([...scanned]);
|
|
343
|
+
}
|
|
348
344
|
function extractClaudeCatalog(text) {
|
|
349
345
|
const aliases = {};
|
|
350
346
|
const aliasMapMatch = text.match(/\{opus:"(claude-[^"]+)",sonnet:"(claude-[^"]+)",haiku:"(claude-[^"]+)"\}/);
|
|
@@ -406,16 +402,7 @@ function extractClaudeCatalog(text) {
|
|
|
406
402
|
// catalog while a newer one still gets a real catalog (incl. fable/mythos and
|
|
407
403
|
// the opus-5/sonnet-5 line) rather than an empty or single-model one.
|
|
408
404
|
if (models.length < 2) {
|
|
409
|
-
const
|
|
410
|
-
// Dash-separated segments only. Real ids are `claude-sonnet-4-6`; the dotted
|
|
411
|
-
// form `claude-sonnet-4.6` appears only inside the binary's own "Typo in
|
|
412
|
-
// model ID" troubleshooting text, so a `.`-permitting pattern would scrape a
|
|
413
|
-
// non-model string as if it were real.
|
|
414
|
-
const idRe = /claude-(?:opus|sonnet|haiku|fable|mythos)-\d+(?:-\d+)*(?:-(?:fast|v\d+))?/g;
|
|
415
|
-
let sm;
|
|
416
|
-
while ((sm = idRe.exec(text)) !== null)
|
|
417
|
-
scanned.add(sm[0]);
|
|
418
|
-
const filtered = dropBareLegacyIds([...scanned]);
|
|
405
|
+
const filtered = scanClaudeCatalogIds(text);
|
|
419
406
|
if (filtered.length >= 2)
|
|
420
407
|
models = build(filtered);
|
|
421
408
|
}
|
|
@@ -460,85 +447,6 @@ function extractCodexCatalog(text) {
|
|
|
460
447
|
}
|
|
461
448
|
return { models, aliases: {} };
|
|
462
449
|
}
|
|
463
|
-
/**
|
|
464
|
-
* Extract Gemini's model catalog from `@google/gemini-cli-core/.../config/models.js`.
|
|
465
|
-
*
|
|
466
|
-
* The module exports a set of named constants (e.g. `DEFAULT_GEMINI_MODEL`,
|
|
467
|
-
* `PREVIEW_GEMINI_FLASH_MODEL`) plus a `VALID_GEMINI_MODELS` Set and a handful
|
|
468
|
-
* of `GEMINI_MODEL_ALIAS_*` strings. We parse it with regex (a JS-module import
|
|
469
|
-
* would pollute the runtime and ES-module interop is awkward from a CJS build).
|
|
470
|
-
*/
|
|
471
|
-
function extractGeminiCatalog(text) {
|
|
472
|
-
// Old (<=0.41) layout used `export const FOO = 'bar';` in models.js.
|
|
473
|
-
// New (0.42+) bundle inlines the same constants as `var FOO = "bar";`.
|
|
474
|
-
const constRe = /(?:export\s+const|var)\s+([A-Z0-9_]+)\s*=\s*['"]([^'"]+)['"]/g;
|
|
475
|
-
const constants = new Map();
|
|
476
|
-
let m;
|
|
477
|
-
while ((m = constRe.exec(text)) !== null) {
|
|
478
|
-
constants.set(m[1], m[2]);
|
|
479
|
-
}
|
|
480
|
-
// The set of ids the CLI accepts as "valid model names". Names (not values)
|
|
481
|
-
// are listed inside `new Set([...])`, so we expand them via the constants map.
|
|
482
|
-
const validIds = new Set();
|
|
483
|
-
const setBlock = text.match(/VALID_GEMINI_MODELS\s*=\s*(?:\/\*[^*]*\*\/\s*)?new\s+Set\(\[([\s\S]*?)\]\)/);
|
|
484
|
-
if (setBlock) {
|
|
485
|
-
const nameRe = /([A-Z0-9_]+)/g;
|
|
486
|
-
let nm;
|
|
487
|
-
while ((nm = nameRe.exec(setBlock[1])) !== null) {
|
|
488
|
-
const id = constants.get(nm[1]);
|
|
489
|
-
if (id)
|
|
490
|
-
validIds.add(id);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
// Fall back to any gemini-shaped id we saw in the constants map -- useful
|
|
494
|
-
// when the Set shape changes across gemini versions.
|
|
495
|
-
if (validIds.size === 0) {
|
|
496
|
-
for (const [name, value] of constants) {
|
|
497
|
-
if (/^(DEFAULT_|PREVIEW_)/.test(name) && /^gemini-/.test(value)) {
|
|
498
|
-
validIds.add(value);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
// Aliases are exported as `GEMINI_MODEL_ALIAS_FLASH = 'flash'` etc. The alias
|
|
503
|
-
// is the *value*; the target model is resolved at runtime by `resolveModel()`.
|
|
504
|
-
// We replicate that logic here so callers get a concrete id per alias.
|
|
505
|
-
const defaultId = constants.get('DEFAULT_GEMINI_MODEL');
|
|
506
|
-
const previewPro = constants.get('PREVIEW_GEMINI_MODEL');
|
|
507
|
-
const previewFlash = constants.get('PREVIEW_GEMINI_FLASH_MODEL');
|
|
508
|
-
const flashLite = constants.get('DEFAULT_GEMINI_FLASH_LITE_MODEL');
|
|
509
|
-
const aliases = {};
|
|
510
|
-
if (previewPro) {
|
|
511
|
-
aliases.auto = previewPro;
|
|
512
|
-
aliases.pro = previewPro;
|
|
513
|
-
}
|
|
514
|
-
if (previewFlash)
|
|
515
|
-
aliases.flash = previewFlash;
|
|
516
|
-
if (flashLite)
|
|
517
|
-
aliases['flash-lite'] = flashLite;
|
|
518
|
-
const aliasReverse = {};
|
|
519
|
-
for (const [alias, id] of Object.entries(aliases)) {
|
|
520
|
-
(aliasReverse[id] ||= []).push(alias);
|
|
521
|
-
}
|
|
522
|
-
const defaults = new Set();
|
|
523
|
-
if (defaultId)
|
|
524
|
-
defaults.add(defaultId);
|
|
525
|
-
if (previewPro)
|
|
526
|
-
defaults.add(previewPro); // auto/pro alias resolves here
|
|
527
|
-
const displayNameFor = (id) => {
|
|
528
|
-
// Gemini has a `getDisplayString` for some aliases but the canonical id
|
|
529
|
-
// is human-readable enough ("gemini-3-pro-preview") -- no separate map.
|
|
530
|
-
return undefined;
|
|
531
|
-
};
|
|
532
|
-
const models = Array.from(validIds)
|
|
533
|
-
.sort()
|
|
534
|
-
.map((id) => ({
|
|
535
|
-
id,
|
|
536
|
-
displayName: displayNameFor(id),
|
|
537
|
-
alias: aliasReverse[id]?.[0],
|
|
538
|
-
isDefault: defaults.has(id),
|
|
539
|
-
}));
|
|
540
|
-
return { models, aliases };
|
|
541
|
-
}
|
|
542
450
|
/**
|
|
543
451
|
* Extract OpenCode's catalog by invoking `opencode models --verbose`. The
|
|
544
452
|
* output is a sequence of `<provider>/<id>\n{json}` blocks -- we parse every
|
|
@@ -1042,16 +950,6 @@ export function getModelCatalog(agent, version) {
|
|
|
1042
950
|
: agent === 'codex' ? extractCodexCatalog(text)
|
|
1043
951
|
: { models: [], aliases: {} });
|
|
1044
952
|
}
|
|
1045
|
-
else if (src.kind === 'js') {
|
|
1046
|
-
try {
|
|
1047
|
-
const text = fs.readFileSync(src.path, 'utf-8');
|
|
1048
|
-
if (agent === 'gemini')
|
|
1049
|
-
({ models, aliases } = extractGeminiCatalog(text));
|
|
1050
|
-
}
|
|
1051
|
-
catch {
|
|
1052
|
-
/* unreadable */
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
953
|
else if (src.kind === 'cli') {
|
|
1056
954
|
if (agent === 'opencode')
|
|
1057
955
|
({ models, aliases } = extractOpenCodeCatalog(src.path));
|
|
@@ -14,6 +14,15 @@ export interface TokenUsage {
|
|
|
14
14
|
* convention for models that don't publish a separate cache price).
|
|
15
15
|
*/
|
|
16
16
|
export declare function costOfUsage(u: TokenUsage): number;
|
|
17
|
+
/**
|
|
18
|
+
* USD cost of one usage record priced as if caching were OFF: cache-read and
|
|
19
|
+
* cache-write tokens are billed at the model's full INPUT rate rather than their
|
|
20
|
+
* discounted cache rates. This is the "what would this have cost with no prompt
|
|
21
|
+
* caching?" scenario that `agents output --pricing no-cache` models. Output and
|
|
22
|
+
* uncached input are unchanged — only the cache tokens are repriced. Returns 0
|
|
23
|
+
* for a missing/unpriced model, exactly like {@link costOfUsage}.
|
|
24
|
+
*/
|
|
25
|
+
export declare function costOfUsageNoCache(u: TokenUsage): number;
|
|
17
26
|
/** Sum the USD cost of every usage record in a session. */
|
|
18
27
|
export declare function costOfSession(usages: TokenUsage[]): number;
|
|
19
28
|
/**
|
package/dist/lib/pricing/cost.js
CHANGED
|
@@ -31,6 +31,30 @@ export function costOfUsage(u) {
|
|
|
31
31
|
cacheRead * cacheReadRate +
|
|
32
32
|
cacheWrite * cacheWriteRate);
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* USD cost of one usage record priced as if caching were OFF: cache-read and
|
|
36
|
+
* cache-write tokens are billed at the model's full INPUT rate rather than their
|
|
37
|
+
* discounted cache rates. This is the "what would this have cost with no prompt
|
|
38
|
+
* caching?" scenario that `agents output --pricing no-cache` models. Output and
|
|
39
|
+
* uncached input are unchanged — only the cache tokens are repriced. Returns 0
|
|
40
|
+
* for a missing/unpriced model, exactly like {@link costOfUsage}.
|
|
41
|
+
*/
|
|
42
|
+
export function costOfUsageNoCache(u) {
|
|
43
|
+
if (!u.model)
|
|
44
|
+
return 0;
|
|
45
|
+
const pricing = getModelPricing(u.model);
|
|
46
|
+
if (!pricing)
|
|
47
|
+
return 0;
|
|
48
|
+
const input = u.inputTokens ?? 0;
|
|
49
|
+
const output = u.outputTokens ?? 0;
|
|
50
|
+
const cacheRead = u.cacheReadTokens ?? 0;
|
|
51
|
+
const cacheWrite = u.cacheCreationTokens ?? 0;
|
|
52
|
+
// Cache tokens repriced at the input rate — no caching discount applied.
|
|
53
|
+
return (input * pricing.inputPerToken +
|
|
54
|
+
output * pricing.outputPerToken +
|
|
55
|
+
cacheRead * pricing.inputPerToken +
|
|
56
|
+
cacheWrite * pricing.inputPerToken);
|
|
57
|
+
}
|
|
34
58
|
/** Sum the USD cost of every usage record in a session. */
|
|
35
59
|
export function costOfSession(usages) {
|
|
36
60
|
let total = 0;
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
* enforcement) imports against — keep it stable.
|
|
6
6
|
*/
|
|
7
7
|
export { type ModelPricing, PRICING_VERSION, getModelPricing, listPricedModels, } from './table.js';
|
|
8
|
-
export { type TokenUsage, costOfUsage, costOfSession, formatUsd, estimateCost, actualCost, isModelPriced, } from './cost.js';
|
|
8
|
+
export { type TokenUsage, costOfUsage, costOfUsageNoCache, costOfSession, formatUsd, estimateCost, actualCost, isModelPriced, } from './cost.js';
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
* enforcement) imports against — keep it stable.
|
|
6
6
|
*/
|
|
7
7
|
export { PRICING_VERSION, getModelPricing, listPricedModels, } from './table.js';
|
|
8
|
-
export { costOfUsage, costOfSession, formatUsd, estimateCost, actualCost, isModelPriced, } from './cost.js';
|
|
8
|
+
export { costOfUsage, costOfUsageNoCache, costOfSession, formatUsd, estimateCost, actualCost, isModelPriced, } from './cost.js';
|
package/dist/lib/redact.js
CHANGED
|
@@ -13,9 +13,14 @@ const SECRET_PATTERNS = [
|
|
|
13
13
|
[/\bghp_[A-Za-z0-9]{36}\b/g, '[REDACTED_GITHUB_TOKEN]'],
|
|
14
14
|
[/\bgh[osru]_[A-Za-z0-9]{36}\b/g, '[REDACTED_GITHUB_TOKEN]'],
|
|
15
15
|
[/\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, '[REDACTED_GITHUB_TOKEN]'],
|
|
16
|
-
// Anthropic
|
|
17
|
-
//
|
|
18
|
-
|
|
16
|
+
// Anthropic credentials, before the generic sk- rule so the marker is specific
|
|
17
|
+
// (the generic rule would otherwise swallow them first). Covers every kind of
|
|
18
|
+
// sk-ant token by matching the kind segment generically: API keys
|
|
19
|
+
// (sk-ant-api03-…) AND the long-lived OAuth setup-tokens (sk-ant-oat01-…) that
|
|
20
|
+
// `claude setup-token` mints. The generic sk- rule below CANNOT match an oat01
|
|
21
|
+
// token — the hyphen after `ant` breaks its [A-Za-z0-9]{20,} run — so without
|
|
22
|
+
// this an OAuth setup-token leaks verbatim into any log/export (#1767).
|
|
23
|
+
[/\bsk-ant-[a-z0-9]{3,8}-[A-Za-z0-9_-]{8,}\b/g, '[REDACTED_ANTHROPIC_KEY]'],
|
|
19
24
|
// Stripe live secret / restricted keys.
|
|
20
25
|
[/\b[rs]k_live_[A-Za-z0-9]{20,}\b/g, '[REDACTED_STRIPE_KEY]'],
|
|
21
26
|
[/\bsk-[A-Za-z0-9]{20,}\b/g, '[REDACTED_API_KEY]'],
|
|
@@ -34,6 +34,7 @@ export interface RemoteAgentsJsonOptions<T> {
|
|
|
34
34
|
export type SshCaptureFn = (target: string, remoteCmd: string, opts: {
|
|
35
35
|
timeoutMs: number;
|
|
36
36
|
signal?: AbortSignal;
|
|
37
|
+
extraSshArgs?: string[];
|
|
37
38
|
}) => Promise<{
|
|
38
39
|
code: number | null;
|
|
39
40
|
stdout: string;
|
|
@@ -62,5 +63,36 @@ export declare function parseRemoteAgentsJsonPayload<T>(stdout: string, machine:
|
|
|
62
63
|
};
|
|
63
64
|
/** Build the command one peer runs, with a guard that prevents recursive fan-out. */
|
|
64
65
|
export declare function remoteAgentsJsonCommand(args: string[], noFanoutEnv: string, os?: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* The subset of a spawned `ssh` child this capture loop drives. Structural so a
|
|
68
|
+
* unit test can feed synthetic `data`/`close` events through a fake without a
|
|
69
|
+
* live SSH connection.
|
|
70
|
+
*/
|
|
71
|
+
export interface CapturableChild {
|
|
72
|
+
stdout: {
|
|
73
|
+
on(event: 'data', listener: (chunk: Buffer) => void): unknown;
|
|
74
|
+
} | null;
|
|
75
|
+
on(event: 'error' | 'close', listener: (arg?: number | null) => void): unknown;
|
|
76
|
+
kill(signal?: NodeJS.Signals): unknown;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Stream one child's stdout into memory under a hard per-peer byte ceiling.
|
|
80
|
+
*
|
|
81
|
+
* A cross-machine fan-out awaits every peer's capture under one `Promise.all`,
|
|
82
|
+
* so an unbounded buffer means a single runaway peer (a corrupt or
|
|
83
|
+
* pathologically large payload) exhausts the caller's heap and takes the whole
|
|
84
|
+
* sweep down — RUSH-2065. Once the accumulated bytes would exceed
|
|
85
|
+
* {@link REMOTE_STDOUT_MAX_BYTES}, the child is SIGKILLed and the capture settles
|
|
86
|
+
* as `code: null` (unreachable) rather than trust a truncated body. A definitive
|
|
87
|
+
* early-exit `signal` SIGTERMs the child the same way, and the timer is the
|
|
88
|
+
* per-peer deadline.
|
|
89
|
+
*/
|
|
90
|
+
export declare function captureBoundedStdout(child: CapturableChild, { timeoutMs, signal }: {
|
|
91
|
+
timeoutMs: number;
|
|
92
|
+
signal?: AbortSignal;
|
|
93
|
+
}): Promise<{
|
|
94
|
+
code: number | null;
|
|
95
|
+
stdout: string;
|
|
96
|
+
}>;
|
|
65
97
|
/** Query explicit hosts, or every registered online peer when hosts is omitted. */
|
|
66
98
|
export declare function gatherRemoteAgentsJson<T>(options: RemoteAgentsJsonOptions<T>, deps?: GatherRemoteAgentsJsonDeps): Promise<RemoteAgentsJsonResult<T>>;
|
|
@@ -9,18 +9,20 @@
|
|
|
9
9
|
import { spawn } from 'child_process';
|
|
10
10
|
import { setMaxListeners } from 'node:events';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
|
-
import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote } from './ssh-exec.js';
|
|
13
|
-
import { sshTargetFor } from './devices/connect.js';
|
|
12
|
+
import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote, REMOTE_STDOUT_MAX_BYTES, RemoteUtf8Accumulator, } from './ssh-exec.js';
|
|
13
|
+
import { deviceIdentityArgs, sshTargetFor } from './devices/connect.js';
|
|
14
14
|
import { resolveExplicitTargets } from './devices/resolve-target.js';
|
|
15
15
|
import { loadDevices, isControlDevice, isDialableDevice } from './devices/registry.js';
|
|
16
|
-
import { remoteShellFor, buildWindowsAgentsCommand } from './hosts/remote-cmd.js';
|
|
16
|
+
import { remoteShellFor, buildWindowsAgentsCommand, stripClixml } from './hosts/remote-cmd.js';
|
|
17
17
|
import { machineId, normalizeHost } from './machine-id.js';
|
|
18
18
|
const REMOTE_TIMEOUT_MS = 12_000;
|
|
19
19
|
export function normalizeRemoteAgentsJsonParse(parsed) {
|
|
20
20
|
return Array.isArray(parsed) ? { items: parsed, valid: true } : parsed;
|
|
21
21
|
}
|
|
22
22
|
export function parseRemoteAgentsJsonPayload(stdout, machine, parse) {
|
|
23
|
-
|
|
23
|
+
// A Windows peer can prefix the JSON with a PowerShell CLIXML banner; strip it
|
|
24
|
+
// before handing stdout to the command-specific parser (RUSH-2286).
|
|
25
|
+
const parsed = normalizeRemoteAgentsJsonParse(parse(stripClixml(stdout), machine));
|
|
24
26
|
return parsed.valid
|
|
25
27
|
? { items: parsed.items, parseFailed: false }
|
|
26
28
|
: { items: [], parseFailed: true };
|
|
@@ -33,16 +35,22 @@ export function remoteAgentsJsonCommand(args, noFanoutEnv, os) {
|
|
|
33
35
|
const inner = `${noFanoutEnv}=1 agents ${args.map(shellQuote).join(' ')}`;
|
|
34
36
|
return `bash -lc ${shellQuote(inner)}`;
|
|
35
37
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Stream one child's stdout into memory under a hard per-peer byte ceiling.
|
|
40
|
+
*
|
|
41
|
+
* A cross-machine fan-out awaits every peer's capture under one `Promise.all`,
|
|
42
|
+
* so an unbounded buffer means a single runaway peer (a corrupt or
|
|
43
|
+
* pathologically large payload) exhausts the caller's heap and takes the whole
|
|
44
|
+
* sweep down — RUSH-2065. Once the accumulated bytes would exceed
|
|
45
|
+
* {@link REMOTE_STDOUT_MAX_BYTES}, the child is SIGKILLed and the capture settles
|
|
46
|
+
* as `code: null` (unreachable) rather than trust a truncated body. A definitive
|
|
47
|
+
* early-exit `signal` SIGTERMs the child the same way, and the timer is the
|
|
48
|
+
* per-peer deadline.
|
|
49
|
+
*/
|
|
50
|
+
export function captureBoundedStdout(child, { timeoutMs, signal }) {
|
|
38
51
|
return new Promise((resolve) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const args = [...SSH_OPTS, ...controlOpts(), target, remoteCmd];
|
|
44
|
-
const child = spawn('ssh', args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
45
|
-
let stdout = '';
|
|
52
|
+
const decoded = new RemoteUtf8Accumulator();
|
|
53
|
+
let stdoutBytes = 0;
|
|
46
54
|
let settled = false;
|
|
47
55
|
const done = (code) => {
|
|
48
56
|
if (settled)
|
|
@@ -50,7 +58,10 @@ const sshCapture = (target, remoteCmd, { timeoutMs, signal }) => {
|
|
|
50
58
|
settled = true;
|
|
51
59
|
clearTimeout(timer);
|
|
52
60
|
signal?.removeEventListener('abort', onAbort);
|
|
53
|
-
|
|
61
|
+
// A killed/aborted capture never cleanly ends its decoder; return what
|
|
62
|
+
// decoded so far (the caller treats a non-zero code as unreachable and
|
|
63
|
+
// ignores stdout anyway).
|
|
64
|
+
resolve({ code, stdout: code === null ? decoded.current() : decoded.end() });
|
|
54
65
|
};
|
|
55
66
|
const timer = setTimeout(() => {
|
|
56
67
|
child.kill('SIGKILL');
|
|
@@ -60,10 +71,28 @@ const sshCapture = (target, remoteCmd, { timeoutMs, signal }) => {
|
|
|
60
71
|
// unreachable peer is not left running to its full timeout.
|
|
61
72
|
const onAbort = () => { child.kill('SIGTERM'); done(null); };
|
|
62
73
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
63
|
-
child.stdout
|
|
74
|
+
child.stdout?.on('data', (data) => {
|
|
75
|
+
// Over the ceiling, SIGKILL the child and treat it as unreachable rather
|
|
76
|
+
// than trust a partial payload — the RUSH-2065 OOM guard.
|
|
77
|
+
if (stdoutBytes + data.byteLength > REMOTE_STDOUT_MAX_BYTES) {
|
|
78
|
+
child.kill('SIGKILL');
|
|
79
|
+
done(null);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
stdoutBytes += data.byteLength;
|
|
83
|
+
decoded.write(data);
|
|
84
|
+
});
|
|
64
85
|
child.on('error', () => done(null));
|
|
65
|
-
child.on('close', (code) => done(code));
|
|
86
|
+
child.on('close', (code) => done(code ?? null));
|
|
66
87
|
});
|
|
88
|
+
}
|
|
89
|
+
const sshCapture = (target, remoteCmd, { timeoutMs, signal, extraSshArgs }) => {
|
|
90
|
+
assertValidSshTarget(target);
|
|
91
|
+
if (signal?.aborted)
|
|
92
|
+
return Promise.resolve({ code: null, stdout: '' });
|
|
93
|
+
const args = [...SSH_OPTS, ...controlOpts(), ...(extraSshArgs ?? []), target, remoteCmd];
|
|
94
|
+
const child = spawn('ssh', args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
95
|
+
return captureBoundedStdout(child, { timeoutMs, signal });
|
|
67
96
|
};
|
|
68
97
|
/** Query explicit hosts, or every registered online peer when hosts is omitted. */
|
|
69
98
|
export async function gatherRemoteAgentsJson(options, deps = {}) {
|
|
@@ -102,6 +131,7 @@ export async function gatherRemoteAgentsJson(options, deps = {}) {
|
|
|
102
131
|
machine: normalizeHost(device.name),
|
|
103
132
|
name: device.name,
|
|
104
133
|
os: device.platform,
|
|
134
|
+
extraSshArgs: deviceIdentityArgs(device),
|
|
105
135
|
});
|
|
106
136
|
}
|
|
107
137
|
catch {
|
|
@@ -126,6 +156,7 @@ export async function gatherRemoteAgentsJson(options, deps = {}) {
|
|
|
126
156
|
const result = await capture(target.target, command, {
|
|
127
157
|
timeoutMs: options.timeoutMs ?? REMOTE_TIMEOUT_MS,
|
|
128
158
|
signal: controller?.signal,
|
|
159
|
+
extraSshArgs: target.extraSshArgs,
|
|
129
160
|
});
|
|
130
161
|
// A peer we deliberately cancelled is neither a hit nor a failure — it never
|
|
131
162
|
// got to answer, so it must not pollute skipped/parseFailed (which drive the
|
|
@@ -41,11 +41,10 @@ export function getActiveResourceProfileName() {
|
|
|
41
41
|
return readMeta().profiles?.active ?? null;
|
|
42
42
|
}
|
|
43
43
|
export function getActiveResourceProfile() {
|
|
44
|
-
const meta = readMeta();
|
|
45
44
|
const name = getActiveResourceProfileName();
|
|
46
45
|
if (!name)
|
|
47
46
|
return null;
|
|
48
|
-
const preset =
|
|
47
|
+
const preset = readMeta().profiles?.presets?.[name];
|
|
49
48
|
return preset ? { name, preset } : null;
|
|
50
49
|
}
|
|
51
50
|
export function upsertResourceProfilePreset(name, preset) {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner phone notification on routine FAILURE (RUSH-2288).
|
|
3
|
+
*
|
|
4
|
+
* The desktop lifecycle notifications (routine-notify.ts) never leave the
|
|
5
|
+
* machine, so a failed scheduled routine on a headless fleet box was invisible
|
|
6
|
+
* until someone happened to look. The per-routine prompt pattern (a prompt that
|
|
7
|
+
* ends by shelling `agents notify`) does not close the gap either: when the
|
|
8
|
+
* routine's OWN agent fails to spawn (`auth_failed`) that prompt never runs, so
|
|
9
|
+
* the one failure most worth surfacing is the one that goes unheard.
|
|
10
|
+
*
|
|
11
|
+
* So the DAEMON delivers on failure, from the SAME owner channel stack `agents
|
|
12
|
+
* notify` uses (humans.yaml owner channels, or the legacy `notify.owner`),
|
|
13
|
+
* calling the channel providers IN-PROCESS — no shelling out to `ssh mac-mini
|
|
14
|
+
* agents notify`. When the primary owner channel cannot deliver from this box it
|
|
15
|
+
* walks the remaining configured owner channels as fallbacks (OpenClaw, a second
|
|
16
|
+
* channel, …). Telegram is never used.
|
|
17
|
+
*
|
|
18
|
+
* Only failures notify. A green routine of any kind stays silent — this is an
|
|
19
|
+
* ADDITIONAL failures-only lane, and the desktop thresholds (which still ping a
|
|
20
|
+
* green agent/workflow finish on the local screen) are unchanged. Delivery is
|
|
21
|
+
* deduped per job+runId so a run is announced to the owner at most once.
|
|
22
|
+
*
|
|
23
|
+
* The message BUILDERS (`routineFinishOwnerText` / `routineStartFailedOwnerText`)
|
|
24
|
+
* are pure and unit-tested; `notifyOwnerRoutineFinish` /
|
|
25
|
+
* `notifyOwnerRoutineStartFailed` are the daemon glue that reads config and
|
|
26
|
+
* delivers.
|
|
27
|
+
*/
|
|
28
|
+
import type { Meta } from './types.js';
|
|
29
|
+
import type { JobConfig, RunMeta } from './routines.js';
|
|
30
|
+
/**
|
|
31
|
+
* Owner-phone text for a routine FINISH, or null when the run did NOT fail. Only
|
|
32
|
+
* `failed` and `timeout` are failures worth an owner ping — a `completed` run
|
|
33
|
+
* (any kind) is silent, and `running`/`missed` never reach this path from the
|
|
34
|
+
* daemon's finish hook. The text is a short, phone-sized pointer: what failed,
|
|
35
|
+
* why, and on which box (so the owner knows where to look), well under the
|
|
36
|
+
* `user-message-guard` length ceiling.
|
|
37
|
+
*/
|
|
38
|
+
export declare function routineFinishOwnerText(meta: Pick<RunMeta, 'jobName' | 'status' | 'exitCode' | 'errorMessage' | 'agent' | 'workflow' | 'command'>, host: string): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Owner-phone text for a routine that failed to even START (the daemon's
|
|
41
|
+
* pre-spawn catch — `executeJobDetached` threw before a child existed, e.g.
|
|
42
|
+
* `auth_failed`). Never null: a broken start is always a failure worth the ping,
|
|
43
|
+
* for every routine kind, and it is exactly the case the per-routine `agents
|
|
44
|
+
* notify` prompt can never cover because that prompt never ran.
|
|
45
|
+
*/
|
|
46
|
+
export declare function routineStartFailedOwnerText(config: Pick<JobConfig, 'name' | 'agent' | 'workflow' | 'command'>, error: string, host: string): string;
|
|
47
|
+
/** One resolved owner destination to attempt, in plan order. */
|
|
48
|
+
export interface OwnerDest {
|
|
49
|
+
channel: string;
|
|
50
|
+
to: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Ordered owner delivery plan for a failure ping: the primary owner destination
|
|
54
|
+
* (the same one `agents notify` resolves) first, then every other configured
|
|
55
|
+
* owner channel as a fallback. Deduped by (channel, to). Telegram channels and
|
|
56
|
+
* intrusive channels (a voice call is too much for a routine failure) are
|
|
57
|
+
* excluded entirely — so an owner whose ONLY channel is Telegram gets an empty
|
|
58
|
+
* plan and no ping, which is the intended "silence beats Telegram" outcome.
|
|
59
|
+
*
|
|
60
|
+
* The same filters apply to the primary and the fallback list — a policy that
|
|
61
|
+
* points normal severity at a voice channel must not auto-call the owner on
|
|
62
|
+
* every routine failure.
|
|
63
|
+
*/
|
|
64
|
+
export declare function ownerFailureDeliveryPlan(meta: Meta): OwnerDest[];
|
|
65
|
+
/** Outcome of one delivery attempt, for logging/telemetry. */
|
|
66
|
+
export interface OwnerDeliveryAttempt {
|
|
67
|
+
channel: string;
|
|
68
|
+
ok: boolean;
|
|
69
|
+
error?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface OwnerDeliveryResult {
|
|
72
|
+
/** True once any channel in the plan accepted the message. */
|
|
73
|
+
delivered: boolean;
|
|
74
|
+
/** The channel that delivered, when `delivered`. */
|
|
75
|
+
channel?: string;
|
|
76
|
+
/** Every attempt made, in order — empty when no owner channel is configured. */
|
|
77
|
+
attempts: OwnerDeliveryAttempt[];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Deliver `text` to the owner over the failure plan, in-process, trying each
|
|
81
|
+
* channel until one accepts. Uses the non-dying `lookupTransport` (never
|
|
82
|
+
* `resolveTransport`, which `die()`s the process) so a daemon survives a bad
|
|
83
|
+
* channel name. Best-effort by contract: the daemon wraps this in try/catch, but
|
|
84
|
+
* it also never throws for a delivery failure — it returns the attempt log.
|
|
85
|
+
*/
|
|
86
|
+
export declare function deliverOwnerFailure(text: string, meta: Meta): Promise<OwnerDeliveryResult>;
|
|
87
|
+
/** Test-only: reset the dedup set so cases don't leak state into each other. */
|
|
88
|
+
export declare function __resetOwnerFailureDedup(): void;
|
|
89
|
+
/**
|
|
90
|
+
* Daemon glue: on a routine FINISH, ping the owner IFF the run failed. Green
|
|
91
|
+
* runs return early (no ping). Deduped per job+runId — claimed for the
|
|
92
|
+
* in-flight window so two finish hooks don't double-text, released when no
|
|
93
|
+
* channel accepted so a later sweep can retry. Best-effort.
|
|
94
|
+
*/
|
|
95
|
+
export declare function notifyOwnerRoutineFinish(meta: RunMeta): Promise<OwnerDeliveryResult>;
|
|
96
|
+
/**
|
|
97
|
+
* Daemon glue: on a pre-spawn failure (no run record, so no finish hook and no
|
|
98
|
+
* runId), ping the owner. Not deduped — a pre-spawn failure is one scheduled
|
|
99
|
+
* fire on the cron cadence (never a per-second storm), and each such fire is a
|
|
100
|
+
* distinct failure the owner should hear about. Best-effort.
|
|
101
|
+
*/
|
|
102
|
+
export declare function notifyOwnerRoutineStartFailed(config: JobConfig, error: string): Promise<OwnerDeliveryResult>;
|