@phnx-labs/agents-cli 1.22.6 → 1.22.8
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 +131 -1
- package/README.md +7 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +61 -0
- package/dist/commands/exec.js +83 -16
- package/dist/commands/feed.d.ts +2 -1
- package/dist/commands/feed.js +47 -20
- package/dist/commands/focus.js +22 -1
- package/dist/commands/harness.d.ts +0 -1
- package/dist/commands/harness.js +60 -4
- package/dist/commands/models.js +2 -2
- package/dist/commands/monitors.js +2 -2
- package/dist/commands/projects.js +122 -107
- package/dist/commands/routines.js +2 -2
- package/dist/commands/run-account-picker.js +2 -0
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/sessions-backfill.d.ts +33 -0
- package/dist/commands/sessions-backfill.js +83 -1
- package/dist/commands/sessions-stats.d.ts +36 -0
- package/dist/commands/sessions-stats.js +263 -0
- package/dist/commands/sessions.d.ts +1 -1
- package/dist/commands/sessions.js +21 -1
- package/dist/commands/snapshot.d.ts +11 -0
- package/dist/commands/snapshot.js +107 -0
- package/dist/commands/teams.js +2 -1
- package/dist/commands/view.d.ts +9 -1
- package/dist/commands/view.js +63 -12
- package/dist/index.js +4 -2
- package/dist/lib/activity.js +2 -2
- package/dist/lib/agents.js +68 -11
- package/dist/lib/analytics/recipes.js +11 -5
- package/dist/lib/browser/ipc.js +2 -0
- package/dist/lib/browser/profiles.d.ts +15 -7
- package/dist/lib/browser/profiles.js +53 -12
- package/dist/lib/browser/remote-control.d.ts +35 -0
- package/dist/lib/browser/remote-control.js +48 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +19 -1
- package/dist/lib/browser/types.d.ts +14 -2
- package/dist/lib/byok-usage.d.ts +38 -0
- package/dist/lib/byok-usage.js +117 -0
- package/dist/lib/capabilities.js +1 -1
- package/dist/lib/device-config.js +8 -0
- package/dist/lib/exec.d.ts +20 -0
- package/dist/lib/exec.js +73 -8
- package/dist/lib/feed-outcome.d.ts +1 -0
- package/dist/lib/feed-outcome.js +2 -0
- package/dist/lib/feed-post.js +1 -1
- package/dist/lib/feed-ranking.d.ts +1 -0
- package/dist/lib/feed-ranking.js +4 -0
- package/dist/lib/feed.d.ts +4 -1
- package/dist/lib/feed.js +25 -0
- package/dist/lib/hosts/passthrough.d.ts +10 -1
- package/dist/lib/hosts/passthrough.js +23 -3
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/mcp.js +6 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tiers.js +4 -1
- package/dist/lib/models.js +63 -0
- package/dist/lib/placement.d.ts +82 -0
- package/dist/lib/placement.js +188 -0
- package/dist/lib/profiles.d.ts +73 -10
- package/dist/lib/profiles.js +133 -15
- package/dist/lib/project-focus.d.ts +9 -0
- package/dist/lib/project-focus.js +23 -0
- package/dist/lib/project-key.d.ts +1 -1
- package/dist/lib/project-key.js +1 -1
- package/dist/lib/project-probe.d.ts +18 -0
- package/dist/lib/project-probe.js +46 -0
- package/dist/lib/project-status.d.ts +56 -0
- package/dist/lib/project-status.js +125 -18
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/rotate.d.ts +19 -4
- package/dist/lib/rotate.js +24 -1
- package/dist/lib/routines.d.ts +2 -0
- package/dist/lib/runner.d.ts +3 -0
- package/dist/lib/runner.js +90 -7
- 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/audit.js +1 -1
- package/dist/lib/secrets/index.d.ts +1 -0
- package/dist/lib/secrets/index.js +29 -15
- package/dist/lib/secrets/remote.d.ts +1 -1
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/session/active.d.ts +2 -0
- package/dist/lib/session/bash-command.d.ts +2 -3
- package/dist/lib/session/db.d.ts +92 -1
- package/dist/lib/session/db.js +230 -1
- package/dist/lib/session/digest.d.ts +1 -1
- package/dist/lib/session/digest.js +1 -1
- package/dist/lib/share/publish.js +24 -0
- package/dist/lib/snapshot.d.ts +103 -0
- package/dist/lib/snapshot.js +99 -0
- package/dist/lib/startup/command-registry.d.ts +1 -1
- package/dist/lib/startup/command-registry.js +2 -2
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/types.d.ts +11 -2
- package/dist/lib/usage.d.ts +5 -0
- package/dist/lib/usage.js +3 -3
- package/package.json +1 -1
- package/dist/commands/activity.d.ts +0 -87
- package/dist/commands/activity.js +0 -346
package/dist/lib/profiles.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as yaml from 'yaml';
|
|
|
11
11
|
import { getUserAgentsDir } from './state.js';
|
|
12
12
|
import { getKeychainToken, hasKeychainToken, keychainItemName } from './secrets/profiles.js';
|
|
13
13
|
import { getPreset } from './profiles-presets.js';
|
|
14
|
+
import { MODEL_TIERS, isTierToken } from './model-tiers.js';
|
|
14
15
|
const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9-_]{0,48}$/i;
|
|
15
16
|
/** Get the directory where profile YAML files are stored. */
|
|
16
17
|
export function getProfilesDir() {
|
|
@@ -198,11 +199,46 @@ export function profileAuthLabel(profile) {
|
|
|
198
199
|
return provider;
|
|
199
200
|
}
|
|
200
201
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
202
|
+
* Curated vendor/brand display names, matched case-insensitively per token.
|
|
203
|
+
* Entries with a space (e.g. 'Moonshot AI') are single-token → multi-word expansions.
|
|
204
|
+
*/
|
|
205
|
+
const VENDOR_TABLE = [
|
|
206
|
+
['deepseek', 'DeepSeek'],
|
|
207
|
+
['openai', 'OpenAI'],
|
|
208
|
+
['anthropic', 'Anthropic'],
|
|
209
|
+
['claude', 'Claude'],
|
|
210
|
+
['grok', 'Grok'],
|
|
211
|
+
['xai', 'xAI'],
|
|
212
|
+
['gpt', 'GPT'],
|
|
213
|
+
['meta', 'Meta'],
|
|
214
|
+
['mistral', 'Mistral'],
|
|
215
|
+
['mistralai', 'Mistral'],
|
|
216
|
+
['qwen', 'Qwen'],
|
|
217
|
+
['gemini', 'Gemini'],
|
|
218
|
+
['moonshot', 'Moonshot AI'],
|
|
219
|
+
['moonshotai', 'Moonshot AI'],
|
|
220
|
+
['kimi', 'Kimi'],
|
|
221
|
+
['cohere', 'Cohere'],
|
|
222
|
+
['perplexity', 'Perplexity'],
|
|
223
|
+
];
|
|
224
|
+
function tokenToDisplayName(token) {
|
|
225
|
+
const lower = token.toLowerCase();
|
|
226
|
+
for (const [key, display] of VENDOR_TABLE) {
|
|
227
|
+
if (lower === key)
|
|
228
|
+
return display;
|
|
229
|
+
}
|
|
230
|
+
return token.charAt(0).toUpperCase() + token.slice(1);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Header label for the harness — derived from `profile.name` by splitting on
|
|
234
|
+
* `[-_]` and mapping each token through the vendor/brand table. Never reads
|
|
235
|
+
* the stored `label` field; old YAML files with a `label:` key are unaffected.
|
|
236
|
+
*
|
|
237
|
+
* Examples: `deepseek-flash` → `'DeepSeek Flash'`, `spark` → `'Spark'`,
|
|
238
|
+
* `deepseek_chat_v3` → `'DeepSeek Chat V3'`.
|
|
203
239
|
*/
|
|
204
240
|
export function profileLabel(profile) {
|
|
205
|
-
return profile.
|
|
241
|
+
return profile.name.split(/[-_]/).map(tokenToDisplayName).join(' ');
|
|
206
242
|
}
|
|
207
243
|
/** Build a stable, machine-readable summary for list and view surfaces. */
|
|
208
244
|
export function profileSummary(profile) {
|
|
@@ -299,8 +335,6 @@ export function profileFromHostModel(name, host, model, opts = {}) {
|
|
|
299
335
|
provider: opts.provider ?? host,
|
|
300
336
|
forkedFrom: host,
|
|
301
337
|
};
|
|
302
|
-
if (opts.label)
|
|
303
|
-
profile.label = opts.label;
|
|
304
338
|
if (opts.provider && opts.authEnvVar) {
|
|
305
339
|
profile.auth = { envVar: opts.authEnvVar, keychainItem: keychainItemName(opts.provider) };
|
|
306
340
|
profile.authOptional = false;
|
|
@@ -336,14 +370,6 @@ export function forkProfile(source, name, opts = {}) {
|
|
|
336
370
|
description: opts.description ?? (opts.model ? `Forked from ${source.name}: ${opts.model}` : source.description),
|
|
337
371
|
forkedFrom: source.name,
|
|
338
372
|
};
|
|
339
|
-
// `label` is the header `agents view` prints, so an inherited one would make
|
|
340
|
-
// the fork and its source visually identical — the ambiguity a per-harness
|
|
341
|
-
// block exists to remove. A fork carries a label only when it is given one;
|
|
342
|
-
// otherwise `profileLabel` falls back to the fork's own name.
|
|
343
|
-
if (opts.label)
|
|
344
|
-
forked.label = opts.label;
|
|
345
|
-
else
|
|
346
|
-
delete forked.label;
|
|
347
373
|
// A fork that repoints the model or endpoint is no longer that preset — keep
|
|
348
374
|
// the preset link only while the fork still matches what the preset defines.
|
|
349
375
|
if (opts.model || opts.baseUrl)
|
|
@@ -359,6 +385,51 @@ export function forkProfile(source, name, opts = {}) {
|
|
|
359
385
|
}
|
|
360
386
|
return forked;
|
|
361
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* Edit an existing profile in-place, applying overrides without changing its
|
|
390
|
+
* name or lineage. Reuses {@link forkProfile}'s validation and override logic
|
|
391
|
+
* (model swap, base-URL validation, auth repoint), then restores the original
|
|
392
|
+
* `forkedFrom` so an edit never self-references the profile.
|
|
393
|
+
*
|
|
394
|
+
* Note: this returns the updated `Profile` object but does NOT write it to
|
|
395
|
+
* disk — callers should follow up with `writeProfile(result)` if persistence
|
|
396
|
+
* is needed.
|
|
397
|
+
*/
|
|
398
|
+
export function editProfile(source, opts = {}) {
|
|
399
|
+
const edited = forkProfile(source, source.name, opts);
|
|
400
|
+
// forkProfile sets forkedFrom = source.name; for an in-place edit that would
|
|
401
|
+
// be a self-reference. Restore the original lineage instead.
|
|
402
|
+
edited.forkedFrom = source.forkedFrom;
|
|
403
|
+
return edited;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Rename a profile on disk, then rewrite `forkedFrom` in every other profile
|
|
407
|
+
* that pointed at the old name so lineage display never goes stale.
|
|
408
|
+
*
|
|
409
|
+
* Throws if `oldName` does not exist or `newName` already exists. There is no
|
|
410
|
+
* `--force` / overwrite path — a collision is a hard error directing the user
|
|
411
|
+
* to remove the target first.
|
|
412
|
+
*/
|
|
413
|
+
export function renameProfile(oldName, newName) {
|
|
414
|
+
validateProfileName(newName);
|
|
415
|
+
if (!profileExists(oldName)) {
|
|
416
|
+
throw new Error(`Profile '${oldName}' not found.`);
|
|
417
|
+
}
|
|
418
|
+
if (profileExists(newName)) {
|
|
419
|
+
throw new Error(`Profile '${newName}' already exists; remove it first.`);
|
|
420
|
+
}
|
|
421
|
+
const profile = readProfile(oldName);
|
|
422
|
+
profile.name = newName;
|
|
423
|
+
writeProfile(profile);
|
|
424
|
+
deleteProfile(oldName);
|
|
425
|
+
// Rewrite forkedFrom in every other profile that referenced the old name.
|
|
426
|
+
for (const other of listProfiles()) {
|
|
427
|
+
if (other.name !== newName && other.forkedFrom === oldName) {
|
|
428
|
+
other.forkedFrom = newName;
|
|
429
|
+
writeProfile(other);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
362
433
|
/**
|
|
363
434
|
* Resolve a profile into the env block that should be injected into the
|
|
364
435
|
* spawned agent process. Reads the token from keychain at exec time so the
|
|
@@ -378,17 +449,45 @@ export function resolveProfileEnv(profile) {
|
|
|
378
449
|
}
|
|
379
450
|
return env;
|
|
380
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Resolve a requested cost tier against a profile's `models:` map. An unset
|
|
454
|
+
* tier clamps to the next CHEAPER tier that IS set (ultra -> best -> default
|
|
455
|
+
* -> cheap), mirroring the clamp semantics of `bucketRungs` in
|
|
456
|
+
* model-tiers.ts. Returns null when the profile declares no `models:` at all,
|
|
457
|
+
* or none of the tiers at-or-below the request are set.
|
|
458
|
+
*/
|
|
459
|
+
function resolveProfileTierModel(profile, tier) {
|
|
460
|
+
if (!profile.models)
|
|
461
|
+
return null;
|
|
462
|
+
const idx = MODEL_TIERS.indexOf(tier);
|
|
463
|
+
for (let i = idx; i >= 0; i--) {
|
|
464
|
+
const rung = MODEL_TIERS[i];
|
|
465
|
+
const model = profile.models[rung];
|
|
466
|
+
if (model)
|
|
467
|
+
return { model, clampedFrom: rung === tier ? undefined : rung };
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
381
471
|
/**
|
|
382
472
|
* Resolve a name into (agent, version, env). Throws if the name is not a
|
|
383
473
|
* profile. Callers are expected to try agent-id resolution first and fall
|
|
384
474
|
* back to this when that fails, so we don't need a "isProfile" probe.
|
|
475
|
+
*
|
|
476
|
+
* `requestedModel` is the caller's raw `--model` value. When it is a cost-tier
|
|
477
|
+
* token (`cheap`/`default`/`best`/`ultra`), it is resolved against the
|
|
478
|
+
* profile's OWN `models:` map (see `resolveProfileTierModel`) and substituted
|
|
479
|
+
* into `env` as a concrete model id BEFORE returning — so exec.ts's native
|
|
480
|
+
* tier-resolution block (which indexes the HOST agent's catalog, e.g. Claude's
|
|
481
|
+
* own models) never sees a tier token for a profile-based run, and can't
|
|
482
|
+
* collide the profile's harness identity with its host's catalog.
|
|
385
483
|
*/
|
|
386
|
-
export function resolveProfileForRun(name) {
|
|
484
|
+
export function resolveProfileForRun(name, requestedModel) {
|
|
387
485
|
const profile = readProfile(name);
|
|
486
|
+
const env = resolveProfileEnv(profile);
|
|
388
487
|
const resolved = {
|
|
389
488
|
agent: profile.host.agent,
|
|
390
489
|
version: profile.host.version,
|
|
391
|
-
env
|
|
490
|
+
env,
|
|
392
491
|
profileName: profile.name,
|
|
393
492
|
};
|
|
394
493
|
if (profile.fallback_model) {
|
|
@@ -397,6 +496,25 @@ export function resolveProfileForRun(name) {
|
|
|
397
496
|
resolved.fallbackModel = { envKey, model: profile.fallback_model };
|
|
398
497
|
}
|
|
399
498
|
}
|
|
499
|
+
if (isTierToken(requestedModel)) {
|
|
500
|
+
const tierPick = resolveProfileTierModel(profile, requestedModel);
|
|
501
|
+
if (tierPick) {
|
|
502
|
+
const envKey = profileModelEnvKey(profile) ?? modelEnvKeyForHost(profile.host.agent);
|
|
503
|
+
env[envKey] = tierPick.model;
|
|
504
|
+
resolved.resolvedModel = tierPick.model;
|
|
505
|
+
// Mirror the native-harness tier block (lib/exec.ts's resolveTier callers):
|
|
506
|
+
// a clamp is always announced, never silent, so a user asking for "ultra"
|
|
507
|
+
// on a harness that only configures "best" knows what it actually got.
|
|
508
|
+
if (tierPick.clampedFrom) {
|
|
509
|
+
resolved.tierNote = `no "${requestedModel}" model configured on profile '${profile.name}'; using its "${tierPick.clampedFrom}" tier (${tierPick.model})`;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// No `models:` opt-in at all, or no rung to clamp to: leave `env` and
|
|
513
|
+
// `requestedModel` untouched. `agents commands/exec.ts`'s own profile-tier
|
|
514
|
+
// guard (the "cost tiers don't apply to profile ..." discard) still sees
|
|
515
|
+
// the raw tier token downstream and handles the message -- this function
|
|
516
|
+
// doesn't compete with that canonical fallback for the no-opt-in case.
|
|
517
|
+
}
|
|
400
518
|
return resolved;
|
|
401
519
|
}
|
|
402
520
|
/**
|
|
@@ -40,3 +40,12 @@ export declare function rankFocusAreas(files: string[], limit?: number): FocusAr
|
|
|
40
40
|
* the user's last fetch, which is the correct trade for a read-only card.
|
|
41
41
|
*/
|
|
42
42
|
export declare function readFocusAreas(root: string, windowDays: number): Promise<FocusArea[]>;
|
|
43
|
+
/** Compact count: 2329 → "2.3k", under 1000 stays exact. */
|
|
44
|
+
export declare function formatFocusCount(n: number): string;
|
|
45
|
+
/**
|
|
46
|
+
* One scannable focus line: path + count, with a single unit trailer so the
|
|
47
|
+
* bare integer is never mistaken for commits or minutes.
|
|
48
|
+
*
|
|
49
|
+
* apps/cli/src 2.3k · apps/cli/docs 302 · apps/factory/src 245 file-touches (7d)
|
|
50
|
+
*/
|
|
51
|
+
export declare function formatFocusAreas(areas: FocusArea[], windowDays: number): string;
|
|
@@ -78,3 +78,26 @@ export async function readFocusAreas(root, windowDays) {
|
|
|
78
78
|
return [];
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
/** Compact count: 2329 → "2.3k", under 1000 stays exact. */
|
|
82
|
+
export function formatFocusCount(n) {
|
|
83
|
+
if (!Number.isFinite(n) || n < 0)
|
|
84
|
+
return '0';
|
|
85
|
+
if (n < 1000)
|
|
86
|
+
return String(Math.round(n));
|
|
87
|
+
const k = n / 1000;
|
|
88
|
+
const s = k >= 10 ? String(Math.round(k)) : k.toFixed(1).replace(/\.0$/, '');
|
|
89
|
+
return `${s}k`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* One scannable focus line: path + count, with a single unit trailer so the
|
|
93
|
+
* bare integer is never mistaken for commits or minutes.
|
|
94
|
+
*
|
|
95
|
+
* apps/cli/src 2.3k · apps/cli/docs 302 · apps/factory/src 245 file-touches (7d)
|
|
96
|
+
*/
|
|
97
|
+
export function formatFocusAreas(areas, windowDays) {
|
|
98
|
+
if (areas.length === 0)
|
|
99
|
+
return '';
|
|
100
|
+
const body = areas.map((a) => `${a.path} ${formatFocusCount(a.touches)}`).join(' · ');
|
|
101
|
+
const unit = `file-touches (${windowDays}d)`;
|
|
102
|
+
return `${body} ${unit}`;
|
|
103
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The one worktree-aware cwd -> project fold.
|
|
3
3
|
*
|
|
4
4
|
* Several surfaces bucket work "by project": the `agents sessions` overview,
|
|
5
|
-
* the `agents
|
|
5
|
+
* the `agents feed` timeline, and anything else that has a cwd and needs a
|
|
6
6
|
* stable repo-level key. They must agree, or the same session shows up under
|
|
7
7
|
* `agents-cli` in one view and `my-branch-slug` in another — so the rule lives
|
|
8
8
|
* here and every caller delegates.
|
package/dist/lib/project-key.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The one worktree-aware cwd -> project fold.
|
|
3
3
|
*
|
|
4
4
|
* Several surfaces bucket work "by project": the `agents sessions` overview,
|
|
5
|
-
* the `agents
|
|
5
|
+
* the `agents feed` timeline, and anything else that has a cwd and needs a
|
|
6
6
|
* stable repo-level key. They must agree, or the same session shows up under
|
|
7
7
|
* `agents-cli` in one view and `my-branch-slug` in another — so the rule lives
|
|
8
8
|
* here and every caller delegates.
|
|
@@ -73,3 +73,21 @@ export declare function formatWorkspaceLine(s: RepoWorkspaceStatus): string;
|
|
|
73
73
|
* `fleet` row label.
|
|
74
74
|
*/
|
|
75
75
|
export declare function formatFleetWorkspaces(statuses: HostWorkspaceStatus[]): string[];
|
|
76
|
+
/** Severity for a workspace/repo warning on the project card. */
|
|
77
|
+
export type WorkspaceWarningSeverity = 'critical' | 'continue';
|
|
78
|
+
export interface WorkspaceWarning {
|
|
79
|
+
severity: WorkspaceWarningSeverity;
|
|
80
|
+
text: string;
|
|
81
|
+
remediation?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Turn probed workspace rows into card-footer warnings.
|
|
85
|
+
*
|
|
86
|
+
* - missing / unreadable git → critical (agents there cannot share a tree)
|
|
87
|
+
* - behind upstream → critical when ≥10 commits, continue otherwise
|
|
88
|
+
* - dirty tree → continue (local work is fine; just note it)
|
|
89
|
+
* - ahead-only is not a warning (that is progress waiting to push)
|
|
90
|
+
*
|
|
91
|
+
* Pure. Caller decides whether the rows came from `--fleet` or a local probe.
|
|
92
|
+
*/
|
|
93
|
+
export declare function workspaceWarnings(statuses: HostWorkspaceStatus[]): WorkspaceWarning[];
|
|
@@ -158,3 +158,49 @@ export function formatFleetWorkspaces(statuses) {
|
|
|
158
158
|
return multi ? `${chalk.dim(`${p} · `)}${body}` : body;
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Turn probed workspace rows into card-footer warnings.
|
|
163
|
+
*
|
|
164
|
+
* - missing / unreadable git → critical (agents there cannot share a tree)
|
|
165
|
+
* - behind upstream → critical when ≥10 commits, continue otherwise
|
|
166
|
+
* - dirty tree → continue (local work is fine; just note it)
|
|
167
|
+
* - ahead-only is not a warning (that is progress waiting to push)
|
|
168
|
+
*
|
|
169
|
+
* Pure. Caller decides whether the rows came from `--fleet` or a local probe.
|
|
170
|
+
*/
|
|
171
|
+
export function workspaceWarnings(statuses) {
|
|
172
|
+
const out = [];
|
|
173
|
+
for (const s of [...statuses].sort((a, b) => a.host.localeCompare(b.host) || a.path.localeCompare(b.path))) {
|
|
174
|
+
const where = s.host ? `${s.host}` : 'local';
|
|
175
|
+
const pathBit = s.path ? ` (${s.path})` : '';
|
|
176
|
+
if (!s.present) {
|
|
177
|
+
out.push({
|
|
178
|
+
severity: 'critical',
|
|
179
|
+
text: `${where}: checkout missing${pathBit}`,
|
|
180
|
+
remediation: 'clone or sync the project root on that host before landing agents there',
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (s.error) {
|
|
185
|
+
out.push({
|
|
186
|
+
severity: 'critical',
|
|
187
|
+
text: `${where}: ${s.error}${pathBit}`,
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (s.behind !== undefined && s.behind > 0) {
|
|
192
|
+
out.push({
|
|
193
|
+
severity: s.behind >= 10 ? 'critical' : 'continue',
|
|
194
|
+
text: `${where} is ${s.behind} commit${s.behind === 1 ? '' : 's'} behind ${s.upstream ?? 'upstream'}${pathBit}`,
|
|
195
|
+
remediation: 'pull (or rebase) before agents on this host open PRs against a stale base',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (s.dirty !== undefined && s.dirty > 0) {
|
|
199
|
+
out.push({
|
|
200
|
+
severity: 'continue',
|
|
201
|
+
text: `${where} has ${s.dirty} uncommitted change${s.dirty === 1 ? '' : 's'}${pathBit}`,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
@@ -53,6 +53,13 @@ export interface ProjectSessionRollup {
|
|
|
53
53
|
* containing only projects with at least one matched session — callers merge
|
|
54
54
|
* with the full definition list to show zero-agent projects.
|
|
55
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* Ensure every session carries a host for the card roster. Local
|
|
58
|
+
* `getActiveSessions()` omits `machine`; remotes already set it. Pure.
|
|
59
|
+
*/
|
|
60
|
+
export declare function withDefaultMachine<T extends {
|
|
61
|
+
machine?: string;
|
|
62
|
+
}>(sessions: T[], defaultHost: string): T[];
|
|
56
63
|
export declare function rollupSessionsByProject(defs: ProjectDef[], sessions: ActiveSession[]): Map<string, ProjectSessionRollup>;
|
|
57
64
|
/**
|
|
58
65
|
* True when a session's status means it is over. Exported so the card can keep
|
|
@@ -84,6 +91,19 @@ export declare function liveDeadSplit(byStatus: Partial<Record<ActiveStatus, num
|
|
|
84
91
|
export declare function sortProjectMembers(members: ProjectMember[]): ProjectMember[];
|
|
85
92
|
/** Cap for the members line before it collapses to `+N more`. */
|
|
86
93
|
export declare const MEMBERS_LINE_LIMIT = 6;
|
|
94
|
+
/** Cap for host groups on the multi-line agents roster. */
|
|
95
|
+
export declare const MEMBERS_HOST_LIMIT = 8;
|
|
96
|
+
/**
|
|
97
|
+
* Collapse members into distinct state cells (`agent · status · ticket[@host]`),
|
|
98
|
+
* counting duplicates as `×N`. Pure.
|
|
99
|
+
*/
|
|
100
|
+
export declare function collapseMemberCells(members: ProjectMember[], opts?: {
|
|
101
|
+
includeHostOnCell?: boolean;
|
|
102
|
+
}): Array<{
|
|
103
|
+
cell: string;
|
|
104
|
+
n: number;
|
|
105
|
+
members: number;
|
|
106
|
+
}>;
|
|
87
107
|
/**
|
|
88
108
|
* The `agents` line under `live`: one cell per DISTINCT member state —
|
|
89
109
|
* `claude · running · RUSH-2107 @zion` — with identical cells collapsed to a
|
|
@@ -91,8 +111,44 @@ export declare const MEMBERS_LINE_LIMIT = 6;
|
|
|
91
111
|
* truncated duplicates), capped at {@link MEMBERS_LINE_LIMIT} cells with a
|
|
92
112
|
* `+N more` tail counting members, not cells. Pure (chalk styling only); the
|
|
93
113
|
* caller adds the label.
|
|
114
|
+
*
|
|
115
|
+
* Prefer {@link formatProjectMembersByHost} on the card: a flat line hides which
|
|
116
|
+
* machine is running the work when the same harness×status spans hosts.
|
|
94
117
|
*/
|
|
95
118
|
export declare function formatProjectMembers(members: ProjectMember[], limit?: number): string;
|
|
119
|
+
/**
|
|
120
|
+
* Host-grouped agents roster. One content line per host:
|
|
121
|
+
* `@zion claude · running ×9 · claude · idle ×4`
|
|
122
|
+
*
|
|
123
|
+
* When no member carries a host (local-only rollup with no machine stamp), falls
|
|
124
|
+
* back to a single flat line via {@link formatProjectMembers}. Pure.
|
|
125
|
+
*/
|
|
126
|
+
export declare function formatProjectMembersByHost(members: ProjectMember[], opts?: {
|
|
127
|
+
cellLimit?: number;
|
|
128
|
+
hostLimit?: number;
|
|
129
|
+
}): string[];
|
|
130
|
+
/** A card-level warning collected for the footer. */
|
|
131
|
+
export type ProjectWarningSeverity = 'critical' | 'continue';
|
|
132
|
+
export interface ProjectWarning {
|
|
133
|
+
severity: ProjectWarningSeverity;
|
|
134
|
+
/** One human line. */
|
|
135
|
+
text: string;
|
|
136
|
+
/** Optional fix or next step. */
|
|
137
|
+
remediation?: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Severity markers for the warnings footer. User-facing by design: critical
|
|
141
|
+
* stops you (wrong repo, missing checkout, large drift); continue is a soft
|
|
142
|
+
* nudge (dirty tree, schedule not measurable).
|
|
143
|
+
*/
|
|
144
|
+
export declare function warningEmoji(severity: ProjectWarningSeverity): string;
|
|
145
|
+
/** Stable sort: critical first, then continue; stable within a tier. */
|
|
146
|
+
export declare function sortProjectWarnings(warnings: ProjectWarning[]): ProjectWarning[];
|
|
147
|
+
/**
|
|
148
|
+
* Format one or more warning lines for the card footer. Pure (chalk only).
|
|
149
|
+
* Returns empty when there is nothing to say.
|
|
150
|
+
*/
|
|
151
|
+
export declare function formatProjectWarnings(warnings: ProjectWarning[]): string[];
|
|
96
152
|
/** Harvested signals not on the session list: repo-global merged PRs + releases, local artifacts, in a time window. */
|
|
97
153
|
export interface ProjectRemoteSignals {
|
|
98
154
|
windowDays: number;
|
|
@@ -41,6 +41,13 @@ function blank(name) {
|
|
|
41
41
|
* containing only projects with at least one matched session — callers merge
|
|
42
42
|
* with the full definition list to show zero-agent projects.
|
|
43
43
|
*/
|
|
44
|
+
/**
|
|
45
|
+
* Ensure every session carries a host for the card roster. Local
|
|
46
|
+
* `getActiveSessions()` omits `machine`; remotes already set it. Pure.
|
|
47
|
+
*/
|
|
48
|
+
export function withDefaultMachine(sessions, defaultHost) {
|
|
49
|
+
return sessions.map((s) => (s.machine ? s : { ...s, machine: defaultHost }));
|
|
50
|
+
}
|
|
44
51
|
export function rollupSessionsByProject(defs, sessions) {
|
|
45
52
|
const map = new Map();
|
|
46
53
|
const prSeen = new Map();
|
|
@@ -148,25 +155,20 @@ export function sortProjectMembers(members) {
|
|
|
148
155
|
}
|
|
149
156
|
/** Cap for the members line before it collapses to `+N more`. */
|
|
150
157
|
export const MEMBERS_LINE_LIMIT = 6;
|
|
158
|
+
/** Cap for host groups on the multi-line agents roster. */
|
|
159
|
+
export const MEMBERS_HOST_LIMIT = 8;
|
|
151
160
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* `×N` count (35 same-harness sessions in one state are one fact, not six
|
|
155
|
-
* truncated duplicates), capped at {@link MEMBERS_LINE_LIMIT} cells with a
|
|
156
|
-
* `+N more` tail counting members, not cells. Pure (chalk styling only); the
|
|
157
|
-
* caller adds the label.
|
|
161
|
+
* Collapse members into distinct state cells (`agent · status · ticket[@host]`),
|
|
162
|
+
* counting duplicates as `×N`. Pure.
|
|
158
163
|
*/
|
|
159
|
-
export function
|
|
160
|
-
|
|
161
|
-
return '';
|
|
162
|
-
// Collapse identical cells — 35 same-harness sessions in the same state are
|
|
163
|
-
// one fact (`claude · running ×16`), not six truncated duplicates.
|
|
164
|
+
export function collapseMemberCells(members, opts = {}) {
|
|
165
|
+
const includeHost = opts.includeHostOnCell !== false;
|
|
164
166
|
const counts = new Map();
|
|
165
167
|
for (const m of sortProjectMembers(members)) {
|
|
166
168
|
const parts = [m.agent, m.status];
|
|
167
169
|
if (m.ticket)
|
|
168
170
|
parts.push(m.ticket);
|
|
169
|
-
const cell = parts.join(' · ') + (m.host ? ` @${m.host}` : '');
|
|
171
|
+
const cell = parts.join(' · ') + (includeHost && m.host ? ` @${m.host}` : '');
|
|
170
172
|
const key = cell.toLowerCase();
|
|
171
173
|
const entry = counts.get(key);
|
|
172
174
|
if (entry)
|
|
@@ -174,12 +176,117 @@ export function formatProjectMembers(members, limit = MEMBERS_LINE_LIMIT) {
|
|
|
174
176
|
else
|
|
175
177
|
counts.set(key, { cell, n: 1 });
|
|
176
178
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
return [...counts.values()].map(({ cell, n }) => ({ cell, n, members: n }));
|
|
180
|
+
}
|
|
181
|
+
function formatCollapsedCells(cells, memberTotal, limit) {
|
|
182
|
+
if (cells.length === 0)
|
|
183
|
+
return '';
|
|
184
|
+
const shown = cells.slice(0, Math.max(1, limit));
|
|
185
|
+
const shownMembers = shown.reduce((acc, e) => acc + e.members, 0);
|
|
186
|
+
const more = memberTotal - shownMembers;
|
|
187
|
+
const parts = shown.map(({ cell, n }) => (n > 1 ? `${cell} ×${n}` : cell));
|
|
188
|
+
return parts.join(chalk.dim(' · ')) + (more > 0 ? chalk.dim(` · +${more} more`) : '');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The `agents` line under `live`: one cell per DISTINCT member state —
|
|
192
|
+
* `claude · running · RUSH-2107 @zion` — with identical cells collapsed to a
|
|
193
|
+
* `×N` count (35 same-harness sessions in one state are one fact, not six
|
|
194
|
+
* truncated duplicates), capped at {@link MEMBERS_LINE_LIMIT} cells with a
|
|
195
|
+
* `+N more` tail counting members, not cells. Pure (chalk styling only); the
|
|
196
|
+
* caller adds the label.
|
|
197
|
+
*
|
|
198
|
+
* Prefer {@link formatProjectMembersByHost} on the card: a flat line hides which
|
|
199
|
+
* machine is running the work when the same harness×status spans hosts.
|
|
200
|
+
*/
|
|
201
|
+
export function formatProjectMembers(members, limit = MEMBERS_LINE_LIMIT) {
|
|
202
|
+
if (members.length === 0)
|
|
203
|
+
return '';
|
|
204
|
+
return formatCollapsedCells(collapseMemberCells(members, { includeHostOnCell: true }), members.length, limit);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Host-grouped agents roster. One content line per host:
|
|
208
|
+
* `@zion claude · running ×9 · claude · idle ×4`
|
|
209
|
+
*
|
|
210
|
+
* When no member carries a host (local-only rollup with no machine stamp), falls
|
|
211
|
+
* back to a single flat line via {@link formatProjectMembers}. Pure.
|
|
212
|
+
*/
|
|
213
|
+
export function formatProjectMembersByHost(members, opts = {}) {
|
|
214
|
+
if (members.length === 0)
|
|
215
|
+
return [];
|
|
216
|
+
const cellLimit = opts.cellLimit ?? MEMBERS_LINE_LIMIT;
|
|
217
|
+
const hostLimit = opts.hostLimit ?? MEMBERS_HOST_LIMIT;
|
|
218
|
+
const byHost = new Map();
|
|
219
|
+
let anyHost = false;
|
|
220
|
+
for (const m of members) {
|
|
221
|
+
if (m.host)
|
|
222
|
+
anyHost = true;
|
|
223
|
+
const key = m.host ?? '';
|
|
224
|
+
const list = byHost.get(key);
|
|
225
|
+
if (list)
|
|
226
|
+
list.push(m);
|
|
227
|
+
else
|
|
228
|
+
byHost.set(key, [m]);
|
|
229
|
+
}
|
|
230
|
+
// Local-only (no host stamps at all) — keep the compact one-liner.
|
|
231
|
+
if (!anyHost) {
|
|
232
|
+
const line = formatProjectMembers(members, cellLimit);
|
|
233
|
+
return line ? [line] : [];
|
|
234
|
+
}
|
|
235
|
+
// Hosts with the most members first, then name; unstamped ("") last.
|
|
236
|
+
const hosts = [...byHost.entries()].sort((a, b) => {
|
|
237
|
+
if (a[0] === '' && b[0] !== '')
|
|
238
|
+
return 1;
|
|
239
|
+
if (b[0] === '' && a[0] !== '')
|
|
240
|
+
return -1;
|
|
241
|
+
if (b[1].length !== a[1].length)
|
|
242
|
+
return b[1].length - a[1].length;
|
|
243
|
+
return a[0].localeCompare(b[0]);
|
|
244
|
+
});
|
|
245
|
+
const shownHosts = hosts.slice(0, Math.max(1, hostLimit));
|
|
246
|
+
const hiddenMembers = hosts.slice(hostLimit).reduce((acc, [, ms]) => acc + ms.length, 0);
|
|
247
|
+
const hostWidth = Math.max(...shownHosts.map(([h]) => (h ? `@${h}` : '@local').length), 1);
|
|
248
|
+
const lines = shownHosts.map(([host, ms]) => {
|
|
249
|
+
const label = (host ? `@${host}` : '@local').padEnd(hostWidth);
|
|
250
|
+
// Host is the row key — do not repeat @host on every cell.
|
|
251
|
+
const cells = collapseMemberCells(ms, { includeHostOnCell: false });
|
|
252
|
+
const body = formatCollapsedCells(cells, ms.length, cellLimit);
|
|
253
|
+
return `${chalk.cyan(label)} ${body}`;
|
|
254
|
+
});
|
|
255
|
+
if (hiddenMembers > 0) {
|
|
256
|
+
const restHosts = hosts.length - shownHosts.length;
|
|
257
|
+
lines.push(chalk.dim(`+${hiddenMembers} more on ${restHosts} host${restHosts === 1 ? '' : 's'}`));
|
|
258
|
+
}
|
|
259
|
+
return lines;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Severity markers for the warnings footer. User-facing by design: critical
|
|
263
|
+
* stops you (wrong repo, missing checkout, large drift); continue is a soft
|
|
264
|
+
* nudge (dirty tree, schedule not measurable).
|
|
265
|
+
*/
|
|
266
|
+
export function warningEmoji(severity) {
|
|
267
|
+
return severity === 'critical' ? '🔴' : '⚠️';
|
|
268
|
+
}
|
|
269
|
+
/** Stable sort: critical first, then continue; stable within a tier. */
|
|
270
|
+
export function sortProjectWarnings(warnings) {
|
|
271
|
+
const rank = { critical: 0, continue: 1 };
|
|
272
|
+
return [...warnings].sort((a, b) => rank[a.severity] - rank[b.severity] || a.text.localeCompare(b.text));
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Format one or more warning lines for the card footer. Pure (chalk only).
|
|
276
|
+
* Returns empty when there is nothing to say.
|
|
277
|
+
*/
|
|
278
|
+
export function formatProjectWarnings(warnings) {
|
|
279
|
+
if (warnings.length === 0)
|
|
280
|
+
return [];
|
|
281
|
+
const lines = [];
|
|
282
|
+
for (const w of sortProjectWarnings(warnings)) {
|
|
283
|
+
const mark = warningEmoji(w.severity);
|
|
284
|
+
const color = w.severity === 'critical' ? chalk.red : chalk.yellow;
|
|
285
|
+
lines.push(` ${mark} ${color(w.text)}`);
|
|
286
|
+
if (w.remediation)
|
|
287
|
+
lines.push(` ${chalk.dim(w.remediation)}`);
|
|
288
|
+
}
|
|
289
|
+
return lines;
|
|
183
290
|
}
|
|
184
291
|
/**
|
|
185
292
|
* Harvest the signals that don't live on the active-session list: recently
|
|
@@ -124,6 +124,9 @@ export function getMcpConfigPath(agent, versionHome) {
|
|
|
124
124
|
return path.join(versionHome, '.grok', 'mcp.json');
|
|
125
125
|
case 'hermes':
|
|
126
126
|
return path.join(versionHome, '.hermes', 'config.yaml');
|
|
127
|
+
case 'pi':
|
|
128
|
+
// omp reads user-scope MCP from ~/.omp/agent/.mcp.json (Claude schema).
|
|
129
|
+
return path.join(versionHome, '.omp', 'agent', '.mcp.json');
|
|
127
130
|
default:
|
|
128
131
|
return null;
|
|
129
132
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Union: All resources from all layers are combined
|
|
6
6
|
* - Override on name conflict: Higher layer wins (project > user > system)
|
|
7
7
|
*/
|
|
8
|
-
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes';
|
|
8
|
+
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi';
|
|
9
9
|
export type Layer = 'system' | 'user' | 'project';
|
|
10
10
|
export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow' | 'memory';
|
|
11
11
|
/** A resolved resource with its origin layer. */
|
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { AgentId, RunStrategy } from './types.js';
|
|
|
8
8
|
import type { FallbackEntry } from './exec.js';
|
|
9
9
|
import { type AccountInfo, type CredentialPresence } from './agents.js';
|
|
10
10
|
import { type UsageSnapshot } from './usage.js';
|
|
11
|
+
import { type AuthVerdict } from './auth-health.js';
|
|
11
12
|
export interface RotateCandidate {
|
|
12
13
|
agent: AgentId;
|
|
13
14
|
version: string;
|
|
@@ -35,6 +36,18 @@ export interface RotateCandidate {
|
|
|
35
36
|
usageMinutesToLimit: number | null;
|
|
36
37
|
plan: string | null;
|
|
37
38
|
signedIn: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Live auth-health verdict for this (agent, version) from the daemon's probe
|
|
41
|
+
* cache (`auth-health.ts`), or null when no probe row exists (cold cache, or a
|
|
42
|
+
* harness with no live-probe endpoint). `signedIn` only means "a credential
|
|
43
|
+
* file is present and its email decodes" — it cannot tell a good token from a
|
|
44
|
+
* revoked-but-unexpired one, so a server-rejected account reads
|
|
45
|
+
* `signedIn: true` but `authVerdict: 'revoked'`. Eligibility excludes a
|
|
46
|
+
* revoked account so rotation never launches into a doomed auth (see
|
|
47
|
+
* {@link readinessFromCandidate}). Fail-open: any non-revoked or null verdict
|
|
48
|
+
* does not gate — a stale/absent probe never blocks a launch.
|
|
49
|
+
*/
|
|
50
|
+
authVerdict: AuthVerdict | null;
|
|
38
51
|
lastActive: Date | null;
|
|
39
52
|
}
|
|
40
53
|
export interface RotateResult {
|
|
@@ -108,15 +121,17 @@ export declare const USAGE_DECISION_MAX_AGE_MS: number;
|
|
|
108
121
|
export declare function isUsageVerified(candidate: RotateCandidate, nowMs?: number): boolean;
|
|
109
122
|
/**
|
|
110
123
|
* Whether a specific account can serve a run right now, and — when it can't —
|
|
111
|
-
* why. `signed_out` covers a missing usable credential; `
|
|
112
|
-
*
|
|
113
|
-
*
|
|
124
|
+
* why. `signed_out` covers a missing usable credential; `revoked` is a token the
|
|
125
|
+
* server has actually rejected (401/403, from the live auth-health probe);
|
|
126
|
+
* `rate_limited` and `out_of_credits` name the throttle. Used to pre-warn on a
|
|
127
|
+
* version-pinned teammate whose account rotation won't route around (a pin IS
|
|
128
|
+
* the target).
|
|
114
129
|
*/
|
|
115
130
|
export type AccountReadiness = {
|
|
116
131
|
ready: true;
|
|
117
132
|
} | {
|
|
118
133
|
ready: false;
|
|
119
|
-
reason: 'rate_limited' | 'out_of_credits' | 'signed_out';
|
|
134
|
+
reason: 'rate_limited' | 'out_of_credits' | 'signed_out' | 'revoked';
|
|
120
135
|
email: string | null;
|
|
121
136
|
};
|
|
122
137
|
/**
|