@phnx-labs/agents-cli 1.20.32 → 1.20.33
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/dist/commands/commands.js +3 -3
- package/dist/commands/computer-actions.js +1 -0
- package/dist/commands/cost.js +2 -2
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/exec.js +56 -1
- package/dist/commands/hooks.js +3 -3
- package/dist/commands/inspect.js +13 -17
- package/dist/commands/mcp.js +3 -3
- package/dist/commands/permissions.js +3 -3
- package/dist/commands/rules.js +2 -2
- package/dist/commands/sessions.js +18 -1
- package/dist/commands/skills.js +3 -3
- package/dist/commands/sync.js +2 -2
- package/dist/commands/teams.js +7 -12
- package/dist/commands/usage.js +2 -2
- package/dist/commands/utils.d.ts +8 -0
- package/dist/commands/utils.js +20 -0
- package/dist/commands/versions.js +2 -2
- package/dist/commands/view.js +23 -9
- package/dist/commands/workflows.js +3 -3
- package/dist/index.js +12 -0
- package/dist/lib/agent-spec/index.d.ts +18 -0
- package/dist/lib/agent-spec/index.js +35 -0
- package/dist/lib/agent-spec/primitives.d.ts +28 -0
- package/dist/lib/agent-spec/primitives.js +57 -0
- package/dist/lib/agent-spec/provider.d.ts +2 -0
- package/dist/lib/agent-spec/provider.js +9 -0
- package/dist/lib/agent-spec/resolve.d.ts +33 -0
- package/dist/lib/agent-spec/resolve.js +174 -0
- package/dist/lib/agent-spec/types.d.ts +57 -0
- package/dist/lib/agent-spec/types.js +18 -0
- package/dist/lib/crabbox/cli.d.ts +98 -0
- package/dist/lib/crabbox/cli.js +218 -0
- package/dist/lib/crabbox/lease.d.ts +41 -0
- package/dist/lib/crabbox/lease.js +73 -0
- package/dist/lib/crabbox/runtimes.d.ts +57 -0
- package/dist/lib/crabbox/runtimes.js +109 -0
- package/dist/lib/hosts/dispatch.d.ts +27 -10
- package/dist/lib/hosts/dispatch.js +55 -19
- package/dist/lib/hosts/option.d.ts +14 -0
- package/dist/lib/hosts/option.js +19 -0
- package/dist/lib/hosts/passthrough.d.ts +30 -0
- package/dist/lib/hosts/passthrough.js +141 -0
- package/dist/lib/hosts/remote-cmd.d.ts +36 -0
- package/dist/lib/hosts/remote-cmd.js +56 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/secrets/bundles.js +29 -20
- package/dist/lib/secrets/index.d.ts +11 -0
- package/dist/lib/secrets/index.js +18 -1
- package/dist/lib/secrets/linux.d.ts +14 -0
- package/dist/lib/secrets/linux.js +21 -0
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +18 -1
- package/dist/lib/session/provenance.d.ts +56 -0
- package/dist/lib/session/provenance.js +157 -0
- package/dist/lib/ssh-exec.d.ts +22 -0
- package/dist/lib/ssh-exec.js +59 -2
- package/dist/lib/ssh-tunnel.d.ts +0 -5
- package/dist/lib/ssh-tunnel.js +65 -8
- package/dist/lib/versions.d.ts +2 -4
- package/dist/lib/versions.js +7 -20
- package/package.json +2 -1
- package/dist/lib/agent-spec.d.ts +0 -36
- package/dist/lib/agent-spec.js +0 -157
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Public API for the agent-spec engine. Commands import from here and get the
|
|
2
|
+
// production provider bound automatically; tests import the pure `./resolve.js`
|
|
3
|
+
// core directly and inject a fake provider.
|
|
4
|
+
//
|
|
5
|
+
// One vocabulary, one resolver, reused by every subcommand that accepts
|
|
6
|
+
// `<agent>[@<qualifier>]`. Built for the hot path: exact / @pinned / bare specs
|
|
7
|
+
// resolve with no directory enumeration; only @latest/@oldest/@all enumerate,
|
|
8
|
+
// via the mtime-cached provider.
|
|
9
|
+
import { defaultVersionProvider } from './provider.js';
|
|
10
|
+
import * as core from './resolve.js';
|
|
11
|
+
export * from './types.js';
|
|
12
|
+
export * from './primitives.js';
|
|
13
|
+
/** Shared `--help` epilog so every agent-spec command documents the same grammar. */
|
|
14
|
+
export const AGENT_SPEC_HELP = 'Agent spec: <agent>[@<qualifier>]. Qualifiers: ' +
|
|
15
|
+
'@latest (highest installed), @oldest (lowest installed), ' +
|
|
16
|
+
'@pinned / @default (your configured default — synonyms), ' +
|
|
17
|
+
'@all (every installed version), or an exact @x.y.z. ' +
|
|
18
|
+
'Bare <agent> uses the resolved default (project pin → global default). ' +
|
|
19
|
+
'Comma-separate to combine: claude@all,codex@latest.';
|
|
20
|
+
/** Resolve a spec (single or comma-list) into concrete installed targets. */
|
|
21
|
+
export function resolveAgentTargets(spec, opts = {}) {
|
|
22
|
+
return core.resolveAgentTargets(spec, defaultVersionProvider, opts);
|
|
23
|
+
}
|
|
24
|
+
/** Resolve a spec that must name exactly one installed version. */
|
|
25
|
+
export function resolveSingleAgentTarget(spec, opts = {}) {
|
|
26
|
+
return core.resolveSingleAgentTarget(spec, defaultVersionProvider, opts);
|
|
27
|
+
}
|
|
28
|
+
/** Resolve a read/list command's version filter (undefined → all, @default → the default, else concrete). */
|
|
29
|
+
export function resolveVersionFilter(agent, qualifier, opts = {}) {
|
|
30
|
+
return core.resolveVersionFilter(agent, qualifier, defaultVersionProvider, opts);
|
|
31
|
+
}
|
|
32
|
+
/** Concrete version filter for list/display commands (undefined → show all, @default → the default version). */
|
|
33
|
+
export function resolveListFilter(agent, qualifier, opts = {}) {
|
|
34
|
+
return core.resolveListFilter(agent, qualifier, defaultVersionProvider, opts);
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only shape a version string may take before it reaches an exec/shim/path
|
|
3
|
+
* boundary. Accepts the literal `latest` or a 1–64 char run of `[A-Za-z0-9._+-]`
|
|
4
|
+
* with no `..` traversal. This is THE validation gate — every resolver funnels
|
|
5
|
+
* exact-version tokens through it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const VERSION_RE: RegExp;
|
|
8
|
+
/** Canonical qualifier set, in help/display order. `pinned` ≡ `default`. */
|
|
9
|
+
export declare const AGENT_QUALIFIERS: readonly ["latest", "oldest", "pinned", "default", "all"];
|
|
10
|
+
export type AgentQualifier = (typeof AGENT_QUALIFIERS)[number];
|
|
11
|
+
/**
|
|
12
|
+
* Record-filter-only qualifiers. `any` means "no version constraint" for
|
|
13
|
+
* historical-record queries (sessions/teams resume) — accepted by the filter
|
|
14
|
+
* path but intentionally kept out of the display vocabulary.
|
|
15
|
+
*/
|
|
16
|
+
export declare const RECORD_ONLY_QUALIFIERS: readonly ["any"];
|
|
17
|
+
/**
|
|
18
|
+
* Ordering for version strings (ascending).
|
|
19
|
+
* 1. numeric segment comparison (semver-ish: `2.1.187` > `2.1.143`)
|
|
20
|
+
* 2. tie → trailing `-N` build suffix, numerically (`2026.2.19-2` > `2026.2.19`)
|
|
21
|
+
* 3. still tied → 0 (legacy behavior: `1.0` == `1.0.0`, non-numeric tails == 0)
|
|
22
|
+
*
|
|
23
|
+
* Deliberately NOT a full semver comparator: OpenClaw's `-N` is a rebuild marker
|
|
24
|
+
* (higher = newer); a semver comparator would invert it. The `-N` tiebreak is the
|
|
25
|
+
* only addition over the historical numeric-only compare, so suffix-free versions
|
|
26
|
+
* (claude/codex semver) are unaffected.
|
|
27
|
+
*/
|
|
28
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Pure version primitives for the agent-spec engine — zero dependencies, so the
|
|
2
|
+
// whole module (and versions.ts, which re-uses these) stays trivially testable.
|
|
3
|
+
/**
|
|
4
|
+
* The only shape a version string may take before it reaches an exec/shim/path
|
|
5
|
+
* boundary. Accepts the literal `latest` or a 1–64 char run of `[A-Za-z0-9._+-]`
|
|
6
|
+
* with no `..` traversal. This is THE validation gate — every resolver funnels
|
|
7
|
+
* exact-version tokens through it.
|
|
8
|
+
*/
|
|
9
|
+
export const VERSION_RE = /^(?:latest|(?!.*\.\.)[A-Za-z0-9._+-]{1,64})$/;
|
|
10
|
+
/** Canonical qualifier set, in help/display order. `pinned` ≡ `default`. */
|
|
11
|
+
export const AGENT_QUALIFIERS = ['latest', 'oldest', 'pinned', 'default', 'all'];
|
|
12
|
+
/**
|
|
13
|
+
* Record-filter-only qualifiers. `any` means "no version constraint" for
|
|
14
|
+
* historical-record queries (sessions/teams resume) — accepted by the filter
|
|
15
|
+
* path but intentionally kept out of the display vocabulary.
|
|
16
|
+
*/
|
|
17
|
+
export const RECORD_ONLY_QUALIFIERS = ['any'];
|
|
18
|
+
/** Split a version into numeric `.`-segments (non-numeric tail → 0), e.g. `2026.2.19-2` → [2026,2,19]. */
|
|
19
|
+
function numericParts(v) {
|
|
20
|
+
return v.split('.').map((n) => parseInt(n, 10) || 0);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Trailing `-<digits>` build suffix as a number (0 when absent). OpenClaw ships
|
|
24
|
+
* same-day rebuilds as `2026.2.19-2` where a higher `-N` is NEWER — the opposite
|
|
25
|
+
* of a semver pre-release. Used only to break exact numeric ties, so semver-style
|
|
26
|
+
* versions (which carry no `-N`) are unaffected.
|
|
27
|
+
*/
|
|
28
|
+
function buildSuffix(v) {
|
|
29
|
+
const m = /-(\d+)$/.exec(v);
|
|
30
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Ordering for version strings (ascending).
|
|
34
|
+
* 1. numeric segment comparison (semver-ish: `2.1.187` > `2.1.143`)
|
|
35
|
+
* 2. tie → trailing `-N` build suffix, numerically (`2026.2.19-2` > `2026.2.19`)
|
|
36
|
+
* 3. still tied → 0 (legacy behavior: `1.0` == `1.0.0`, non-numeric tails == 0)
|
|
37
|
+
*
|
|
38
|
+
* Deliberately NOT a full semver comparator: OpenClaw's `-N` is a rebuild marker
|
|
39
|
+
* (higher = newer); a semver comparator would invert it. The `-N` tiebreak is the
|
|
40
|
+
* only addition over the historical numeric-only compare, so suffix-free versions
|
|
41
|
+
* (claude/codex semver) are unaffected.
|
|
42
|
+
*/
|
|
43
|
+
export function compareVersions(a, b) {
|
|
44
|
+
const na = numericParts(a);
|
|
45
|
+
const nb = numericParts(b);
|
|
46
|
+
for (let i = 0; i < Math.max(na.length, nb.length); i++) {
|
|
47
|
+
const av = na[i] || 0;
|
|
48
|
+
const bv = nb[i] || 0;
|
|
49
|
+
if (av !== bv)
|
|
50
|
+
return av - bv;
|
|
51
|
+
}
|
|
52
|
+
const sa = buildSuffix(a);
|
|
53
|
+
const sb = buildSuffix(b);
|
|
54
|
+
if (sa !== sb)
|
|
55
|
+
return sa - sb;
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The production VersionProvider: the only fs/meta-coupled file in the engine.
|
|
2
|
+
// Everything else in this module is pure and testable without it.
|
|
3
|
+
import { listInstalledVersions, getGlobalDefault, getProjectVersion, isVersionInstalled, } from '../versions.js';
|
|
4
|
+
export const defaultVersionProvider = {
|
|
5
|
+
listInstalled: (agent) => listInstalledVersions(agent),
|
|
6
|
+
getProjectVersion: (agent, cwd) => getProjectVersion(agent, cwd),
|
|
7
|
+
getGlobalDefault: (agent) => getGlobalDefault(agent),
|
|
8
|
+
isInstalled: (agent, version) => isVersionInstalled(agent, version),
|
|
9
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AgentId } from '../types.js';
|
|
2
|
+
import { type AgentTarget, type VersionProvider, type VersionSource, type ResolveOptions, type VersionFilter } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve an agent spec (single or comma-list) into concrete installed targets.
|
|
5
|
+
* `@latest`/`@oldest`/`@all` range over installed versions.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveAgentTargets(spec: string, provider: VersionProvider, opts?: ResolveOptions): AgentTarget[];
|
|
8
|
+
/**
|
|
9
|
+
* Single-target commands (`run`, `sync`, `inspect`): resolve a spec that must
|
|
10
|
+
* name exactly one installed version. Rejects `@all` / multi-target specs.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveSingleAgentTarget(spec: string, provider: VersionProvider, opts?: ResolveOptions): {
|
|
13
|
+
agent: AgentId;
|
|
14
|
+
version: string;
|
|
15
|
+
source: VersionSource;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Read/list commands: resolve a qualifier into a version filter.
|
|
19
|
+
* undefined / '' / @any → no filter (show all installed versions)
|
|
20
|
+
* @default / @pinned → the literal 'default' sentinel (show the configured default)
|
|
21
|
+
* @latest/@oldest/x.y.z → a concrete version (throws if not installed)
|
|
22
|
+
* Uniform `@default` handling fixes the prior rules-vs-view inconsistency.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveVersionFilter(agent: AgentId, qualifier: string | undefined | null, provider: VersionProvider, opts?: ResolveOptions): VersionFilter;
|
|
25
|
+
/**
|
|
26
|
+
* Concrete version filter for list/display commands whose downstream code
|
|
27
|
+
* filters by an exact version string (not the `'default'` sentinel `view` uses).
|
|
28
|
+
* undefined / '' / @any → undefined (no filter → show all installed)
|
|
29
|
+
* @default / @pinned → the configured default version, or undefined if none
|
|
30
|
+
* is set (falls back to show-all rather than erroring)
|
|
31
|
+
* @latest/@oldest/x.y.z → a concrete version (throws AgentSpecError if bad)
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveListFilter(agent: AgentId, qualifier: string | undefined | null, provider: VersionProvider, opts?: ResolveOptions): string | undefined;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Pure agent-spec resolution — the engine core. Takes a VersionProvider instead
|
|
2
|
+
// of touching the filesystem, so every branch is unit-testable with in-memory
|
|
3
|
+
// fixtures. Domain = installed versions (`add`/install use a separate npm path).
|
|
4
|
+
// Never calls process.exit; throws AgentSpecError on bad input.
|
|
5
|
+
import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError } from '../agents.js';
|
|
6
|
+
import { VERSION_RE } from './primitives.js';
|
|
7
|
+
import { AgentSpecError, } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Resolve an agent spec (single or comma-list) into concrete installed targets.
|
|
10
|
+
* `@latest`/`@oldest`/`@all` range over installed versions.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveAgentTargets(spec, provider, opts = {}) {
|
|
13
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
14
|
+
const available = opts.availableAgents ?? ALL_AGENT_IDS;
|
|
15
|
+
const onAmbiguous = opts.onAmbiguous ?? 'error';
|
|
16
|
+
const rawEntries = spec.split(',').map((s) => s.trim()).filter(Boolean);
|
|
17
|
+
if (rawEntries.length === 0) {
|
|
18
|
+
throw new AgentSpecError('Empty agent spec.', 'empty');
|
|
19
|
+
}
|
|
20
|
+
// Expand the bare literal `all` (or `all@all`) into every available agent that
|
|
21
|
+
// has ≥1 installed version. Lenient: agents with nothing installed are skipped.
|
|
22
|
+
const entries = [];
|
|
23
|
+
for (const e of rawEntries) {
|
|
24
|
+
if (e === 'all' || e === 'all@all') {
|
|
25
|
+
for (const a of available) {
|
|
26
|
+
if (provider.listInstalled(a).length > 0)
|
|
27
|
+
entries.push(`${a}@all`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
entries.push(e);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const out = [];
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
const push = (agent, version, source) => {
|
|
37
|
+
const key = `${agent}@${version ?? ''}`;
|
|
38
|
+
if (!seen.has(key)) {
|
|
39
|
+
seen.add(key);
|
|
40
|
+
out.push({ agent, version, source });
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const at = entry.indexOf('@');
|
|
45
|
+
const agentToken = (at === -1 ? entry : entry.slice(0, at)).trim();
|
|
46
|
+
const qualifier = at === -1 ? null : entry.slice(at + 1).trim();
|
|
47
|
+
if (!agentToken)
|
|
48
|
+
continue;
|
|
49
|
+
if (at !== -1 && !qualifier) {
|
|
50
|
+
throw new AgentSpecError(`Missing version in '${entry}'. Use ${agentToken}@x.y.z, @latest, @oldest, @pinned, @default, or @all.`, 'missing-version');
|
|
51
|
+
}
|
|
52
|
+
const agent = resolveAgentName(agentToken);
|
|
53
|
+
if (!agent || !available.includes(agent)) {
|
|
54
|
+
throw new AgentSpecError(formatAgentError(agentToken, [...available]), 'unknown-agent');
|
|
55
|
+
}
|
|
56
|
+
const name = AGENTS[agent].name;
|
|
57
|
+
// ----- bare: project pin → global default → sole/ambiguous installed -----
|
|
58
|
+
if (qualifier === null) {
|
|
59
|
+
const proj = provider.getProjectVersion(agent, cwd);
|
|
60
|
+
if (proj) {
|
|
61
|
+
push(agent, proj, 'project-pin');
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const glob = provider.getGlobalDefault(agent);
|
|
65
|
+
if (glob) {
|
|
66
|
+
push(agent, glob, 'global-default');
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const installed = provider.listInstalled(agent);
|
|
70
|
+
if (installed.length === 0) {
|
|
71
|
+
push(agent, null, 'none');
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (installed.length === 1) {
|
|
75
|
+
push(agent, installed[0], 'sole-installed');
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (onAmbiguous === 'newest') {
|
|
79
|
+
push(agent, installed[installed.length - 1], 'newest-installed');
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
throw new AgentSpecError(`No default version set for ${name}. Specify one (${agent}@<version>) or set it: agents use ${agent}@<version>.`, 'no-default', agent, installed);
|
|
83
|
+
}
|
|
84
|
+
// ----- @pinned / @default: the configured global default -----
|
|
85
|
+
if (qualifier === 'pinned' || qualifier === 'default') {
|
|
86
|
+
const def = provider.getGlobalDefault(agent);
|
|
87
|
+
if (!def) {
|
|
88
|
+
throw new AgentSpecError(`No default version set for ${name}. Run: agents use ${agent}@<version>`, 'no-default', agent, provider.listInstalled(agent));
|
|
89
|
+
}
|
|
90
|
+
push(agent, def, 'global-default(@pinned)');
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// ----- @all: every installed version -----
|
|
94
|
+
if (qualifier === 'all') {
|
|
95
|
+
const installed = provider.listInstalled(agent);
|
|
96
|
+
if (installed.length === 0) {
|
|
97
|
+
throw new AgentSpecError(`No managed versions are installed for ${name}. Run: agents add ${agent}@latest`, 'none-installed', agent);
|
|
98
|
+
}
|
|
99
|
+
for (const v of installed)
|
|
100
|
+
push(agent, v, 'explicit');
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// ----- @latest / @oldest: ends of the installed range -----
|
|
104
|
+
if (qualifier === 'latest' || qualifier === 'oldest') {
|
|
105
|
+
const installed = provider.listInstalled(agent);
|
|
106
|
+
if (installed.length === 0) {
|
|
107
|
+
throw new AgentSpecError(`No managed versions are installed for ${name}. Run: agents add ${agent}@latest`, 'none-installed', agent);
|
|
108
|
+
}
|
|
109
|
+
const isOldest = qualifier === 'oldest';
|
|
110
|
+
push(agent, isOldest ? installed[0] : installed[installed.length - 1], isOldest ? 'alias-oldest' : 'alias-latest');
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// ----- exact version: validate then existence-check (no enumeration) -----
|
|
114
|
+
if (!VERSION_RE.test(qualifier)) {
|
|
115
|
+
throw new AgentSpecError(`Invalid version '${qualifier}' for ${name}. Allowed: latest or [A-Za-z0-9._+-]{1,64}.`, 'invalid-version', agent);
|
|
116
|
+
}
|
|
117
|
+
if (!provider.isInstalled(agent, qualifier)) {
|
|
118
|
+
const installed = provider.listInstalled(agent);
|
|
119
|
+
const hint = installed.length ? ` Installed: ${installed.join(', ')}.` : '';
|
|
120
|
+
throw new AgentSpecError(`${name}@${qualifier} is not installed.${hint} Install it: agents add ${agent}@${qualifier}`, 'not-installed', agent, installed);
|
|
121
|
+
}
|
|
122
|
+
push(agent, qualifier, 'explicit');
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Single-target commands (`run`, `sync`, `inspect`): resolve a spec that must
|
|
128
|
+
* name exactly one installed version. Rejects `@all` / multi-target specs.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveSingleAgentTarget(spec, provider, opts = {}) {
|
|
131
|
+
const targets = resolveAgentTargets(spec, provider, opts);
|
|
132
|
+
if (targets.length !== 1) {
|
|
133
|
+
throw new AgentSpecError(`'${spec}' resolves to ${targets.length} targets; this command needs exactly one.`, 'multi-not-allowed');
|
|
134
|
+
}
|
|
135
|
+
const t = targets[0];
|
|
136
|
+
if (t.version === null) {
|
|
137
|
+
throw new AgentSpecError(`No installed version for ${AGENTS[t.agent].name}. Run: agents add ${t.agent}@latest`, 'none-installed', t.agent);
|
|
138
|
+
}
|
|
139
|
+
return { agent: t.agent, version: t.version, source: t.source };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Read/list commands: resolve a qualifier into a version filter.
|
|
143
|
+
* undefined / '' / @any → no filter (show all installed versions)
|
|
144
|
+
* @default / @pinned → the literal 'default' sentinel (show the configured default)
|
|
145
|
+
* @latest/@oldest/x.y.z → a concrete version (throws if not installed)
|
|
146
|
+
* Uniform `@default` handling fixes the prior rules-vs-view inconsistency.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveVersionFilter(agent, qualifier, provider, opts = {}) {
|
|
149
|
+
const q = qualifier?.trim();
|
|
150
|
+
if (!q)
|
|
151
|
+
return { version: null, source: 'all-versions' };
|
|
152
|
+
if (q === 'default' || q === 'pinned')
|
|
153
|
+
return { version: 'default', source: 'default' };
|
|
154
|
+
if (q === 'any')
|
|
155
|
+
return { version: null, source: 'all-versions' };
|
|
156
|
+
const { version, source } = resolveSingleAgentTarget(`${agent}@${q}`, provider, { ...opts, availableAgents: [agent] });
|
|
157
|
+
return { version, source };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Concrete version filter for list/display commands whose downstream code
|
|
161
|
+
* filters by an exact version string (not the `'default'` sentinel `view` uses).
|
|
162
|
+
* undefined / '' / @any → undefined (no filter → show all installed)
|
|
163
|
+
* @default / @pinned → the configured default version, or undefined if none
|
|
164
|
+
* is set (falls back to show-all rather than erroring)
|
|
165
|
+
* @latest/@oldest/x.y.z → a concrete version (throws AgentSpecError if bad)
|
|
166
|
+
*/
|
|
167
|
+
export function resolveListFilter(agent, qualifier, provider, opts = {}) {
|
|
168
|
+
const q = qualifier?.trim();
|
|
169
|
+
if (!q || q === 'any')
|
|
170
|
+
return undefined;
|
|
171
|
+
if (q === 'default' || q === 'pinned')
|
|
172
|
+
return provider.getGlobalDefault(agent) ?? undefined;
|
|
173
|
+
return resolveSingleAgentTarget(`${agent}@${q}`, provider, { ...opts, availableAgents: [agent] }).version;
|
|
174
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AgentId } from '../types.js';
|
|
2
|
+
/** How a resolved version was arrived at — for provenance, display, and error rendering. */
|
|
3
|
+
export type VersionSource = 'explicit' | 'project-pin' | 'global-default' | 'global-default(@pinned)' | 'sole-installed' | 'newest-installed' | 'alias-latest' | 'alias-oldest' | 'none';
|
|
4
|
+
export interface AgentTarget {
|
|
5
|
+
agent: AgentId;
|
|
6
|
+
/** Resolved exact version, or null when the agent has no installed versions yet. */
|
|
7
|
+
version: string | null;
|
|
8
|
+
source: VersionSource;
|
|
9
|
+
}
|
|
10
|
+
export type AgentSpecErrorCode = 'empty' | 'unknown-agent' | 'missing-version' | 'invalid-version' | 'not-installed' | 'no-default' | 'none-installed' | 'multi-not-allowed';
|
|
11
|
+
/**
|
|
12
|
+
* Thrown on any bad spec — never `process.exit`, so the engine is safe on the
|
|
13
|
+
* hot path and in library contexts. `code` + `installed` let callers render a
|
|
14
|
+
* consistent message (e.g. the "No default … Specify one:" version list) without
|
|
15
|
+
* string-matching.
|
|
16
|
+
*/
|
|
17
|
+
export declare class AgentSpecError extends Error {
|
|
18
|
+
readonly code: AgentSpecErrorCode;
|
|
19
|
+
readonly agent?: AgentId | undefined;
|
|
20
|
+
readonly installed?: string[] | undefined;
|
|
21
|
+
constructor(message: string, code: AgentSpecErrorCode, agent?: AgentId | undefined, installed?: string[] | undefined);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The filesystem/meta seam. The pure resolver takes this instead of importing
|
|
25
|
+
* versions.ts, so it is fully unit-testable with in-memory fixtures — no $HOME,
|
|
26
|
+
* no subprocess. `provider.ts` supplies the production adapter.
|
|
27
|
+
*/
|
|
28
|
+
export interface VersionProvider {
|
|
29
|
+
/** Installed versions, sorted ascending by `compareVersions`. */
|
|
30
|
+
listInstalled(agent: AgentId): string[];
|
|
31
|
+
/** Version pinned by a project-root agents.yaml, or null. */
|
|
32
|
+
getProjectVersion(agent: AgentId, cwd: string): string | null;
|
|
33
|
+
/** The configured global default version, or null. */
|
|
34
|
+
getGlobalDefault(agent: AgentId): string | null;
|
|
35
|
+
/** Whether an exact version is installed. */
|
|
36
|
+
isInstalled(agent: AgentId, version: string): boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface ResolveOptions {
|
|
39
|
+
/** Project dir for a bare spec's project pin. Defaults to process.cwd(). */
|
|
40
|
+
cwd?: string;
|
|
41
|
+
/** Restrict which agents a spec may name (e.g. only mcp-capable). Defaults to all. */
|
|
42
|
+
availableAgents?: readonly AgentId[];
|
|
43
|
+
/**
|
|
44
|
+
* Bare spec, >1 installed, no pin/default:
|
|
45
|
+
* 'error' (default) → throw AgentSpecError{code:'no-default'} — safe for
|
|
46
|
+
* state-mutating commands (sync/use).
|
|
47
|
+
* 'newest' → pick the newest installed (source:'newest-installed');
|
|
48
|
+
* callers should note it. For execution verbs (run/exec).
|
|
49
|
+
*/
|
|
50
|
+
onAmbiguous?: 'error' | 'newest';
|
|
51
|
+
}
|
|
52
|
+
/** A read/list command's version filter. `null` = show all; `'default'` = show the configured default. */
|
|
53
|
+
export type FilterVersion = string | null | 'default';
|
|
54
|
+
export interface VersionFilter {
|
|
55
|
+
version: FilterVersion;
|
|
56
|
+
source: VersionSource | 'all-versions' | 'default';
|
|
57
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown on any bad spec — never `process.exit`, so the engine is safe on the
|
|
3
|
+
* hot path and in library contexts. `code` + `installed` let callers render a
|
|
4
|
+
* consistent message (e.g. the "No default … Specify one:" version list) without
|
|
5
|
+
* string-matching.
|
|
6
|
+
*/
|
|
7
|
+
export class AgentSpecError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
agent;
|
|
10
|
+
installed;
|
|
11
|
+
constructor(message, code, agent, installed) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.agent = agent;
|
|
15
|
+
this.installed = installed;
|
|
16
|
+
this.name = 'AgentSpecError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed wrapper over the external `crabbox` binary (github.com/openclaw/crabbox).
|
|
3
|
+
*
|
|
4
|
+
* crabbox leases ephemeral cloud boxes (Hetzner/DO/EC2/…), syncs the dirty
|
|
5
|
+
* checkout, and runs commands on them. We use it as the transport for
|
|
6
|
+
* `agents run --lease`: warm a box → run the agent on it via `crabbox run` →
|
|
7
|
+
* stop it. crabbox owns the SSH connection, so agents-cli never needs a direct
|
|
8
|
+
* ssh target (unlike the `agents hosts` model).
|
|
9
|
+
*
|
|
10
|
+
* crabbox talks to its cloud provider's API for list/status/warmup/stop, which
|
|
11
|
+
* needs a provider token (e.g. HCLOUD_TOKEN) in the environment. We inject it
|
|
12
|
+
* from a secrets bundle when one is configured (see `crabboxEnv`).
|
|
13
|
+
*/
|
|
14
|
+
/** A crabbox machine as reported by `crabbox list --json`. */
|
|
15
|
+
export interface CrabboxBox {
|
|
16
|
+
/** Provider machine name, e.g. `crabbox-blue-hermit-1039689b`. */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Provider run state, e.g. `running`. */
|
|
19
|
+
status: string;
|
|
20
|
+
/** Friendly slug used with `--id`, e.g. `blue-hermit`. */
|
|
21
|
+
slug: string;
|
|
22
|
+
/** Lease id, e.g. `cbx_9968746bb15c`. */
|
|
23
|
+
lease: string;
|
|
24
|
+
/** crabbox bootstrap state; `ready` once the box is usable. */
|
|
25
|
+
state: string;
|
|
26
|
+
/** Public IPv4, when the provider exposes one. */
|
|
27
|
+
ip?: string;
|
|
28
|
+
profile?: string;
|
|
29
|
+
class?: string;
|
|
30
|
+
/** True when running + bootstrap-complete. */
|
|
31
|
+
ready: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface CrabboxOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Name of a secrets bundle whose env (e.g. `HCLOUD_TOKEN`) crabbox needs to
|
|
36
|
+
* reach its cloud provider. Resolved via agents-cli's own keychain-backed
|
|
37
|
+
* secrets. When unset, crabbox runs with the ambient environment / its own
|
|
38
|
+
* `crabbox login` credentials.
|
|
39
|
+
*/
|
|
40
|
+
secretsBundle?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Locate the crabbox binary, or throw an actionable error. */
|
|
43
|
+
export declare function findCrabbox(): string;
|
|
44
|
+
/** Build the child env for crabbox, injecting a secrets bundle when configured. */
|
|
45
|
+
export declare function crabboxEnv(opts: CrabboxOptions): NodeJS.ProcessEnv;
|
|
46
|
+
/** All crabbox machines the broker knows about. */
|
|
47
|
+
export declare function crabboxList(opts?: CrabboxOptions): CrabboxBox[];
|
|
48
|
+
/** Find one box by slug, or null. */
|
|
49
|
+
export declare function crabboxFind(slug: string, opts?: CrabboxOptions): CrabboxBox | null;
|
|
50
|
+
export interface WarmupOptions extends CrabboxOptions {
|
|
51
|
+
class?: string;
|
|
52
|
+
profile?: string;
|
|
53
|
+
/** Provision web code-server capability on the box. */
|
|
54
|
+
code?: boolean;
|
|
55
|
+
/** Cloud backend override (crabbox provider id, e.g. hetzner/aws/do). */
|
|
56
|
+
provider?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Lease a box and block until it is ready. Returns the leased box.
|
|
60
|
+
*
|
|
61
|
+
* We diff `crabbox list` before/after so we reliably identify the box this call
|
|
62
|
+
* created even if warmup's stdout format changes — the new lease id is the one
|
|
63
|
+
* that wasn't present before.
|
|
64
|
+
*/
|
|
65
|
+
export declare function crabboxWarmup(opts?: WarmupOptions): CrabboxBox;
|
|
66
|
+
/**
|
|
67
|
+
* Poll until the box reports ready, or throw after timeoutMs.
|
|
68
|
+
* `sleep` is injectable so tests don't wall-clock wait.
|
|
69
|
+
*/
|
|
70
|
+
export declare function crabboxWaitReady(slug: string, opts?: CrabboxOptions & {
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
intervalMs?: number;
|
|
73
|
+
sleep?: (ms: number) => Promise<void>;
|
|
74
|
+
}): Promise<CrabboxBox>;
|
|
75
|
+
export interface CrabboxRunOptions extends CrabboxOptions {
|
|
76
|
+
/** Called with each chunk of combined stdout/stderr as it streams. */
|
|
77
|
+
onData?: (chunk: string) => void;
|
|
78
|
+
/** Force a full remote resync before running. */
|
|
79
|
+
fullResync?: boolean;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Run `remoteCmd` on the leased box via `crabbox run` (crabbox syncs the dirty
|
|
83
|
+
* checkout and owns the SSH). Streams combined output; resolves with the remote
|
|
84
|
+
* exit code (or null if crabbox itself failed to dispatch).
|
|
85
|
+
*/
|
|
86
|
+
export declare function crabboxRun(slug: string, remoteCmd: string, opts?: CrabboxRunOptions): Promise<number | null>;
|
|
87
|
+
/**
|
|
88
|
+
* Upload `script` to the box via `crabbox run --script-stdin` and run it.
|
|
89
|
+
*
|
|
90
|
+
* The script body travels over stdin and is written to a file on the box before
|
|
91
|
+
* execution — it never appears in argv / `ps` / shell history, which is why this
|
|
92
|
+
* is the transport for credential provisioning (the token contents live only in
|
|
93
|
+
* the uploaded script, then the file is removed by the script itself).
|
|
94
|
+
* Streams combined output; resolves with the remote exit code (null on dispatch failure).
|
|
95
|
+
*/
|
|
96
|
+
export declare function crabboxRunScript(slug: string, script: string, opts?: CrabboxRunOptions): Promise<number | null>;
|
|
97
|
+
/** Release the lease / delete the box. Best-effort; never throws. */
|
|
98
|
+
export declare function crabboxStop(slug: string, opts?: CrabboxOptions): boolean;
|