@phnx-labs/agents-cli 1.22.75 → 1.22.76
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 +117 -0
- package/README.md +20 -8
- package/dist/bootstrap.js +7 -7
- package/dist/cli/command-registry.js +5 -0
- package/dist/commands/artifacts-setup.js +1 -1
- package/dist/commands/artifacts.js +1 -1
- package/dist/commands/auth.js +7 -1
- package/dist/commands/browser.js +104 -10
- package/dist/commands/commands.js +7 -6
- package/dist/commands/config.js +27 -4
- package/dist/commands/cost.js +6 -4
- package/dist/commands/doctor.d.ts +6 -5
- package/dist/commands/doctor.js +32 -274
- package/dist/commands/exec.d.ts +2 -0
- package/dist/commands/exec.js +9 -2
- package/dist/commands/harness.d.ts +1 -0
- package/dist/commands/harness.js +11 -3
- package/dist/commands/hooks.js +7 -6
- package/dist/commands/mcp.js +7 -6
- package/dist/commands/memory.js +7 -7
- package/dist/commands/monitors.js +3 -2
- package/dist/commands/open.d.ts +25 -12
- package/dist/commands/open.js +24 -10
- package/dist/commands/permissions.js +7 -6
- package/dist/commands/plugins.js +21 -17
- package/dist/commands/route.js +33 -16
- package/dist/commands/rules.js +7 -12
- package/dist/commands/sessions-share.js +1 -1
- package/dist/commands/setup-watchdog.js +2 -2
- package/dist/commands/setup.js +22 -1
- package/dist/commands/share.js +26 -10
- package/dist/commands/skills.js +7 -6
- package/dist/commands/subagents.js +7 -6
- package/dist/commands/sync.js +81 -10
- package/dist/commands/view.js +4 -1
- package/dist/commands/watchdog.d.ts +1 -1
- package/dist/commands/watchdog.js +10 -10
- package/dist/commands/webhook.d.ts +4 -0
- package/dist/commands/webhook.js +22 -4
- package/dist/commands/workflows.js +7 -6
- package/dist/lib/accounting/rotate.d.ts +3 -1
- package/dist/lib/accounting/rotate.js +8 -4
- package/dist/lib/auth-health.d.ts +2 -0
- package/dist/lib/auth-health.js +2 -0
- package/dist/lib/browser/chrome.d.ts +21 -0
- package/dist/lib/browser/chrome.js +60 -3
- package/dist/lib/browser/drivers/local.d.ts +21 -0
- package/dist/lib/browser/drivers/local.js +102 -9
- package/dist/lib/browser/profiles.d.ts +29 -1
- package/dist/lib/browser/profiles.js +50 -1
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/config-keys.d.ts +7 -2
- package/dist/lib/config-keys.js +17 -2
- package/dist/lib/daemon/daemon.js +8 -0
- package/dist/lib/daemon/session-summarizer-service.d.ts +24 -0
- package/dist/lib/daemon/session-summarizer-service.js +39 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/daemon-ticks.d.ts +2 -2
- package/dist/lib/daemon-ticks.js +2 -1
- package/dist/lib/deeplink/register.js +10 -9
- package/dist/lib/deeplink/url.d.ts +4 -4
- package/dist/lib/deeplink/url.js +4 -4
- package/dist/lib/device-config.js +25 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -4
- package/dist/lib/devices/doctor-findings.js +14 -8
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/stats-cache.d.ts +4 -0
- package/dist/lib/devices/stats-cache.js +19 -0
- package/dist/lib/drift-sync.d.ts +3 -1
- package/dist/lib/drift-sync.js +16 -5
- package/dist/lib/exec.d.ts +2 -0
- package/dist/lib/exec.js +16 -1
- package/dist/lib/fleet-shared-state.d.ts +8 -0
- package/dist/lib/heal.d.ts +4 -3
- package/dist/lib/heal.js +5 -4
- package/dist/lib/hosts/ready.d.ts +1 -1
- package/dist/lib/hosts/ready.js +16 -4
- package/dist/lib/hosts/reconnect.js +4 -2
- package/dist/lib/identity/client.d.ts +6 -0
- package/dist/lib/identity/index.d.ts +16 -0
- package/dist/lib/identity/index.js +25 -1
- package/dist/lib/profiles.d.ts +2 -0
- package/dist/lib/profiles.js +28 -9
- package/dist/lib/reconcile-and-repair.d.ts +109 -0
- package/dist/lib/reconcile-and-repair.js +267 -0
- package/dist/lib/routers.d.ts +12 -1
- package/dist/lib/routers.js +30 -1
- package/dist/lib/scheduling/routines.js +8 -2
- package/dist/lib/session/active.d.ts +13 -0
- package/dist/lib/session/db.d.ts +47 -7
- package/dist/lib/session/db.js +114 -12
- package/dist/lib/session/mirror.js +58 -0
- package/dist/lib/session/remote/watch.js +22 -2
- package/dist/lib/session/session-cache.d.ts +19 -0
- package/dist/lib/session/session-cache.js +46 -0
- package/dist/lib/session/types.d.ts +34 -0
- package/dist/lib/share/backend.d.ts +6 -4
- package/dist/lib/share/backend.js +10 -8
- package/dist/lib/share/config.d.ts +4 -3
- package/dist/lib/share/config.js +10 -1
- package/dist/lib/share/delete.d.ts +1 -1
- package/dist/lib/share/delete.js +1 -1
- package/dist/lib/share/html.d.ts +1 -1
- package/dist/lib/share/html.js +1 -1
- package/dist/lib/share/provision.d.ts +1 -1
- package/dist/lib/share/provision.js +2 -2
- package/dist/lib/share/publish.d.ts +23 -7
- package/dist/lib/share/publish.js +58 -12
- package/dist/lib/share/worker-template.js +221 -60
- package/dist/lib/startup/command-registry.js +2 -2
- package/dist/lib/state.d.ts +15 -0
- package/dist/lib/state.js +29 -7
- package/dist/lib/summarizer/config.d.ts +46 -0
- package/dist/lib/summarizer/config.js +83 -0
- package/dist/lib/summarizer/pass.d.ts +45 -0
- package/dist/lib/summarizer/pass.js +112 -0
- package/dist/lib/summarizer/summarize.d.ts +68 -0
- package/dist/lib/summarizer/summarize.js +120 -0
- package/dist/lib/teams/agents.d.ts +4 -3
- package/dist/lib/teams/agents.js +12 -4
- package/dist/lib/teams/scheduler.d.ts +4 -2
- package/dist/lib/teams/scheduler.js +6 -6
- package/dist/lib/tmux/session.d.ts +2 -0
- package/dist/lib/tmux/session.js +7 -1
- package/dist/lib/types.d.ts +20 -0
- package/dist/lib/verbs.d.ts +23 -0
- package/dist/lib/verbs.js +24 -0
- package/dist/lib/view-types.d.ts +4 -0
- package/dist/lib/watchdog/rotate.d.ts +1 -1
- package/dist/lib/watchdog/rotate.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-summarizer configuration (PHNX-3939).
|
|
3
|
+
*
|
|
4
|
+
* Resolves the three knobs behind the daemon summarizer — enabled / base URL /
|
|
5
|
+
* model — from `agents config` (`summarizer.*`, user-scope in the central
|
|
6
|
+
* agents.yaml) with a per-process env override
|
|
7
|
+
* (`AGENTS_SUMMARIZER_ENABLED` / `AGENTS_SUMMARIZER_BASEURL` /
|
|
8
|
+
* `AGENTS_SUMMARIZER_MODEL`). Off by default: with no config and no env, the
|
|
9
|
+
* summarizer is disabled and makes zero model calls.
|
|
10
|
+
*
|
|
11
|
+
* `isSummarizerReady()` is memoized on a short TTL because the display merge
|
|
12
|
+
* (the watch-stream projections) calls it once per session row — a fresh
|
|
13
|
+
* agents.yaml read per row would defeat the "blazing fast" requirement.
|
|
14
|
+
*/
|
|
15
|
+
import { getConfigValue } from '../device-config.js';
|
|
16
|
+
/** Parse a boolean-ish env value; undefined when the var is unset. */
|
|
17
|
+
function envBool(raw) {
|
|
18
|
+
if (raw === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
const v = raw.trim().toLowerCase();
|
|
21
|
+
if (v === '1' || v === 'true' || v === 'on' || v === 'yes')
|
|
22
|
+
return true;
|
|
23
|
+
if (v === '0' || v === 'false' || v === 'off' || v === 'no' || v === '')
|
|
24
|
+
return false;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function envString(raw) {
|
|
28
|
+
const v = raw?.trim();
|
|
29
|
+
return v ? v : undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the full summarizer config. Env overrides the stored config key by key;
|
|
33
|
+
* an unset env var falls through to `agents config`, then to the built-in
|
|
34
|
+
* default (disabled). Never throws — a missing/corrupt config reads as unset.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveSummarizerConfig(env = process.env) {
|
|
37
|
+
let storedEnabled;
|
|
38
|
+
let storedBaseUrl;
|
|
39
|
+
let storedModel;
|
|
40
|
+
try {
|
|
41
|
+
storedEnabled = getConfigValue('summarizer.enabled').value;
|
|
42
|
+
storedBaseUrl = getConfigValue('summarizer.baseUrl').value;
|
|
43
|
+
storedModel = getConfigValue('summarizer.model').value;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// A missing/corrupt config store must not break the read path — treat as unset.
|
|
47
|
+
}
|
|
48
|
+
const enabled = envBool(env.AGENTS_SUMMARIZER_ENABLED) ?? storedEnabled ?? false;
|
|
49
|
+
const baseUrl = envString(env.AGENTS_SUMMARIZER_BASEURL) ?? envString(storedBaseUrl);
|
|
50
|
+
const model = envString(env.AGENTS_SUMMARIZER_MODEL) ?? envString(storedModel);
|
|
51
|
+
return { enabled, baseUrl, model };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* True only when the summarizer is enabled AND has a base URL + model to call.
|
|
55
|
+
* A configuration that is `enabled` but missing an endpoint cannot produce a
|
|
56
|
+
* summary, so it is treated as unconfigured (the service no-ops, the merge marks
|
|
57
|
+
* `skipped`) rather than erroring on every tick.
|
|
58
|
+
*/
|
|
59
|
+
export function isSummarizerRunnable(config) {
|
|
60
|
+
return config.enabled && Boolean(config.baseUrl) && Boolean(config.model);
|
|
61
|
+
}
|
|
62
|
+
let cachedReady = null;
|
|
63
|
+
const READY_TTL_MS = 3_000;
|
|
64
|
+
/**
|
|
65
|
+
* Memoized "will a summary actually be produced?" check for the hot merge path.
|
|
66
|
+
* Reflects {@link isSummarizerRunnable} — enabled AND a base URL AND a model —
|
|
67
|
+
* NOT just `enabled`, because an enabled-but-unconfigured summarizer computes
|
|
68
|
+
* nothing, so a row with no cached summary must read `skipped`, not a `pending`
|
|
69
|
+
* that never resolves (the exact case: `summarizer.enabled on` set before the
|
|
70
|
+
* endpoint). TTL keeps a config change visible within a few seconds without a
|
|
71
|
+
* per-row agents.yaml read.
|
|
72
|
+
*/
|
|
73
|
+
export function isSummarizerReady(nowMs = Date.now()) {
|
|
74
|
+
if (cachedReady && nowMs - cachedReady.at < READY_TTL_MS)
|
|
75
|
+
return cachedReady.value;
|
|
76
|
+
const value = isSummarizerRunnable(resolveSummarizerConfig());
|
|
77
|
+
cachedReady = { at: nowMs, value };
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
/** Test seam: drop the memoized ready flag so the next read re-resolves. */
|
|
81
|
+
export function resetSummarizerReadyCacheForTest() {
|
|
82
|
+
cachedReady = null;
|
|
83
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One session-summarizer pass (PHNX-3939) — the work the background
|
|
3
|
+
* SessionSummarizerService does per tick, factored out so it is testable with an
|
|
4
|
+
* injected model stub and no daemon scaffolding.
|
|
5
|
+
*
|
|
6
|
+
* Off the request path by construction: it reads THIS box's already-published
|
|
7
|
+
* live sessions (the warm cache the session-state service writes — never a fresh
|
|
8
|
+
* gather) and, per session, reuses the transcript-keyed `session_summaries` row
|
|
9
|
+
* until the transcript bytes change, so an unchanged session costs one indexed
|
|
10
|
+
* read and zero model calls. When the summarizer is disabled or has no endpoint,
|
|
11
|
+
* the pass does nothing at all.
|
|
12
|
+
*/
|
|
13
|
+
import type { ActiveSession } from '../session/active.js';
|
|
14
|
+
import { type SummarizerConfig } from './config.js';
|
|
15
|
+
import { summarize as defaultSummarize } from './summarize.js';
|
|
16
|
+
/** Max sessions summarized per tick — a debounce ceiling on local-model calls. */
|
|
17
|
+
export declare const SUMMARIZER_MAX_PER_TICK = 8;
|
|
18
|
+
export interface SummarizerPassOptions {
|
|
19
|
+
now?: number;
|
|
20
|
+
config?: SummarizerConfig;
|
|
21
|
+
/** Live sessions to consider; default = this box's warm local-session cache. */
|
|
22
|
+
sessions?: ActiveSession[];
|
|
23
|
+
/** Injectable model call (tests stub the endpoint here). */
|
|
24
|
+
summarizeImpl?: typeof defaultSummarize;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
maxPerTick?: number;
|
|
27
|
+
/** Injectable stat for tests; defaults to fs.statSync. */
|
|
28
|
+
statFile?: (p: string) => {
|
|
29
|
+
mtimeMs: number;
|
|
30
|
+
size: number;
|
|
31
|
+
};
|
|
32
|
+
/** Skip the reader-recency gate (tests pass sessions directly). */
|
|
33
|
+
requireReader?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface SummarizerPassResult {
|
|
36
|
+
disabled: boolean;
|
|
37
|
+
computed: number;
|
|
38
|
+
reused: number;
|
|
39
|
+
skipped: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run one pass. Returns per-outcome counts. Never throws — a per-session failure
|
|
43
|
+
* is isolated so one bad transcript can't stall the whole tick.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runSummarizerPass(opts?: SummarizerPassOptions): Promise<SummarizerPassResult>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One session-summarizer pass (PHNX-3939) — the work the background
|
|
3
|
+
* SessionSummarizerService does per tick, factored out so it is testable with an
|
|
4
|
+
* injected model stub and no daemon scaffolding.
|
|
5
|
+
*
|
|
6
|
+
* Off the request path by construction: it reads THIS box's already-published
|
|
7
|
+
* live sessions (the warm cache the session-state service writes — never a fresh
|
|
8
|
+
* gather) and, per session, reuses the transcript-keyed `session_summaries` row
|
|
9
|
+
* until the transcript bytes change, so an unchanged session costs one indexed
|
|
10
|
+
* read and zero model calls. When the summarizer is disabled or has no endpoint,
|
|
11
|
+
* the pass does nothing at all.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import { readSessionSummary, readSessionSummaryAny, writeSessionSummary, } from '../session/db.js';
|
|
15
|
+
import { isActiveSessionsJournalReaderRecent, readActiveSessionsCache, } from '../session/session-cache.js';
|
|
16
|
+
import { resolveSummarizerConfig, isSummarizerRunnable } from './config.js';
|
|
17
|
+
import { summarize as defaultSummarize } from './summarize.js';
|
|
18
|
+
/** Max sessions summarized per tick — a debounce ceiling on local-model calls. */
|
|
19
|
+
export const SUMMARIZER_MAX_PER_TICK = 8;
|
|
20
|
+
/**
|
|
21
|
+
* Merge model checkpoint TEXTS with any prior stored checkpoints so a line that
|
|
22
|
+
* already existed keeps its original `at`, and only genuinely new lines get
|
|
23
|
+
* stamped `now`. Order follows the model output (newest last).
|
|
24
|
+
*/
|
|
25
|
+
function stampCheckpoints(texts, prior, nowIso) {
|
|
26
|
+
const priorAt = new Map((prior ?? []).map((c) => [c.text, c.at]));
|
|
27
|
+
return texts.map((text) => ({ text, at: priorAt.get(text) ?? nowIso }));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Run one pass. Returns per-outcome counts. Never throws — a per-session failure
|
|
31
|
+
* is isolated so one bad transcript can't stall the whole tick.
|
|
32
|
+
*/
|
|
33
|
+
export async function runSummarizerPass(opts = {}) {
|
|
34
|
+
const now = opts.now ?? Date.now();
|
|
35
|
+
const config = opts.config ?? resolveSummarizerConfig();
|
|
36
|
+
const result = { disabled: false, computed: 0, reused: 0, skipped: 0 };
|
|
37
|
+
if (!isSummarizerRunnable(config)) {
|
|
38
|
+
result.disabled = true;
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
let sessions = opts.sessions;
|
|
42
|
+
if (!sessions) {
|
|
43
|
+
// Reader-gated like the active-sessions warm tick: no watcher, no work.
|
|
44
|
+
if (opts.requireReader !== false && !isActiveSessionsJournalReaderRecent(now))
|
|
45
|
+
return result;
|
|
46
|
+
sessions = readActiveSessionsCache('local')?.sessions ?? [];
|
|
47
|
+
}
|
|
48
|
+
const stat = opts.statFile ?? ((p) => {
|
|
49
|
+
const s = fs.statSync(p);
|
|
50
|
+
return { mtimeMs: s.mtimeMs, size: s.size };
|
|
51
|
+
});
|
|
52
|
+
const runSummarize = opts.summarizeImpl ?? defaultSummarize;
|
|
53
|
+
const maxPerTick = opts.maxPerTick ?? SUMMARIZER_MAX_PER_TICK;
|
|
54
|
+
const nowIso = new Date(now).toISOString();
|
|
55
|
+
let budget = maxPerTick;
|
|
56
|
+
for (const s of sessions) {
|
|
57
|
+
if (opts.signal?.aborted)
|
|
58
|
+
break;
|
|
59
|
+
if (budget <= 0)
|
|
60
|
+
break;
|
|
61
|
+
const id = s.sessionId;
|
|
62
|
+
const file = s.sessionFile;
|
|
63
|
+
if (!id || !file)
|
|
64
|
+
continue;
|
|
65
|
+
let stamp;
|
|
66
|
+
try {
|
|
67
|
+
const st = stat(file);
|
|
68
|
+
stamp = { fileMtimeMs: Math.round(st.mtimeMs), fileSize: st.size };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue; // transcript unreadable this tick — try again next time
|
|
72
|
+
}
|
|
73
|
+
// Cached for these exact bytes → never recompute (the core "blazing fast" rule).
|
|
74
|
+
if (readSessionSummary(id, stamp)) {
|
|
75
|
+
result.reused++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
budget--;
|
|
79
|
+
const prompt = (s.firstUserMessage ?? s.topic ?? '').trim();
|
|
80
|
+
const prior = readSessionSummaryAny(id);
|
|
81
|
+
if (!prompt) {
|
|
82
|
+
// No extractable intent yet — cache a skip against these bytes so we don't
|
|
83
|
+
// re-attempt every tick; a later transcript write (new bytes) retries.
|
|
84
|
+
writeSessionSummary({ id, ...stamp, summary: { summaryState: 'skipped' } });
|
|
85
|
+
result.skipped++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
let computed;
|
|
89
|
+
try {
|
|
90
|
+
computed = await runSummarize(prompt, { todos: s.todos, plan: s.plan, phase: s.phase }, { baseUrl: config.baseUrl, model: config.model, ...(opts.signal ? { signal: opts.signal } : {}) });
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
computed = undefined;
|
|
94
|
+
}
|
|
95
|
+
if (!computed) {
|
|
96
|
+
writeSessionSummary({ id, ...stamp, summary: { summaryState: 'skipped' } });
|
|
97
|
+
result.skipped++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const entry = {
|
|
101
|
+
// Goal is computed ONCE at first sight — a prior goal is kept across
|
|
102
|
+
// progress deltas so it stays stable while checkpoints/checklist refresh.
|
|
103
|
+
goal: prior?.goal ?? computed.goal,
|
|
104
|
+
checkpoints: stampCheckpoints(computed.checkpoints, prior?.checkpoints, nowIso),
|
|
105
|
+
summaryChecklist: computed.checklist,
|
|
106
|
+
summaryState: 'ready',
|
|
107
|
+
};
|
|
108
|
+
writeSessionSummary({ id, ...stamp, summary: entry });
|
|
109
|
+
result.computed++;
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one model call behind the session summarizer (PHNX-3939).
|
|
3
|
+
*
|
|
4
|
+
* A pure boundary: given the session's first user turn (the goal source) and its
|
|
5
|
+
* live progress (todos / plan / phase off the state engine), it asks a local
|
|
6
|
+
* Anthropic-wire endpoint (Ollama / vLLM / LiteLLM — the same configurable base
|
|
7
|
+
* URL `computer/model.ts` speaks) for a strict JSON `{goal, checkpoints, checklist}`
|
|
8
|
+
* and validates the shape. On ANY failure — network, non-2xx, non-JSON, wrong
|
|
9
|
+
* shape — it returns `undefined`, and the caller records `summaryState: 'skipped'`.
|
|
10
|
+
*
|
|
11
|
+
* NEVER called on the request path: only the background SessionSummarizerService
|
|
12
|
+
* invokes it, debounced and reader-gated.
|
|
13
|
+
*/
|
|
14
|
+
import type { TodoProgress } from '../session/types.js';
|
|
15
|
+
/** Live progress fed to the model alongside the goal-bearing prompt. */
|
|
16
|
+
export interface SummarizeProgress {
|
|
17
|
+
/** Latest checklist write (TodoWrite / update_plan), when the session has one. */
|
|
18
|
+
todos?: TodoProgress;
|
|
19
|
+
/** Plan markdown from the last ExitPlanMode, when present. */
|
|
20
|
+
plan?: string;
|
|
21
|
+
/** Coarse lifecycle phase (running / waiting / idle / …), when known. */
|
|
22
|
+
phase?: string;
|
|
23
|
+
}
|
|
24
|
+
/** The validated model output. `at` timestamps are stamped by the caller. */
|
|
25
|
+
export interface SummarizeResult {
|
|
26
|
+
goal: string;
|
|
27
|
+
/** Progress checkpoints, newest last (short lines). */
|
|
28
|
+
checkpoints: string[];
|
|
29
|
+
/** Detailed checklist. */
|
|
30
|
+
checklist: {
|
|
31
|
+
text: string;
|
|
32
|
+
done: boolean;
|
|
33
|
+
}[];
|
|
34
|
+
}
|
|
35
|
+
export interface SummarizeOptions {
|
|
36
|
+
baseUrl: string;
|
|
37
|
+
model: string;
|
|
38
|
+
maxTokens?: number;
|
|
39
|
+
/**
|
|
40
|
+
* API key for the endpoint. Deliberately NOT resolved from `ANTHROPIC_API_KEY`:
|
|
41
|
+
* the summarizer targets an operator-configured local/remote endpoint
|
|
42
|
+
* (Ollama/vLLM/LiteLLM) that typically ignores the key, so forwarding the real
|
|
43
|
+
* Anthropic credential there would leak it. Only an explicit
|
|
44
|
+
* `AGENTS_SUMMARIZER_API_KEY` (or this option) is ever sent; otherwise the
|
|
45
|
+
* header is empty.
|
|
46
|
+
*/
|
|
47
|
+
apiKey?: string;
|
|
48
|
+
/** Injectable for tests; defaults to the global fetch. */
|
|
49
|
+
fetchImpl?: typeof fetch;
|
|
50
|
+
/** Abort the request when the daemon tick's deadline elapses. */
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}
|
|
53
|
+
/** Compose the user message from the goal-bearing prompt and the live progress. */
|
|
54
|
+
export declare function buildSummarizeUserMessage(prompt: string, progress: SummarizeProgress): string;
|
|
55
|
+
/**
|
|
56
|
+
* Coerce an untrusted parsed object into a {@link SummarizeResult}, or undefined
|
|
57
|
+
* when the shape is wrong. Extra/missing optional arrays degrade to `[]` rather
|
|
58
|
+
* than failing, but a non-string goal is a hard reject — a summary with no goal
|
|
59
|
+
* is not a summary.
|
|
60
|
+
*/
|
|
61
|
+
export declare function validateSummarizeResult(parsed: unknown): SummarizeResult | undefined;
|
|
62
|
+
/** Strip ``` fences and pull the first {...} block so a chatty model still parses. */
|
|
63
|
+
export declare function extractJsonObject(text: string): string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Run one summarization. Returns the validated result, or `undefined` on any
|
|
66
|
+
* failure (the caller then marks the session `summaryState: 'skipped'`).
|
|
67
|
+
*/
|
|
68
|
+
export declare function summarize(prompt: string, progress: SummarizeProgress, opts: SummarizeOptions): Promise<SummarizeResult | undefined>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one model call behind the session summarizer (PHNX-3939).
|
|
3
|
+
*
|
|
4
|
+
* A pure boundary: given the session's first user turn (the goal source) and its
|
|
5
|
+
* live progress (todos / plan / phase off the state engine), it asks a local
|
|
6
|
+
* Anthropic-wire endpoint (Ollama / vLLM / LiteLLM — the same configurable base
|
|
7
|
+
* URL `computer/model.ts` speaks) for a strict JSON `{goal, checkpoints, checklist}`
|
|
8
|
+
* and validates the shape. On ANY failure — network, non-2xx, non-JSON, wrong
|
|
9
|
+
* shape — it returns `undefined`, and the caller records `summaryState: 'skipped'`.
|
|
10
|
+
*
|
|
11
|
+
* NEVER called on the request path: only the background SessionSummarizerService
|
|
12
|
+
* invokes it, debounced and reader-gated.
|
|
13
|
+
*/
|
|
14
|
+
import { ANTHROPIC_VERSION } from '../computer/model.js';
|
|
15
|
+
const SYSTEM_PROMPT = [
|
|
16
|
+
'You summarize a coding-agent session for an operator dashboard.',
|
|
17
|
+
'Reply with ONLY a single JSON object, no prose and no code fences, of exactly this shape:',
|
|
18
|
+
'{"goal": string, "checkpoints": string[], "checklist": [{"text": string, "done": boolean}]}',
|
|
19
|
+
'- goal: 1-2 lines capturing what the user actually asked for, in plain language.',
|
|
20
|
+
'- checkpoints: short lines of concrete progress so far, newest last; [] if none yet.',
|
|
21
|
+
'- checklist: the detailed steps to finish the goal, each with a done flag; [] if unknown.',
|
|
22
|
+
'Do not invent progress that is not evidenced by the provided context.',
|
|
23
|
+
].join('\n');
|
|
24
|
+
/** Compose the user message from the goal-bearing prompt and the live progress. */
|
|
25
|
+
export function buildSummarizeUserMessage(prompt, progress) {
|
|
26
|
+
const parts = [`USER REQUEST:\n${prompt.trim()}`];
|
|
27
|
+
if (progress.phase)
|
|
28
|
+
parts.push(`PHASE: ${progress.phase}`);
|
|
29
|
+
if (progress.todos && progress.todos.items.length > 0) {
|
|
30
|
+
const lines = progress.todos.items.map((t) => `- [${t.status === 'completed' ? 'x' : ' '}] ${t.content}`);
|
|
31
|
+
parts.push(`CURRENT CHECKLIST (${progress.todos.done}/${progress.todos.total} done):\n${lines.join('\n')}`);
|
|
32
|
+
}
|
|
33
|
+
if (progress.plan)
|
|
34
|
+
parts.push(`PLAN:\n${progress.plan.trim().slice(0, 4000)}`);
|
|
35
|
+
return parts.join('\n\n');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Coerce an untrusted parsed object into a {@link SummarizeResult}, or undefined
|
|
39
|
+
* when the shape is wrong. Extra/missing optional arrays degrade to `[]` rather
|
|
40
|
+
* than failing, but a non-string goal is a hard reject — a summary with no goal
|
|
41
|
+
* is not a summary.
|
|
42
|
+
*/
|
|
43
|
+
export function validateSummarizeResult(parsed) {
|
|
44
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
45
|
+
return undefined;
|
|
46
|
+
const obj = parsed;
|
|
47
|
+
if (typeof obj.goal !== 'string' || obj.goal.trim().length === 0)
|
|
48
|
+
return undefined;
|
|
49
|
+
const checkpoints = Array.isArray(obj.checkpoints)
|
|
50
|
+
? obj.checkpoints.filter((c) => typeof c === 'string' && c.trim().length > 0).map((c) => c.trim())
|
|
51
|
+
: [];
|
|
52
|
+
const checklist = Array.isArray(obj.checklist)
|
|
53
|
+
? obj.checklist
|
|
54
|
+
.filter((c) => Boolean(c) && typeof c === 'object' && !Array.isArray(c))
|
|
55
|
+
.map((c) => ({ text: typeof c.text === 'string' ? c.text.trim() : '', done: Boolean(c.done) }))
|
|
56
|
+
.filter((c) => c.text.length > 0)
|
|
57
|
+
: [];
|
|
58
|
+
return { goal: obj.goal.trim(), checkpoints, checklist };
|
|
59
|
+
}
|
|
60
|
+
/** Extract the assistant text from an Anthropic Messages response body. */
|
|
61
|
+
function textFromBody(body) {
|
|
62
|
+
const content = body.content;
|
|
63
|
+
if (!Array.isArray(content))
|
|
64
|
+
return '';
|
|
65
|
+
return content
|
|
66
|
+
.filter((b) => Boolean(b) && typeof b === 'object')
|
|
67
|
+
.filter((b) => b.type === 'text' && typeof b.text === 'string')
|
|
68
|
+
.map((b) => b.text)
|
|
69
|
+
.join('');
|
|
70
|
+
}
|
|
71
|
+
/** Strip ``` fences and pull the first {...} block so a chatty model still parses. */
|
|
72
|
+
export function extractJsonObject(text) {
|
|
73
|
+
const unfenced = text.replace(/```(?:json)?/gi, '').trim();
|
|
74
|
+
const start = unfenced.indexOf('{');
|
|
75
|
+
const end = unfenced.lastIndexOf('}');
|
|
76
|
+
if (start === -1 || end === -1 || end < start)
|
|
77
|
+
return undefined;
|
|
78
|
+
return unfenced.slice(start, end + 1);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Run one summarization. Returns the validated result, or `undefined` on any
|
|
82
|
+
* failure (the caller then marks the session `summaryState: 'skipped'`).
|
|
83
|
+
*/
|
|
84
|
+
export async function summarize(prompt, progress, opts) {
|
|
85
|
+
if (!prompt.trim())
|
|
86
|
+
return undefined;
|
|
87
|
+
const baseUrl = opts.baseUrl.replace(/\/+$/, '');
|
|
88
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
89
|
+
// Only an explicit summarizer key is ever forwarded — never the ambient
|
|
90
|
+
// ANTHROPIC_API_KEY, which would leak to the operator's local endpoint.
|
|
91
|
+
const apiKey = opts.apiKey ?? process.env.AGENTS_SUMMARIZER_API_KEY ?? '';
|
|
92
|
+
try {
|
|
93
|
+
const res = await doFetch(`${baseUrl}/v1/messages`, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: {
|
|
96
|
+
'content-type': 'application/json',
|
|
97
|
+
'x-api-key': apiKey,
|
|
98
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
model: opts.model,
|
|
102
|
+
max_tokens: opts.maxTokens ?? 512,
|
|
103
|
+
system: SYSTEM_PROMPT,
|
|
104
|
+
messages: [{ role: 'user', content: buildSummarizeUserMessage(prompt, progress) }],
|
|
105
|
+
}),
|
|
106
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok)
|
|
109
|
+
return undefined;
|
|
110
|
+
const body = await res.json();
|
|
111
|
+
const text = textFromBody(body);
|
|
112
|
+
const json = extractJsonObject(text);
|
|
113
|
+
if (!json)
|
|
114
|
+
return undefined;
|
|
115
|
+
return validateSummarizeResult(JSON.parse(json));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -569,9 +569,10 @@ export declare class AgentManager {
|
|
|
569
569
|
private resolveScheduledPlacement;
|
|
570
570
|
/**
|
|
571
571
|
* Place an UNPINNED, non-cloud teammate onto the team pool via the cascade
|
|
572
|
-
* (least-loaded)
|
|
573
|
-
*
|
|
574
|
-
*
|
|
572
|
+
* (least-loaded). A poolless team consumes the active worker allowlist and
|
|
573
|
+
* fails loud when none exists; it never silently lands on the orchestrator.
|
|
574
|
+
* A no-op only for a pinned teammate (hostName already set from `--device`) or
|
|
575
|
+
* a cloud teammate. Shared by spawn()
|
|
575
576
|
* (immediate add-launch) and startReady() (staged launch) so an unpinned pool
|
|
576
577
|
* teammate schedules identically no matter how it was fired.
|
|
577
578
|
*/
|
package/dist/lib/teams/agents.js
CHANGED
|
@@ -38,6 +38,7 @@ import { atomicWriteJsonSync } from '../fs-atomic.js';
|
|
|
38
38
|
import { resolvePlacement, classifyExclusions, isTransientPlacementBlock, NoViableDeviceError, } from './scheduler.js';
|
|
39
39
|
import { probePoolSignals } from './placement-probe.js';
|
|
40
40
|
import { readMaxConcurrentCaps } from '../device-config.js';
|
|
41
|
+
import { filterAutoPool, listWorkerDevices } from '../devices/pool.js';
|
|
41
42
|
import { redactSecrets, sanitizeForTerminal } from '../redact.js';
|
|
42
43
|
import chalk from 'chalk';
|
|
43
44
|
let lastMemoryWarnAt = 0;
|
|
@@ -2328,9 +2329,10 @@ export class AgentManager {
|
|
|
2328
2329
|
}
|
|
2329
2330
|
/**
|
|
2330
2331
|
* Place an UNPINNED, non-cloud teammate onto the team pool via the cascade
|
|
2331
|
-
* (least-loaded)
|
|
2332
|
-
*
|
|
2333
|
-
*
|
|
2332
|
+
* (least-loaded). A poolless team consumes the active worker allowlist and
|
|
2333
|
+
* fails loud when none exists; it never silently lands on the orchestrator.
|
|
2334
|
+
* A no-op only for a pinned teammate (hostName already set from `--device`) or
|
|
2335
|
+
* a cloud teammate. Shared by spawn()
|
|
2334
2336
|
* (immediate add-launch) and startReady() (staged launch) so an unpinned pool
|
|
2335
2337
|
* teammate schedules identically no matter how it was fired.
|
|
2336
2338
|
*/
|
|
@@ -2341,7 +2343,12 @@ export class AgentManager {
|
|
|
2341
2343
|
if (!teamMeta)
|
|
2342
2344
|
return;
|
|
2343
2345
|
const roster = await this.listByTask(taskName);
|
|
2344
|
-
|
|
2346
|
+
// A poolless team is still an automatic placement request: use the same
|
|
2347
|
+
// explicit worker allowlist as `--device auto`. Never silently run it on a
|
|
2348
|
+
// personal/desktop orchestrator merely because `devices` was omitted.
|
|
2349
|
+
const pool = teamMeta.devices?.length
|
|
2350
|
+
? teamMeta.devices
|
|
2351
|
+
: filterAutoPool(listWorkerDevices());
|
|
2345
2352
|
const maxConcurrent = pool.length > 1 ? readMaxConcurrentCaps(pool) : undefined;
|
|
2346
2353
|
// On the start path (opts.probe), gather live signals so the pick is health-,
|
|
2347
2354
|
// harness-, and load-aware (RUSH-2002); the add path stays the cap-only
|
|
@@ -2353,6 +2360,7 @@ export class AgentManager {
|
|
|
2353
2360
|
const placeOpts = {
|
|
2354
2361
|
maxConcurrent,
|
|
2355
2362
|
signals,
|
|
2363
|
+
defaultDevices: pool,
|
|
2356
2364
|
agentLabel: this.placementAgentLabel(agent),
|
|
2357
2365
|
};
|
|
2358
2366
|
if (signals) {
|
|
@@ -45,6 +45,8 @@ export interface PlacementTeam {
|
|
|
45
45
|
* is never second-guessed.
|
|
46
46
|
*/
|
|
47
47
|
export interface PlacementOptions {
|
|
48
|
+
/** Worker pool used when the team does not declare an explicit device pool. */
|
|
49
|
+
defaultDevices?: string[];
|
|
48
50
|
maxConcurrent?: Record<string, number>;
|
|
49
51
|
/**
|
|
50
52
|
* Live per-device signals (RUSH-2002). When present, the many-device pick
|
|
@@ -144,8 +146,8 @@ export declare function classifyExclusions(devices: string[], roster: RosterEntr
|
|
|
144
146
|
*/
|
|
145
147
|
export declare function pickBestDevice(devices: string[], roster: RosterEntry[], opts?: PlacementOptions): string;
|
|
146
148
|
/**
|
|
147
|
-
* Resolve where a teammate runs. Returns `{ device: null }`
|
|
148
|
-
*
|
|
149
|
+
* Resolve where a teammate runs. Returns `{ device: null }` only when the
|
|
150
|
+
* explicitly/default-selected device is the local machine, and
|
|
149
151
|
* `{ device: <name> }` for a remote placement. See the cascade in the module
|
|
150
152
|
* header. Throws {@link NoViableDeviceError} when a required pool has no viable
|
|
151
153
|
* host — the caller surfaces it at `teams start` rather than falling back local.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 1. teammate has an explicit `--device` pin → that device
|
|
10
10
|
* 2. else the team pool has exactly one device → that device (whole team)
|
|
11
11
|
* 3. else the team pool has many devices → best-viable pick
|
|
12
|
-
* 4. else (no pin, no pool) →
|
|
12
|
+
* 4. else (no pin, no pool) → active worker pool
|
|
13
13
|
*
|
|
14
14
|
* Step 3 is cap-, health-, and harness-aware (RUSH-2002). When the caller
|
|
15
15
|
* supplies live {@link DevicePlacementSignal}s (probed reachability, headroom,
|
|
@@ -294,8 +294,8 @@ export function pickBestDevice(devices, roster, opts) {
|
|
|
294
294
|
})[0];
|
|
295
295
|
}
|
|
296
296
|
/**
|
|
297
|
-
* Resolve where a teammate runs. Returns `{ device: null }`
|
|
298
|
-
*
|
|
297
|
+
* Resolve where a teammate runs. Returns `{ device: null }` only when the
|
|
298
|
+
* explicitly/default-selected device is the local machine, and
|
|
299
299
|
* `{ device: <name> }` for a remote placement. See the cascade in the module
|
|
300
300
|
* header. Throws {@link NoViableDeviceError} when a required pool has no viable
|
|
301
301
|
* host — the caller surfaces it at `teams start` rather than falling back local.
|
|
@@ -306,10 +306,10 @@ export function resolvePlacement(team, explicitDevice, roster, opts) {
|
|
|
306
306
|
if (explicitDevice) {
|
|
307
307
|
return { device: isLocalDevice(explicitDevice) ? null : explicitDevice };
|
|
308
308
|
}
|
|
309
|
-
const pool = team.devices ?? [];
|
|
310
|
-
//
|
|
309
|
+
const pool = team.devices?.length ? team.devices : (opts?.defaultDevices ?? []);
|
|
310
|
+
// An omitted pool means automatic worker placement, never implicit local.
|
|
311
311
|
if (pool.length === 0)
|
|
312
|
-
|
|
312
|
+
throw new NoViableDeviceError([], opts?.agentLabel);
|
|
313
313
|
// 2. Pool of one → the whole team runs there. Respect the choice for
|
|
314
314
|
// load/cap/reachability, but still fail loud when the agent is provably
|
|
315
315
|
// NOT installed there — running it would be futile.
|
|
@@ -25,6 +25,8 @@ export declare const AGENTS_TMUX_HISTORY_LIMIT = 20000;
|
|
|
25
25
|
* these defaults, silently. Same shape as AGENT_HOOK_SCHEMA below.
|
|
26
26
|
*/
|
|
27
27
|
export declare const AGENTS_TMUX_CONFIG_SCHEMA = 1;
|
|
28
|
+
/** Source one user config without tmux's quiet flag, preserving path bytes. */
|
|
29
|
+
export declare function userConfigSourceLine(value: string): string;
|
|
28
30
|
/** Provenance written alongside each live tmux session. */
|
|
29
31
|
export interface SessionMeta {
|
|
30
32
|
name: string;
|
package/dist/lib/tmux/session.js
CHANGED
|
@@ -55,6 +55,10 @@ let startupConfigSequence = 0;
|
|
|
55
55
|
function tmuxConfigArgument(value) {
|
|
56
56
|
return `"${value.replace(/([\\"$])/g, '\\$1')}"`;
|
|
57
57
|
}
|
|
58
|
+
/** Source one user config without tmux's quiet flag, preserving path bytes. */
|
|
59
|
+
export function userConfigSourceLine(value) {
|
|
60
|
+
return `source-file ${tmuxConfigArgument(value)}`;
|
|
61
|
+
}
|
|
58
62
|
/**
|
|
59
63
|
* tmux loads `-f` instead of its normal user config, so put agents-cli's
|
|
60
64
|
* defaults first and explicitly source the first user config tmux would have
|
|
@@ -90,7 +94,9 @@ function writeStartupConfig(env) {
|
|
|
90
94
|
'bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-selection-no-clear',
|
|
91
95
|
];
|
|
92
96
|
for (const userConfig of userConfigs) {
|
|
93
|
-
|
|
97
|
+
// Do not suppress parse/read failures: a broken user config must fail the
|
|
98
|
+
// launch visibly instead of producing a silently half-configured server.
|
|
99
|
+
lines.push(userConfigSourceLine(userConfig));
|
|
94
100
|
}
|
|
95
101
|
fs.writeFileSync(startupConfig, `${lines.join('\n')}\n`, { mode: 0o600 });
|
|
96
102
|
return startupConfig;
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -1204,6 +1204,26 @@ export interface BrowserProfileConfig {
|
|
|
1204
1204
|
}>;
|
|
1205
1205
|
/** Preset name to use when `--endpoint` is not passed to `start`. */
|
|
1206
1206
|
defaultEndpoint?: string;
|
|
1207
|
+
/**
|
|
1208
|
+
* How `agents browser` obtains a live browser for this profile (PHNX-3967):
|
|
1209
|
+
* - `launch` (default when absent): agents-cli spawns the browser itself
|
|
1210
|
+
* under a managed `--user-data-dir` when nothing is serving CDP on the port.
|
|
1211
|
+
* - `attach-only`: agents-cli NEVER spawns a rival window. It attaches to a
|
|
1212
|
+
* browser the user (or a one-time command) already started with remote
|
|
1213
|
+
* debugging, and fails loud with a relaunch hint when none is there. This
|
|
1214
|
+
* is how "enforce one window" is expressed — Arc is inherently attach-only,
|
|
1215
|
+
* and a canonical signed-in Comet uses it so agents can't spawn a second,
|
|
1216
|
+
* logged-out instance. Pairs with a durable {@link userDataDir}.
|
|
1217
|
+
*/
|
|
1218
|
+
launchPolicy?: 'attach-only' | 'launch';
|
|
1219
|
+
/**
|
|
1220
|
+
* Absolute durable `--user-data-dir` for this profile's browser (PHNX-3967).
|
|
1221
|
+
* When absent, an attach-only profile resolves a default durable dir outside
|
|
1222
|
+
* `~/.agents/.cache` (`getBrowserDurableDir()`), so a one-time sign-in survives
|
|
1223
|
+
* quit+relaunch and `profiles remove`'s cache sweep. Also the value the
|
|
1224
|
+
* ownership guard compares a running instance against to reject a port-squatter.
|
|
1225
|
+
*/
|
|
1226
|
+
userDataDir?: string;
|
|
1207
1227
|
chrome?: {
|
|
1208
1228
|
headless?: boolean;
|
|
1209
1229
|
args?: string[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* The ONE place that defines the CLI's standard short-form verb aliases.
|
|
4
|
+
*
|
|
5
|
+
* These are the canonical short forms an agent or user reaches for on a
|
|
6
|
+
* command group's CRUD verbs — `ls` for `list`, `rm` for `remove`, and so on.
|
|
7
|
+
* They are NOT cross-verb synonyms (`info`/`install`/`create`); those are
|
|
8
|
+
* per-command back-compat aliases owned by the individual commands.
|
|
9
|
+
*
|
|
10
|
+
* Apply them with {@link withAliases} so every group stays in lockstep instead
|
|
11
|
+
* of each command hand-rolling its own `.alias('rm')`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const CANONICAL_ALIASES: {
|
|
14
|
+
readonly list: readonly ["ls"];
|
|
15
|
+
readonly view: readonly ["show"];
|
|
16
|
+
readonly add: readonly [];
|
|
17
|
+
readonly remove: readonly ["rm"];
|
|
18
|
+
readonly rename: readonly ["mv"];
|
|
19
|
+
readonly edit: readonly [];
|
|
20
|
+
};
|
|
21
|
+
export type CanonicalVerb = keyof typeof CANONICAL_ALIASES;
|
|
22
|
+
/** Apply the standard aliases for `verb` to an already-created subcommand, returning it. */
|
|
23
|
+
export declare function withAliases(cmd: Command, verb: CanonicalVerb): Command;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE place that defines the CLI's standard short-form verb aliases.
|
|
3
|
+
*
|
|
4
|
+
* These are the canonical short forms an agent or user reaches for on a
|
|
5
|
+
* command group's CRUD verbs — `ls` for `list`, `rm` for `remove`, and so on.
|
|
6
|
+
* They are NOT cross-verb synonyms (`info`/`install`/`create`); those are
|
|
7
|
+
* per-command back-compat aliases owned by the individual commands.
|
|
8
|
+
*
|
|
9
|
+
* Apply them with {@link withAliases} so every group stays in lockstep instead
|
|
10
|
+
* of each command hand-rolling its own `.alias('rm')`.
|
|
11
|
+
*/
|
|
12
|
+
export const CANONICAL_ALIASES = {
|
|
13
|
+
list: ['ls'],
|
|
14
|
+
view: ['show'],
|
|
15
|
+
add: [],
|
|
16
|
+
remove: ['rm'],
|
|
17
|
+
rename: ['mv'],
|
|
18
|
+
edit: [],
|
|
19
|
+
};
|
|
20
|
+
/** Apply the standard aliases for `verb` to an already-created subcommand, returning it. */
|
|
21
|
+
export function withAliases(cmd, verb) {
|
|
22
|
+
const aliases = CANONICAL_ALIASES[verb];
|
|
23
|
+
return aliases.length ? cmd.aliases([...aliases]) : cmd;
|
|
24
|
+
}
|