Skip to content

Commit f437e57

Browse files
committed
docs: add CLI Registry documentation
1 parent abb644d commit f437e57

1 file changed

Lines changed: 282 additions & 0 deletions

File tree

docs/features/CLI_REGISTRY.md

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# CLI Registry
2+
3+
The CLI Registry is AgentOS's auto-discovery system for installed command-line tools. It scans the user's PATH for known binaries, detects versions, and exposes results to providers, extensions, and the capability discovery engine.
4+
5+
## Overview
6+
7+
AgentOS ships with a JSON-based registry of 54 CLI descriptors across 8 categories. At startup (or on demand), the `CLIRegistry` runs `which` + `--version` for each registered binary in parallel, producing a scan result that tells the runtime exactly what's available on the host machine.
8+
9+
This powers:
10+
- **LLM provider auto-detection** -- `ClaudeCodeCLIBridge` and `GeminiCLIBridge` check if their binary is installed before attempting subprocess calls.
11+
- **`wunderland doctor`** -- health-check output includes detected CLIs.
12+
- **Capability discovery** -- the discovery engine indexes installed tools as capabilities agents can reference.
13+
- **cli-executor extension** -- `shell_execute` relies on the host having the right binaries.
14+
15+
## Registry Categories
16+
17+
The 54 bundled descriptors live in `src/sandbox/subprocess/registry/` as plain JSON files:
18+
19+
| File | Category | Count | Examples |
20+
|------|----------|-------|----------|
21+
| `llm.json` | llm | 5 | claude, gemini, ollama, lmstudio, aichat |
22+
| `devtools.json` | devtools | 10 | git, gh, docker, docker-compose, kubectl, terraform, make, jq, yq, tmux |
23+
| `runtimes.json` | runtime | 8 | node, python3, deno, bun, ruby, go, rustc, java |
24+
| `package-managers.json` | package-manager | 7 | npm, pnpm, yarn, pip, uv, brew, cargo |
25+
| `cloud.json` | cloud | 9 | gcloud, aws, az, flyctl, vercel, netlify, railway, heroku, wrangler |
26+
| `databases.json` | database | 5 | psql, mysql, sqlite3, redis-cli, mongosh |
27+
| `media.json` | media | 5 | ffmpeg, ffprobe, magick, sox, yt-dlp |
28+
| `networking.json` | networking | 5 | curl, wget, ssh, rsync, scp |
29+
30+
## CLIDescriptor Shape
31+
32+
Each JSON entry conforms to the `CLIDescriptor` interface:
33+
34+
```typescript
35+
interface CLIDescriptor {
36+
/** Binary name on PATH (e.g. 'claude', 'docker', 'ffmpeg'). */
37+
binaryName: string;
38+
/** Human-readable display name. */
39+
displayName: string;
40+
/** What this CLI does. */
41+
description: string;
42+
/** Category for grouping (e.g. 'llm', 'media', 'devtools'). */
43+
category: string;
44+
/** How to install if missing. */
45+
installGuidance: string;
46+
/** Version flag override if not --version. */
47+
versionFlag?: string;
48+
/** Regex to parse version from output (default: /(\d+\.\d+\.\d+)/). */
49+
versionPattern?: RegExp;
50+
}
51+
```
52+
53+
Example from `cloud.json`:
54+
55+
```json
56+
{
57+
"binaryName": "gcloud",
58+
"displayName": "Google Cloud SDK",
59+
"description": "Google Cloud resource management",
60+
"category": "cloud",
61+
"installGuidance": "https://cloud.google.com/sdk/docs/install",
62+
"versionFlag": "--version"
63+
}
64+
```
65+
66+
## CLIRegistry API
67+
68+
```typescript
69+
import { CLIRegistry, WELL_KNOWN_CLIS } from '@framers/agentos/sandbox/subprocess';
70+
```
71+
72+
### Constructor
73+
74+
```typescript
75+
const registry = new CLIRegistry(); // loads bundled JSON descriptors
76+
const empty = new CLIRegistry(false); // starts empty (no defaults)
77+
```
78+
79+
### Methods
80+
81+
| Method | Returns | Description |
82+
|--------|---------|-------------|
83+
| `register(descriptor)` | `void` | Register a single CLI descriptor. Overwrites existing entry for same `binaryName`. |
84+
| `registerAll(descriptors)` | `void` | Register multiple descriptors at once. |
85+
| `unregister(binaryName)` | `boolean` | Remove a descriptor by binary name. |
86+
| `scan()` | `Promise<CLIScanResult[]>` | Scan PATH for all registered CLIs (parallel `which` + `--version`). |
87+
| `check(binaryName)` | `Promise<CLIScanResult>` | Check a single binary by name. |
88+
| `list()` | `CLIDescriptor[]` | Get all registered descriptors (installed status unknown). |
89+
| `installed()` | `Promise<CLIScanResult[]>` | Get only CLIs that are installed. |
90+
| `byCategory(category)` | `Promise<CLIScanResult[]>` | Get CLIs by category (scans first). |
91+
| `categories()` | `string[]` | Get all unique categories. |
92+
| `has(binaryName)` | `boolean` | Check if a binary is registered (not whether installed). |
93+
| `get(binaryName)` | `CLIDescriptor \| undefined` | Get a descriptor by binary name. |
94+
| `size` | `number` | Total number of registered descriptors. |
95+
96+
### CLIScanResult
97+
98+
The result from `scan()` or `check()` extends `CLIDescriptor`:
99+
100+
```typescript
101+
interface CLIScanResult extends CLIDescriptor {
102+
installed: boolean; // whether the binary was found on PATH
103+
binaryPath?: string; // resolved absolute path (e.g. /usr/local/bin/node)
104+
version?: string; // parsed version string (e.g. "22.4.0")
105+
}
106+
```
107+
108+
## Adding Custom CLIs
109+
110+
### Option 1: Edit JSON (permanent)
111+
112+
Add a new entry to an existing category file, or create a new `*.json` file in `src/sandbox/subprocess/registry/`:
113+
114+
```json
115+
[
116+
{
117+
"binaryName": "my-tool",
118+
"displayName": "My Tool",
119+
"description": "Internal deployment CLI",
120+
"category": "devtools",
121+
"installGuidance": "brew install my-tool"
122+
}
123+
]
124+
```
125+
126+
### Option 2: Register at runtime (dynamic)
127+
128+
```typescript
129+
const registry = new CLIRegistry();
130+
131+
registry.register({
132+
binaryName: 'my-tool',
133+
displayName: 'My Tool',
134+
description: 'Internal deployment CLI',
135+
category: 'devtools',
136+
installGuidance: 'brew install my-tool',
137+
});
138+
139+
const result = await registry.check('my-tool');
140+
if (result.installed) {
141+
console.log(`my-tool v${result.version} at ${result.binaryPath}`);
142+
}
143+
```
144+
145+
### Option 3: Full scan with custom CLIs
146+
147+
```typescript
148+
const registry = new CLIRegistry();
149+
150+
// Add several custom CLIs
151+
registry.registerAll([
152+
{ binaryName: 'tsc', displayName: 'TypeScript', description: 'TS compiler', category: 'devtools', installGuidance: 'npm i -g typescript' },
153+
{ binaryName: 'eslint', displayName: 'ESLint', description: 'JS linter', category: 'devtools', installGuidance: 'npm i -g eslint' },
154+
]);
155+
156+
// Scan everything (bundled + custom)
157+
const results = await registry.scan();
158+
for (const r of results) {
159+
const status = r.installed ? `v${r.version}` : 'not installed';
160+
console.log(`${r.displayName.padEnd(24)} ${status}`);
161+
}
162+
163+
// Filter by category
164+
const llmClis = await registry.byCategory('llm');
165+
console.log(`LLM CLIs found: ${llmClis.filter(c => c.installed).length}/${llmClis.length}`);
166+
```
167+
168+
## Integration with CLISubprocessBridge
169+
170+
The `CLISubprocessBridge` is an abstract base class for managing CLI subprocesses. It handles spawning, stdin piping, NDJSON stream parsing, timeouts, and abort signals. Subclasses implement CLI-specific flag assembly and error classification.
171+
172+
Two production bridges extend it:
173+
174+
| Bridge | Binary | Purpose |
175+
|--------|--------|---------|
176+
| `ClaudeCodeCLIBridge` | `claude` | Anthropic Claude via Max subscription (no API key needed) |
177+
| `GeminiCLIBridge` | `gemini` | Google Gemini via Google account login (no API key needed) |
178+
179+
Both bridges use `checkBinaryInstalled()` (which internally runs `which` + `--version`) before attempting LLM calls, and fall back gracefully when the binary is missing.
180+
181+
### Creating a custom bridge
182+
183+
```typescript
184+
import { CLISubprocessBridge } from '@framers/agentos/sandbox/subprocess';
185+
import { CLISubprocessError, CLI_ERROR } from '@framers/agentos/sandbox/subprocess';
186+
187+
class MyToolBridge extends CLISubprocessBridge {
188+
protected readonly binaryName = 'mytool';
189+
190+
protected buildArgs(options, format) {
191+
return ['--prompt', options.prompt, '--format', format];
192+
}
193+
194+
protected classifyError(error) {
195+
if (error.code === 'ENOENT') {
196+
return new CLISubprocessError(
197+
'mytool not found',
198+
CLI_ERROR.BINARY_NOT_FOUND,
199+
'mytool',
200+
'Install: brew install mytool',
201+
false,
202+
);
203+
}
204+
return new CLISubprocessError(
205+
error.message,
206+
CLI_ERROR.CRASHED,
207+
'mytool',
208+
'Check mytool logs',
209+
true,
210+
);
211+
}
212+
213+
protected parseStreamEvent(raw) {
214+
if (raw.text) return { type: 'text_delta', text: raw.text };
215+
if (raw.done) return { type: 'result', result: raw.output };
216+
return null;
217+
}
218+
}
219+
```
220+
221+
## Integration with cli-executor Extension
222+
223+
The `cli-executor` extension pack (`@framers/agentos-ext-cli-executor`) provides tools that let agents execute arbitrary shell commands on the host. While it does not import `CLIRegistry` directly, the two systems are complementary:
224+
225+
- **CLIRegistry** answers "what binaries exist?" -- discovery and detection.
226+
- **cli-executor** answers "can the agent run this command?" -- execution with security guardrails.
227+
228+
When the wunderland runtime loads the cli-executor extension, it configures filesystem roots, security checks, and the `dangerouslySkipSecurityChecks` flag based on the active security tier. See the [Wunderland CLI Tools doc](../../../wunderland/docs/features/CLI_TOOLS.md) for details.
229+
230+
## Security Considerations
231+
232+
The CLI Registry itself is read-only and does not execute commands beyond `which` and `--version`. However, downstream consumers should respect the active security tier:
233+
234+
| Security Tier | CLI Execution | File Writes | External APIs |
235+
|--------------|---------------|-------------|---------------|
236+
| `dangerous` | Allowed | Allowed | Allowed |
237+
| `permissive` | Allowed | Allowed | Allowed |
238+
| `balanced` | Allowed | Blocked | Allowed |
239+
| `strict` | Blocked | Blocked | Allowed |
240+
| `paranoid` | Blocked | Blocked | Blocked |
241+
242+
The `balanced` tier is the recommended default. It permits CLI execution but blocks file writes unless the agent requests folder access through the HITL approval flow.
243+
244+
## Error Handling
245+
246+
The `CLISubprocessError` class provides structured errors with actionable guidance:
247+
248+
```typescript
249+
import { CLISubprocessError, CLI_ERROR } from '@framers/agentos/sandbox/subprocess';
250+
251+
// Common error codes:
252+
CLI_ERROR.BINARY_NOT_FOUND // Binary not found on PATH
253+
CLI_ERROR.NOT_AUTHENTICATED // Binary installed but not logged in
254+
CLI_ERROR.VERSION_OUTDATED // Version too old for required features
255+
CLI_ERROR.SPAWN_FAILED // Process failed to start
256+
CLI_ERROR.TIMEOUT // Process exceeded timeout
257+
CLI_ERROR.CRASHED // Non-zero exit code
258+
CLI_ERROR.RATE_LIMITED // Rate limit / quota exceeded
259+
CLI_ERROR.PERMISSION_DENIED // EACCES
260+
CLI_ERROR.CONTEXT_TOO_LONG // Input too long for the CLI
261+
```
262+
263+
Each error carries a `guidance` string with human-readable fix instructions and a `recoverable` flag indicating whether retry/fallback is appropriate.
264+
265+
## Exports
266+
267+
Everything is exported from the barrel at `@framers/agentos/sandbox/subprocess`:
268+
269+
```typescript
270+
export { CLISubprocessBridge } from './CLISubprocessBridge';
271+
export { CLIRegistry, WELL_KNOWN_CLIS } from './CLIRegistry';
272+
export { CLISubprocessError, CLI_ERROR } from './errors';
273+
export type {
274+
BridgeOptions,
275+
BridgeResult,
276+
StreamEvent,
277+
OutputFormat,
278+
InstallCheckResult,
279+
CLIDescriptor,
280+
CLIScanResult,
281+
} from './types';
282+
```

0 commit comments

Comments
 (0)