feat(cli): v2 rewrite with full command coverage and lazy evaluation
- Fix eager required() evaluation bug (route map was evaluated at parse time, causing unrelated commands to crash on missing flags) - Refactor to lazy command handler pattern (functions only execute when their group+action matches) - Add compound subcommands: agents memory get|set|clear, agents soul get|set|templates, tasks comments list|add - Add missing PRD commands: agents attribution, tasks broadcast, sessions transcript, tokens agent-costs/task-costs/trends/export/rotate, status health/overview/dashboard/gateway/models/capabilities, export audit/tasks/activities/pipelines - Add proper SSE streaming for events watch (NDJSON in --json mode, human-readable otherwise, graceful Ctrl+C shutdown) - Add optional() helper for flags with defaults - Update docs/cli-agent-control.md with full v2 command reference
This commit is contained in:
parent
7b104952cc
commit
f2747b5330
|
|
@ -1,15 +1,16 @@
|
||||||
# Mission Control CLI for Agent-Complete Operations (v1 scaffold)
|
# Mission Control CLI for Agent-Complete Operations (v2)
|
||||||
|
|
||||||
This repository now includes a first-party CLI scaffold at:
|
This repository includes a first-party CLI at:
|
||||||
|
|
||||||
- scripts/mc-cli.cjs
|
- scripts/mc-cli.cjs
|
||||||
|
|
||||||
It is designed for autonomous/headless usage first:
|
Designed for autonomous/headless usage first:
|
||||||
- API key auth support
|
- API key auth support
|
||||||
- profile persistence (~/.mission-control/profiles/*.json)
|
- Profile persistence (~/.mission-control/profiles/*.json)
|
||||||
- stable JSON mode (`--json`)
|
- Stable JSON mode (`--json`) with NDJSON for streaming
|
||||||
- deterministic exit code categories
|
- Deterministic exit code categories
|
||||||
- command groups mapped to Mission Control API resources
|
- SSE streaming for real-time event watching
|
||||||
|
- Compound subcommands for memory, soul, comments
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
|
|
@ -21,22 +22,104 @@ It is designed for autonomous/headless usage first:
|
||||||
|
|
||||||
3) Run commands:
|
3) Run commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
node scripts/mc-cli.cjs agents list --json
|
node scripts/mc-cli.cjs agents list --json
|
||||||
node scripts/mc-cli.cjs tasks queue --agent Aegis --max-capacity 2 --json
|
node scripts/mc-cli.cjs tasks queue --agent Aegis --max-capacity 2 --json
|
||||||
node scripts/mc-cli.cjs sessions control --id <session-id> --action terminate
|
node scripts/mc-cli.cjs sessions control --id <session-id> --action terminate
|
||||||
|
```
|
||||||
|
|
||||||
## Supported groups in scaffold
|
## Command groups
|
||||||
|
|
||||||
- auth: login, logout, whoami
|
### auth
|
||||||
- agents: list/get/create/update/delete/wake/diagnostics/heartbeat
|
- login --username --password
|
||||||
- tasks: list/get/create/update/delete/queue
|
- logout
|
||||||
- sessions: list/control/continue
|
- whoami
|
||||||
- connect: register/list/disconnect
|
|
||||||
- tokens: list/stats/by-agent
|
### agents
|
||||||
- skills: list/content/check/upsert/delete
|
- list
|
||||||
- cron: list/create/update/pause/resume/remove/run
|
- get --id
|
||||||
- events: watch (basic HTTP fallback)
|
- create --name --role [--body '{}']
|
||||||
- raw: generic request passthrough
|
- update --id [--body '{}']
|
||||||
|
- delete --id
|
||||||
|
- wake --id
|
||||||
|
- diagnostics --id
|
||||||
|
- heartbeat --id
|
||||||
|
- attribution --id [--hours 24] [--section identity,cost] [--privileged]
|
||||||
|
- memory get --id
|
||||||
|
- memory set --id --content "..." [--append]
|
||||||
|
- memory set --id --file ./memory.md
|
||||||
|
- memory clear --id
|
||||||
|
- soul get --id
|
||||||
|
- soul set --id --content "..."
|
||||||
|
- soul set --id --file ./soul.md
|
||||||
|
- soul set --id --template operator
|
||||||
|
- soul templates --id [--template name]
|
||||||
|
|
||||||
|
### tasks
|
||||||
|
- list
|
||||||
|
- get --id
|
||||||
|
- create --title [--body '{}']
|
||||||
|
- update --id [--body '{}']
|
||||||
|
- delete --id
|
||||||
|
- queue --agent <name> [--max-capacity 2]
|
||||||
|
- broadcast --id --message "..."
|
||||||
|
- comments list --id
|
||||||
|
- comments add --id --content "..." [--parent-id 5]
|
||||||
|
|
||||||
|
### sessions
|
||||||
|
- list
|
||||||
|
- control --id --action monitor|pause|terminate
|
||||||
|
- continue --kind claude-code|codex-cli --id --prompt "..."
|
||||||
|
- transcript --kind claude-code|codex-cli|hermes --id [--limit 40] [--source]
|
||||||
|
|
||||||
|
### connect
|
||||||
|
- register --tool-name --agent-name [--body '{}']
|
||||||
|
- list
|
||||||
|
- disconnect --connection-id
|
||||||
|
|
||||||
|
### tokens
|
||||||
|
- list [--timeframe hour|day|week|month|all]
|
||||||
|
- stats [--timeframe]
|
||||||
|
- by-agent [--days 30]
|
||||||
|
- agent-costs [--timeframe]
|
||||||
|
- task-costs [--timeframe]
|
||||||
|
- trends [--timeframe]
|
||||||
|
- export [--format json|csv] [--timeframe] [--limit]
|
||||||
|
- rotate (shows current key info)
|
||||||
|
- rotate --confirm (generates new key -- admin only)
|
||||||
|
|
||||||
|
### skills
|
||||||
|
- list
|
||||||
|
- content --source --name
|
||||||
|
- check --source --name
|
||||||
|
- upsert --source --name --file ./skill.md
|
||||||
|
- delete --source --name
|
||||||
|
|
||||||
|
### cron
|
||||||
|
- list
|
||||||
|
- create/update/pause/resume/remove/run [--body '{}']
|
||||||
|
|
||||||
|
### events
|
||||||
|
- watch [--types agent,task] [--timeout-ms 3600000]
|
||||||
|
|
||||||
|
Streams SSE events to stdout. In `--json` mode, outputs NDJSON (one JSON object per line). Press Ctrl+C to stop.
|
||||||
|
|
||||||
|
### status
|
||||||
|
- health (no auth required)
|
||||||
|
- overview
|
||||||
|
- dashboard
|
||||||
|
- gateway
|
||||||
|
- models
|
||||||
|
- capabilities
|
||||||
|
|
||||||
|
### export (admin)
|
||||||
|
- audit [--format json|csv] [--since <unix>] [--until <unix>] [--limit]
|
||||||
|
- tasks [--format json|csv] [--since] [--until] [--limit]
|
||||||
|
- activities [--format json|csv] [--since] [--until] [--limit]
|
||||||
|
- pipelines [--format json|csv] [--since] [--until] [--limit]
|
||||||
|
|
||||||
|
### raw
|
||||||
|
- raw --method GET --path /api/... [--body '{}']
|
||||||
|
|
||||||
## Exit code contract
|
## Exit code contract
|
||||||
|
|
||||||
|
|
@ -51,14 +134,18 @@ node scripts/mc-cli.cjs sessions control --id <session-id> --action terminate
|
||||||
|
|
||||||
To detect drift between Next.js route handlers and openapi.json, use:
|
To detect drift between Next.js route handlers and openapi.json, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
node scripts/check-api-contract-parity.mjs \
|
node scripts/check-api-contract-parity.mjs \
|
||||||
--root . \
|
--root . \
|
||||||
--openapi openapi.json \
|
--openapi openapi.json \
|
||||||
--ignore-file scripts/api-contract-parity.ignore
|
--ignore-file scripts/api-contract-parity.ignore
|
||||||
|
```
|
||||||
|
|
||||||
Machine output:
|
Machine output:
|
||||||
|
|
||||||
|
```bash
|
||||||
node scripts/check-api-contract-parity.mjs --json
|
node scripts/check-api-contract-parity.mjs --json
|
||||||
|
```
|
||||||
|
|
||||||
The checker scans `src/app/api/**/route.ts(x)`, derives operations (METHOD + /api/path), compares against OpenAPI operations, and exits non-zero on mismatch.
|
The checker scans `src/app/api/**/route.ts(x)`, derives operations (METHOD + /api/path), compares against OpenAPI operations, and exits non-zero on mismatch.
|
||||||
|
|
||||||
|
|
@ -70,7 +157,7 @@ Baseline policy in this repo:
|
||||||
|
|
||||||
## Next steps
|
## Next steps
|
||||||
|
|
||||||
- Promote scripts to package.json scripts (`mc`, `api:parity`).
|
- Promote script to package.json bin entry (`mc`).
|
||||||
- Add retry/backoff and SSE stream mode for `events watch`.
|
- Add retry/backoff for transient failures.
|
||||||
- Add richer pagination/filter UX and CSV export for reporting commands.
|
|
||||||
- Add integration tests that run the CLI against a test server fixture.
|
- Add integration tests that run the CLI against a test server fixture.
|
||||||
|
- Add richer pagination/filter flags for list commands.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/*
|
/*
|
||||||
Mission Control CLI (v1 scaffold)
|
Mission Control CLI (v2)
|
||||||
- Zero heavy dependencies
|
- Zero heavy dependencies
|
||||||
- API-key first for agent automation
|
- API-key first for agent automation
|
||||||
- JSON mode + stable exit codes
|
- JSON mode + stable exit codes
|
||||||
|
- Lazy command resolution (no eager required() calls)
|
||||||
|
- SSE streaming for events watch
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
|
|
@ -46,16 +48,20 @@ Usage:
|
||||||
mc <group> <action> [--flags]
|
mc <group> <action> [--flags]
|
||||||
|
|
||||||
Groups:
|
Groups:
|
||||||
auth login/logout/whoami
|
auth login/logout/whoami
|
||||||
agents list/get/create/update/delete/wake/diagnostics/heartbeat
|
agents list/get/create/update/delete/wake/diagnostics/heartbeat
|
||||||
tasks list/get/create/update/delete/queue/comment
|
memory get|set|clear / soul get|set|templates / attribution
|
||||||
sessions list/control/continue
|
tasks list/get/create/update/delete/queue
|
||||||
connect register/list/disconnect
|
comments list|add / broadcast
|
||||||
tokens list/stats/by-agent
|
sessions list/control/continue/transcript
|
||||||
skills list/content/upsert/delete/check
|
connect register/list/disconnect
|
||||||
cron list/create/update/pause/resume/remove/run
|
tokens list/stats/by-agent/agent-costs/task-costs/export/rotate
|
||||||
events watch
|
skills list/content/upsert/delete/check
|
||||||
raw request fallback
|
cron list/create/update/pause/resume/remove/run
|
||||||
|
events watch
|
||||||
|
status health/overview/dashboard/gateway/models/capabilities
|
||||||
|
export audit/tasks/activities/pipelines
|
||||||
|
raw request fallback
|
||||||
|
|
||||||
Common flags:
|
Common flags:
|
||||||
--profile <name> profile name (default: default)
|
--profile <name> profile name (default: default)
|
||||||
|
|
@ -67,8 +73,16 @@ Common flags:
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
mc agents list --json
|
mc agents list --json
|
||||||
|
mc agents memory get --id 5
|
||||||
|
mc agents soul set --id 5 --template operator
|
||||||
mc tasks queue --agent Aegis --max-capacity 2
|
mc tasks queue --agent Aegis --max-capacity 2
|
||||||
mc sessions control --id abc123 --action terminate
|
mc tasks comments list --id 42
|
||||||
|
mc tasks comments add --id 42 --content "Looks good"
|
||||||
|
mc sessions transcript --kind claude-code --id abc123
|
||||||
|
mc tokens agent-costs --timeframe week
|
||||||
|
mc tokens export --format csv
|
||||||
|
mc status health
|
||||||
|
mc events watch --types agent,task
|
||||||
mc raw --method GET --path /api/status --json
|
mc raw --method GET --path /api/status --json
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +140,25 @@ function mapStatusToExit(status) {
|
||||||
return EXIT.USAGE;
|
return EXIT.USAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function required(flags, key) {
|
||||||
|
const value = flags[key];
|
||||||
|
if (value === undefined || value === true || String(value).trim() === '') {
|
||||||
|
throw new Error(`Missing required flag --${key}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function optional(flags, key, fallback) {
|
||||||
|
const value = flags[key];
|
||||||
|
if (value === undefined || value === true) return fallback;
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bodyFromFlags(flags) {
|
||||||
|
if (flags.body) return JSON.parse(String(flags.body));
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async function httpRequest({ baseUrl, apiKey, cookie, method, route, body, timeoutMs = 20000 }) {
|
async function httpRequest({ baseUrl, apiKey, cookie, method, route, body, timeoutMs = 20000 }) {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
|
@ -171,6 +204,70 @@ async function httpRequest({ baseUrl, apiKey, cookie, method, route, body, timeo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sseStream({ baseUrl, apiKey, cookie, route, timeoutMs, onEvent, onError }) {
|
||||||
|
const headers = { Accept: 'text/event-stream' };
|
||||||
|
if (apiKey) headers['x-api-key'] = apiKey;
|
||||||
|
if (cookie) headers['Cookie'] = cookie;
|
||||||
|
const url = `${normalizeBaseUrl(baseUrl)}${route}`;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
let timer;
|
||||||
|
if (timeoutMs && timeoutMs < Infinity) {
|
||||||
|
timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graceful shutdown on SIGINT/SIGTERM
|
||||||
|
const shutdown = () => { controller.abort(); };
|
||||||
|
process.on('SIGINT', shutdown);
|
||||||
|
process.on('SIGTERM', shutdown);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { headers, signal: controller.signal });
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
onError({ status: res.status, data: text });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// Parse SSE frames
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || '';
|
||||||
|
let currentData = '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
currentData += line.slice(6);
|
||||||
|
} else if (line === '' && currentData) {
|
||||||
|
try {
|
||||||
|
const event = JSON.parse(currentData);
|
||||||
|
onEvent(event);
|
||||||
|
} catch {
|
||||||
|
// Non-JSON data line, emit raw
|
||||||
|
onEvent({ raw: currentData });
|
||||||
|
}
|
||||||
|
currentData = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err?.name === 'AbortError') return; // clean shutdown
|
||||||
|
onError({ error: err?.message || 'SSE connection error' });
|
||||||
|
} finally {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
process.removeListener('SIGINT', shutdown);
|
||||||
|
process.removeListener('SIGTERM', shutdown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function printResult(result, asJson) {
|
function printResult(result, asJson) {
|
||||||
if (asJson) {
|
if (asJson) {
|
||||||
console.log(JSON.stringify(result, null, 2));
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
|
@ -187,14 +284,358 @@ function printResult(result, asJson) {
|
||||||
console.error(JSON.stringify(result.data, null, 2));
|
console.error(JSON.stringify(result.data, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
function required(flags, key) {
|
// --- Command handlers ---
|
||||||
const value = flags[key];
|
// Each returns { method, route, body? } or handles the request directly and returns null.
|
||||||
if (value === undefined || value === true || String(value).trim() === '') {
|
|
||||||
throw new Error(`Missing required flag --${key}`);
|
const commands = {
|
||||||
|
auth: {
|
||||||
|
async login(flags, ctx) {
|
||||||
|
const username = required(flags, 'username');
|
||||||
|
const password = required(flags, 'password');
|
||||||
|
const result = await httpRequest({
|
||||||
|
baseUrl: ctx.baseUrl,
|
||||||
|
method: 'POST',
|
||||||
|
route: '/api/auth/login',
|
||||||
|
body: { username, password },
|
||||||
|
timeoutMs: ctx.timeoutMs,
|
||||||
|
});
|
||||||
|
if (result.ok && result.setCookie) {
|
||||||
|
ctx.profile.url = ctx.baseUrl;
|
||||||
|
ctx.profile.cookie = result.setCookie.split(';')[0];
|
||||||
|
if (ctx.apiKey) ctx.profile.apiKey = ctx.apiKey;
|
||||||
|
saveProfile(ctx.profile);
|
||||||
|
result.data = { ...result.data, profile: ctx.profile.name, saved_cookie: true };
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
async logout(flags, ctx) {
|
||||||
|
const result = await httpRequest({ baseUrl: ctx.baseUrl, apiKey: ctx.apiKey, cookie: ctx.profile.cookie, method: 'POST', route: '/api/auth/logout', timeoutMs: ctx.timeoutMs });
|
||||||
|
if (result.ok) {
|
||||||
|
ctx.profile.cookie = '';
|
||||||
|
saveProfile(ctx.profile);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
whoami: () => ({ method: 'GET', route: '/api/auth/me' }),
|
||||||
|
},
|
||||||
|
|
||||||
|
agents: {
|
||||||
|
list: () => ({ method: 'GET', route: '/api/agents' }),
|
||||||
|
get: (flags) => ({ method: 'GET', route: `/api/agents/${required(flags, 'id')}` }),
|
||||||
|
create: (flags) => ({
|
||||||
|
method: 'POST',
|
||||||
|
route: '/api/agents',
|
||||||
|
body: bodyFromFlags(flags) || { name: required(flags, 'name'), role: required(flags, 'role') },
|
||||||
|
}),
|
||||||
|
update: (flags) => ({
|
||||||
|
method: 'PUT',
|
||||||
|
route: `/api/agents/${required(flags, 'id')}`,
|
||||||
|
body: bodyFromFlags(flags) || {},
|
||||||
|
}),
|
||||||
|
delete: (flags) => ({ method: 'DELETE', route: `/api/agents/${required(flags, 'id')}` }),
|
||||||
|
wake: (flags) => ({ method: 'POST', route: `/api/agents/${required(flags, 'id')}/wake` }),
|
||||||
|
diagnostics: (flags) => ({ method: 'GET', route: `/api/agents/${required(flags, 'id')}/diagnostics` }),
|
||||||
|
heartbeat: (flags) => ({ method: 'POST', route: `/api/agents/${required(flags, 'id')}/heartbeat` }),
|
||||||
|
attribution: (flags) => {
|
||||||
|
const id = required(flags, 'id');
|
||||||
|
const hours = optional(flags, 'hours', '24');
|
||||||
|
const section = optional(flags, 'section', undefined);
|
||||||
|
let qs = `?hours=${encodeURIComponent(hours)}`;
|
||||||
|
if (section) qs += `§ion=${encodeURIComponent(section)}`;
|
||||||
|
if (flags.privileged) qs += '&privileged=1';
|
||||||
|
return { method: 'GET', route: `/api/agents/${id}/attribution${qs}` };
|
||||||
|
},
|
||||||
|
// Subcommand: agents memory get|set|clear --id <id>
|
||||||
|
memory: (flags) => {
|
||||||
|
const id = required(flags, 'id');
|
||||||
|
const sub = flags._sub;
|
||||||
|
if (sub === 'get' || !sub) return { method: 'GET', route: `/api/agents/${id}/memory` };
|
||||||
|
if (sub === 'set') {
|
||||||
|
const content = flags.content || flags.file
|
||||||
|
? fs.readFileSync(required(flags, 'file'), 'utf8')
|
||||||
|
: required(flags, 'content');
|
||||||
|
return {
|
||||||
|
method: 'PUT',
|
||||||
|
route: `/api/agents/${id}/memory`,
|
||||||
|
body: { working_memory: content, append: Boolean(flags.append) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (sub === 'clear') return { method: 'DELETE', route: `/api/agents/${id}/memory` };
|
||||||
|
throw new Error(`Unknown agents memory subcommand: ${sub}. Use get|set|clear`);
|
||||||
|
},
|
||||||
|
// Subcommand: agents soul get|set|templates --id <id>
|
||||||
|
soul: (flags) => {
|
||||||
|
const id = required(flags, 'id');
|
||||||
|
const sub = flags._sub;
|
||||||
|
if (sub === 'get' || !sub) return { method: 'GET', route: `/api/agents/${id}/soul` };
|
||||||
|
if (sub === 'set') {
|
||||||
|
const body = {};
|
||||||
|
if (flags.template) body.template_name = flags.template;
|
||||||
|
else if (flags.file) body.soul_content = fs.readFileSync(String(flags.file), 'utf8');
|
||||||
|
else body.soul_content = required(flags, 'content');
|
||||||
|
return { method: 'PUT', route: `/api/agents/${id}/soul`, body };
|
||||||
|
}
|
||||||
|
if (sub === 'templates') {
|
||||||
|
const template = optional(flags, 'template', undefined);
|
||||||
|
const qs = template ? `?template=${encodeURIComponent(template)}` : '';
|
||||||
|
return { method: 'PATCH', route: `/api/agents/${id}/soul${qs}` };
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown agents soul subcommand: ${sub}. Use get|set|templates`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
tasks: {
|
||||||
|
list: () => ({ method: 'GET', route: '/api/tasks' }),
|
||||||
|
get: (flags) => ({ method: 'GET', route: `/api/tasks/${required(flags, 'id')}` }),
|
||||||
|
create: (flags) => ({
|
||||||
|
method: 'POST',
|
||||||
|
route: '/api/tasks',
|
||||||
|
body: bodyFromFlags(flags) || { title: required(flags, 'title') },
|
||||||
|
}),
|
||||||
|
update: (flags) => ({
|
||||||
|
method: 'PUT',
|
||||||
|
route: `/api/tasks/${required(flags, 'id')}`,
|
||||||
|
body: bodyFromFlags(flags) || {},
|
||||||
|
}),
|
||||||
|
delete: (flags) => ({ method: 'DELETE', route: `/api/tasks/${required(flags, 'id')}` }),
|
||||||
|
queue: (flags) => {
|
||||||
|
const agent = required(flags, 'agent');
|
||||||
|
let qs = `?agent=${encodeURIComponent(agent)}`;
|
||||||
|
if (flags['max-capacity']) qs += `&max_capacity=${encodeURIComponent(String(flags['max-capacity']))}`;
|
||||||
|
return { method: 'GET', route: `/api/tasks/queue${qs}` };
|
||||||
|
},
|
||||||
|
broadcast: (flags) => ({
|
||||||
|
method: 'POST',
|
||||||
|
route: `/api/tasks/${required(flags, 'id')}/broadcast`,
|
||||||
|
body: { message: required(flags, 'message') },
|
||||||
|
}),
|
||||||
|
// Subcommand: tasks comments list|add --id <id>
|
||||||
|
comments: (flags) => {
|
||||||
|
const id = required(flags, 'id');
|
||||||
|
const sub = flags._sub;
|
||||||
|
if (sub === 'list' || !sub) return { method: 'GET', route: `/api/tasks/${id}/comments` };
|
||||||
|
if (sub === 'add') {
|
||||||
|
const body = { content: required(flags, 'content') };
|
||||||
|
if (flags['parent-id']) body.parent_id = Number(flags['parent-id']);
|
||||||
|
return { method: 'POST', route: `/api/tasks/${id}/comments`, body };
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown tasks comments subcommand: ${sub}. Use list|add`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
sessions: {
|
||||||
|
list: () => ({ method: 'GET', route: '/api/sessions' }),
|
||||||
|
control: (flags) => ({
|
||||||
|
method: 'POST',
|
||||||
|
route: `/api/sessions/${required(flags, 'id')}/control`,
|
||||||
|
body: { action: required(flags, 'action') },
|
||||||
|
}),
|
||||||
|
continue: (flags) => ({
|
||||||
|
method: 'POST',
|
||||||
|
route: '/api/sessions/continue',
|
||||||
|
body: {
|
||||||
|
kind: required(flags, 'kind'),
|
||||||
|
id: required(flags, 'id'),
|
||||||
|
prompt: required(flags, 'prompt'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
transcript: (flags) => {
|
||||||
|
const kind = required(flags, 'kind');
|
||||||
|
const id = required(flags, 'id');
|
||||||
|
let qs = `?kind=${encodeURIComponent(kind)}&id=${encodeURIComponent(id)}`;
|
||||||
|
if (flags.limit) qs += `&limit=${encodeURIComponent(String(flags.limit))}`;
|
||||||
|
if (flags.source) qs += `&source=${encodeURIComponent(String(flags.source))}`;
|
||||||
|
return { method: 'GET', route: `/api/sessions/transcript${qs}` };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
connect: {
|
||||||
|
register: (flags) => ({
|
||||||
|
method: 'POST',
|
||||||
|
route: '/api/connect',
|
||||||
|
body: bodyFromFlags(flags) || { tool_name: required(flags, 'tool-name'), agent_name: required(flags, 'agent-name') },
|
||||||
|
}),
|
||||||
|
list: () => ({ method: 'GET', route: '/api/connect' }),
|
||||||
|
disconnect: (flags) => ({
|
||||||
|
method: 'DELETE',
|
||||||
|
route: '/api/connect',
|
||||||
|
body: { connection_id: required(flags, 'connection-id') },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
tokens: {
|
||||||
|
list: (flags) => {
|
||||||
|
let qs = '?action=list';
|
||||||
|
if (flags.timeframe) qs += `&timeframe=${encodeURIComponent(String(flags.timeframe))}`;
|
||||||
|
return { method: 'GET', route: `/api/tokens${qs}` };
|
||||||
|
},
|
||||||
|
stats: (flags) => {
|
||||||
|
let qs = '?action=stats';
|
||||||
|
if (flags.timeframe) qs += `&timeframe=${encodeURIComponent(String(flags.timeframe))}`;
|
||||||
|
return { method: 'GET', route: `/api/tokens${qs}` };
|
||||||
|
},
|
||||||
|
'by-agent': (flags) => ({
|
||||||
|
method: 'GET',
|
||||||
|
route: `/api/tokens/by-agent?days=${encodeURIComponent(String(flags.days || '30'))}`,
|
||||||
|
}),
|
||||||
|
'agent-costs': (flags) => {
|
||||||
|
let qs = '?action=agent-costs';
|
||||||
|
if (flags.timeframe) qs += `&timeframe=${encodeURIComponent(String(flags.timeframe))}`;
|
||||||
|
return { method: 'GET', route: `/api/tokens${qs}` };
|
||||||
|
},
|
||||||
|
'task-costs': (flags) => {
|
||||||
|
let qs = '?action=task-costs';
|
||||||
|
if (flags.timeframe) qs += `&timeframe=${encodeURIComponent(String(flags.timeframe))}`;
|
||||||
|
return { method: 'GET', route: `/api/tokens${qs}` };
|
||||||
|
},
|
||||||
|
trends: (flags) => {
|
||||||
|
let qs = '?action=trends';
|
||||||
|
if (flags.timeframe) qs += `&timeframe=${encodeURIComponent(String(flags.timeframe))}`;
|
||||||
|
return { method: 'GET', route: `/api/tokens${qs}` };
|
||||||
|
},
|
||||||
|
export: (flags) => {
|
||||||
|
const format = optional(flags, 'format', 'json');
|
||||||
|
let qs = `?action=export&format=${encodeURIComponent(format)}`;
|
||||||
|
if (flags.timeframe) qs += `&timeframe=${encodeURIComponent(String(flags.timeframe))}`;
|
||||||
|
if (flags.limit) qs += `&limit=${encodeURIComponent(String(flags.limit))}`;
|
||||||
|
return { method: 'GET', route: `/api/tokens${qs}` };
|
||||||
|
},
|
||||||
|
rotate: (flags) => {
|
||||||
|
if (flags.confirm) return { method: 'POST', route: '/api/tokens/rotate' };
|
||||||
|
return { method: 'GET', route: '/api/tokens/rotate' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
skills: {
|
||||||
|
list: () => ({ method: 'GET', route: '/api/skills' }),
|
||||||
|
content: (flags) => ({
|
||||||
|
method: 'GET',
|
||||||
|
route: `/api/skills?mode=content&source=${encodeURIComponent(required(flags, 'source'))}&name=${encodeURIComponent(required(flags, 'name'))}`,
|
||||||
|
}),
|
||||||
|
check: (flags) => ({
|
||||||
|
method: 'GET',
|
||||||
|
route: `/api/skills?mode=check&source=${encodeURIComponent(required(flags, 'source'))}&name=${encodeURIComponent(required(flags, 'name'))}`,
|
||||||
|
}),
|
||||||
|
upsert: (flags) => ({
|
||||||
|
method: 'PUT',
|
||||||
|
route: '/api/skills',
|
||||||
|
body: {
|
||||||
|
source: required(flags, 'source'),
|
||||||
|
name: required(flags, 'name'),
|
||||||
|
content: fs.readFileSync(required(flags, 'file'), 'utf8'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
delete: (flags) => ({
|
||||||
|
method: 'DELETE',
|
||||||
|
route: `/api/skills?source=${encodeURIComponent(required(flags, 'source'))}&name=${encodeURIComponent(required(flags, 'name'))}`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
cron: {
|
||||||
|
list: () => ({ method: 'GET', route: '/api/cron' }),
|
||||||
|
create: (flags) => ({ method: 'POST', route: '/api/cron', body: bodyFromFlags(flags) || {} }),
|
||||||
|
update: (flags) => ({ method: 'POST', route: '/api/cron', body: bodyFromFlags(flags) || {} }),
|
||||||
|
pause: (flags) => ({ method: 'POST', route: '/api/cron', body: bodyFromFlags(flags) || {} }),
|
||||||
|
resume: (flags) => ({ method: 'POST', route: '/api/cron', body: bodyFromFlags(flags) || {} }),
|
||||||
|
remove: (flags) => ({ method: 'POST', route: '/api/cron', body: bodyFromFlags(flags) || {} }),
|
||||||
|
run: (flags) => ({ method: 'POST', route: '/api/cron', body: bodyFromFlags(flags) || {} }),
|
||||||
|
},
|
||||||
|
|
||||||
|
status: {
|
||||||
|
health: () => ({ method: 'GET', route: '/api/status?action=health' }),
|
||||||
|
overview: () => ({ method: 'GET', route: '/api/status?action=overview' }),
|
||||||
|
dashboard: () => ({ method: 'GET', route: '/api/status?action=dashboard' }),
|
||||||
|
gateway: () => ({ method: 'GET', route: '/api/status?action=gateway' }),
|
||||||
|
models: () => ({ method: 'GET', route: '/api/status?action=models' }),
|
||||||
|
capabilities: () => ({ method: 'GET', route: '/api/status?action=capabilities' }),
|
||||||
|
},
|
||||||
|
|
||||||
|
export: {
|
||||||
|
audit: (flags) => {
|
||||||
|
const format = optional(flags, 'format', 'json');
|
||||||
|
let qs = `?type=audit&format=${encodeURIComponent(format)}`;
|
||||||
|
if (flags.since) qs += `&since=${encodeURIComponent(String(flags.since))}`;
|
||||||
|
if (flags.until) qs += `&until=${encodeURIComponent(String(flags.until))}`;
|
||||||
|
if (flags.limit) qs += `&limit=${encodeURIComponent(String(flags.limit))}`;
|
||||||
|
return { method: 'GET', route: `/api/export${qs}` };
|
||||||
|
},
|
||||||
|
tasks: (flags) => {
|
||||||
|
const format = optional(flags, 'format', 'json');
|
||||||
|
let qs = `?type=tasks&format=${encodeURIComponent(format)}`;
|
||||||
|
if (flags.since) qs += `&since=${encodeURIComponent(String(flags.since))}`;
|
||||||
|
if (flags.until) qs += `&until=${encodeURIComponent(String(flags.until))}`;
|
||||||
|
if (flags.limit) qs += `&limit=${encodeURIComponent(String(flags.limit))}`;
|
||||||
|
return { method: 'GET', route: `/api/export${qs}` };
|
||||||
|
},
|
||||||
|
activities: (flags) => {
|
||||||
|
const format = optional(flags, 'format', 'json');
|
||||||
|
let qs = `?type=activities&format=${encodeURIComponent(format)}`;
|
||||||
|
if (flags.since) qs += `&since=${encodeURIComponent(String(flags.since))}`;
|
||||||
|
if (flags.until) qs += `&until=${encodeURIComponent(String(flags.until))}`;
|
||||||
|
if (flags.limit) qs += `&limit=${encodeURIComponent(String(flags.limit))}`;
|
||||||
|
return { method: 'GET', route: `/api/export${qs}` };
|
||||||
|
},
|
||||||
|
pipelines: (flags) => {
|
||||||
|
const format = optional(flags, 'format', 'json');
|
||||||
|
let qs = `?type=pipelines&format=${encodeURIComponent(format)}`;
|
||||||
|
if (flags.since) qs += `&since=${encodeURIComponent(String(flags.since))}`;
|
||||||
|
if (flags.until) qs += `&until=${encodeURIComponent(String(flags.until))}`;
|
||||||
|
if (flags.limit) qs += `&limit=${encodeURIComponent(String(flags.limit))}`;
|
||||||
|
return { method: 'GET', route: `/api/export${qs}` };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Events watch (SSE streaming) ---
|
||||||
|
|
||||||
|
async function handleEventsWatch(flags, ctx) {
|
||||||
|
const types = optional(flags, 'types', undefined);
|
||||||
|
let route = '/api/events';
|
||||||
|
if (types) route += `?types=${encodeURIComponent(types)}`;
|
||||||
|
|
||||||
|
if (ctx.asJson) {
|
||||||
|
// JSON mode: one JSON object per line (NDJSON)
|
||||||
|
await sseStream({
|
||||||
|
baseUrl: ctx.baseUrl,
|
||||||
|
apiKey: ctx.apiKey,
|
||||||
|
cookie: ctx.profile.cookie,
|
||||||
|
route,
|
||||||
|
timeoutMs: ctx.timeoutMs,
|
||||||
|
onEvent: (event) => {
|
||||||
|
if (event.type === 'heartbeat') return;
|
||||||
|
console.log(JSON.stringify(event));
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
console.error(JSON.stringify({ ok: false, error: err }));
|
||||||
|
process.exit(EXIT.SERVER);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(`Watching events at ${normalizeBaseUrl(ctx.baseUrl)}${route}`);
|
||||||
|
console.log('Press Ctrl+C to stop.\n');
|
||||||
|
await sseStream({
|
||||||
|
baseUrl: ctx.baseUrl,
|
||||||
|
apiKey: ctx.apiKey,
|
||||||
|
cookie: ctx.profile.cookie,
|
||||||
|
route,
|
||||||
|
timeoutMs: ctx.timeoutMs,
|
||||||
|
onEvent: (event) => {
|
||||||
|
if (event.type === 'heartbeat') return;
|
||||||
|
const ts = event.timestamp ? new Date(event.timestamp).toISOString() : new Date().toISOString();
|
||||||
|
const type = event.type || event.data?.mutation || 'event';
|
||||||
|
console.log(`[${ts}] ${type}: ${JSON.stringify(event.data || event)}`);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
console.error(`SSE error: ${JSON.stringify(err)}`);
|
||||||
|
process.exit(EXIT.SERVER);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return value;
|
process.exit(EXIT.OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Main ---
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const parsed = parseArgs(process.argv.slice(2));
|
const parsed = parseArgs(process.argv.slice(2));
|
||||||
if (parsed.flags.help || parsed._.length === 0) {
|
if (parsed.flags.help || parsed._.length === 0) {
|
||||||
|
|
@ -211,129 +652,68 @@ async function run() {
|
||||||
|
|
||||||
const group = parsed._[0];
|
const group = parsed._[0];
|
||||||
const action = parsed._[1];
|
const action = parsed._[1];
|
||||||
|
// For compound subcommands like: agents memory get / tasks comments add
|
||||||
|
const sub = parsed._[2];
|
||||||
|
|
||||||
|
const ctx = { baseUrl, apiKey, profile, timeoutMs, asJson };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (group === 'auth') {
|
// Raw passthrough
|
||||||
if (action === 'login') {
|
|
||||||
const username = required(parsed.flags, 'username');
|
|
||||||
const password = required(parsed.flags, 'password');
|
|
||||||
const result = await httpRequest({
|
|
||||||
baseUrl,
|
|
||||||
method: 'POST',
|
|
||||||
route: '/api/auth/login',
|
|
||||||
body: { username, password },
|
|
||||||
timeoutMs,
|
|
||||||
});
|
|
||||||
if (result.ok && result.setCookie) {
|
|
||||||
profile.url = baseUrl;
|
|
||||||
profile.cookie = result.setCookie.split(';')[0];
|
|
||||||
if (apiKey) profile.apiKey = apiKey;
|
|
||||||
saveProfile(profile);
|
|
||||||
result.data = { ...result.data, profile: profile.name, saved_cookie: true };
|
|
||||||
}
|
|
||||||
printResult(result, asJson);
|
|
||||||
process.exit(result.ok ? EXIT.OK : mapStatusToExit(result.status));
|
|
||||||
}
|
|
||||||
if (action === 'logout') {
|
|
||||||
const result = await httpRequest({ baseUrl, apiKey, cookie: profile.cookie, method: 'POST', route: '/api/auth/logout', timeoutMs });
|
|
||||||
if (result.ok) {
|
|
||||||
profile.cookie = '';
|
|
||||||
saveProfile(profile);
|
|
||||||
}
|
|
||||||
printResult(result, asJson);
|
|
||||||
process.exit(result.ok ? EXIT.OK : mapStatusToExit(result.status));
|
|
||||||
}
|
|
||||||
if (action === 'whoami') {
|
|
||||||
const result = await httpRequest({ baseUrl, apiKey, cookie: profile.cookie, method: 'GET', route: '/api/auth/me', timeoutMs });
|
|
||||||
printResult(result, asJson);
|
|
||||||
process.exit(result.ok ? EXIT.OK : mapStatusToExit(result.status));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group === 'raw') {
|
if (group === 'raw') {
|
||||||
const method = String(required(parsed.flags, 'method')).toUpperCase();
|
const method = String(required(parsed.flags, 'method')).toUpperCase();
|
||||||
const route = String(required(parsed.flags, 'path'));
|
const route = String(required(parsed.flags, 'path'));
|
||||||
const body = parsed.flags.body ? JSON.parse(String(parsed.flags.body)) : undefined;
|
const body = bodyFromFlags(parsed.flags);
|
||||||
const result = await httpRequest({ baseUrl, apiKey, cookie: profile.cookie, method, route, body, timeoutMs });
|
const result = await httpRequest({ baseUrl, apiKey, cookie: profile.cookie, method, route, body, timeoutMs });
|
||||||
printResult(result, asJson);
|
printResult(result, asJson);
|
||||||
process.exit(result.ok ? EXIT.OK : mapStatusToExit(result.status));
|
process.exit(result.ok ? EXIT.OK : mapStatusToExit(result.status));
|
||||||
}
|
}
|
||||||
|
|
||||||
const map = {
|
// Events watch (SSE)
|
||||||
agents: {
|
|
||||||
list: { method: 'GET', route: '/api/agents' },
|
|
||||||
get: { method: 'GET', route: `/api/agents/${required(parsed.flags, 'id')}` },
|
|
||||||
create: { method: 'POST', route: '/api/agents', body: { name: required(parsed.flags, 'name'), role: required(parsed.flags, 'role') } },
|
|
||||||
update: { method: 'PUT', route: `/api/agents/${required(parsed.flags, 'id')}`, body: parsed.flags.body ? JSON.parse(String(parsed.flags.body)) : {} },
|
|
||||||
delete: { method: 'DELETE', route: `/api/agents/${required(parsed.flags, 'id')}` },
|
|
||||||
wake: { method: 'POST', route: `/api/agents/${required(parsed.flags, 'id')}/wake` },
|
|
||||||
diagnostics: { method: 'GET', route: `/api/agents/${required(parsed.flags, 'id')}/diagnostics` },
|
|
||||||
heartbeat: { method: 'POST', route: `/api/agents/${required(parsed.flags, 'id')}/heartbeat` },
|
|
||||||
},
|
|
||||||
tasks: {
|
|
||||||
list: { method: 'GET', route: '/api/tasks' },
|
|
||||||
get: { method: 'GET', route: `/api/tasks/${required(parsed.flags, 'id')}` },
|
|
||||||
create: { method: 'POST', route: '/api/tasks', body: parsed.flags.body ? JSON.parse(String(parsed.flags.body)) : { title: required(parsed.flags, 'title') } },
|
|
||||||
update: { method: 'PUT', route: `/api/tasks/${required(parsed.flags, 'id')}`, body: parsed.flags.body ? JSON.parse(String(parsed.flags.body)) : {} },
|
|
||||||
delete: { method: 'DELETE', route: `/api/tasks/${required(parsed.flags, 'id')}` },
|
|
||||||
queue: { method: 'GET', route: `/api/tasks/queue?agent=${encodeURIComponent(required(parsed.flags, 'agent'))}${parsed.flags['max-capacity'] ? `&max_capacity=${encodeURIComponent(String(parsed.flags['max-capacity']))}` : ''}` },
|
|
||||||
},
|
|
||||||
sessions: {
|
|
||||||
list: { method: 'GET', route: '/api/sessions' },
|
|
||||||
control: { method: 'POST', route: `/api/sessions/${required(parsed.flags, 'id')}/control`, body: { action: required(parsed.flags, 'action') } },
|
|
||||||
continue: { method: 'POST', route: '/api/sessions/continue', body: { kind: required(parsed.flags, 'kind'), id: required(parsed.flags, 'id'), prompt: required(parsed.flags, 'prompt') } },
|
|
||||||
},
|
|
||||||
connect: {
|
|
||||||
register: { method: 'POST', route: '/api/connect', body: parsed.flags.body ? JSON.parse(String(parsed.flags.body)) : { tool_name: required(parsed.flags, 'tool-name'), agent_name: required(parsed.flags, 'agent-name') } },
|
|
||||||
list: { method: 'GET', route: '/api/connect' },
|
|
||||||
disconnect: { method: 'DELETE', route: '/api/connect', body: { connection_id: required(parsed.flags, 'connection-id') } },
|
|
||||||
},
|
|
||||||
tokens: {
|
|
||||||
list: { method: 'GET', route: '/api/tokens?action=list' },
|
|
||||||
stats: { method: 'GET', route: '/api/tokens?action=stats' },
|
|
||||||
'by-agent': { method: 'GET', route: `/api/tokens/by-agent?days=${encodeURIComponent(String(parsed.flags.days || '30'))}` },
|
|
||||||
},
|
|
||||||
skills: {
|
|
||||||
list: { method: 'GET', route: '/api/skills' },
|
|
||||||
content: { method: 'GET', route: `/api/skills?mode=content&source=${encodeURIComponent(required(parsed.flags, 'source'))}&name=${encodeURIComponent(required(parsed.flags, 'name'))}` },
|
|
||||||
check: { method: 'GET', route: `/api/skills?mode=check&source=${encodeURIComponent(required(parsed.flags, 'source'))}&name=${encodeURIComponent(required(parsed.flags, 'name'))}` },
|
|
||||||
upsert: { method: 'PUT', route: '/api/skills', body: { source: required(parsed.flags, 'source'), name: required(parsed.flags, 'name'), content: fs.readFileSync(required(parsed.flags, 'file'), 'utf8') } },
|
|
||||||
delete: { method: 'DELETE', route: `/api/skills?source=${encodeURIComponent(required(parsed.flags, 'source'))}&name=${encodeURIComponent(required(parsed.flags, 'name'))}` },
|
|
||||||
},
|
|
||||||
cron: {
|
|
||||||
list: { method: 'GET', route: '/api/cron' },
|
|
||||||
create: { method: 'POST', route: '/api/cron', body: parsed.flags.body ? JSON.parse(String(parsed.flags.body)) : {} },
|
|
||||||
update: { method: 'POST', route: '/api/cron', body: parsed.flags.body ? JSON.parse(String(parsed.flags.body)) : {} },
|
|
||||||
pause: { method: 'POST', route: '/api/cron', body: parsed.flags.body ? JSON.parse(String(parsed.flags.body)) : {} },
|
|
||||||
resume: { method: 'POST', route: '/api/cron', body: parsed.flags.body ? JSON.parse(String(parsed.flags.body)) : {} },
|
|
||||||
remove: { method: 'POST', route: '/api/cron', body: parsed.flags.body ? JSON.parse(String(parsed.flags.body)) : {} },
|
|
||||||
run: { method: 'POST', route: '/api/cron', body: parsed.flags.body ? JSON.parse(String(parsed.flags.body)) : {} },
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
watch: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (group === 'events' && action === 'watch') {
|
if (group === 'events' && action === 'watch') {
|
||||||
const result = await httpRequest({ baseUrl, apiKey, cookie: profile.cookie, method: 'GET', route: '/api/events', timeoutMs: Number(parsed.flags['timeout-ms'] || 3600000) });
|
await handleEventsWatch(parsed.flags, { ...ctx, timeoutMs: Number(parsed.flags['timeout-ms'] || 3600000) });
|
||||||
// Basic fallback: if server doesn't stream in this fetch mode, print response payload
|
return;
|
||||||
printResult(result, asJson);
|
|
||||||
process.exit(result.ok ? EXIT.OK : mapStatusToExit(result.status));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cfg = map[group] && map[group][action];
|
// Look up group and action in the commands map
|
||||||
if (!cfg) {
|
const groupMap = commands[group];
|
||||||
|
if (!groupMap) {
|
||||||
|
console.error(`Unknown group: ${group}`);
|
||||||
usage();
|
usage();
|
||||||
process.exit(EXIT.USAGE);
|
process.exit(EXIT.USAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let handler = groupMap[action];
|
||||||
|
if (!handler) {
|
||||||
|
console.error(`Unknown action: ${group} ${action}`);
|
||||||
|
usage();
|
||||||
|
process.exit(EXIT.USAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject sub-command into flags for compound commands (memory, soul, comments)
|
||||||
|
if (sub && typeof handler === 'function') {
|
||||||
|
parsed.flags._sub = sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute handler
|
||||||
|
const result_or_config = await (typeof handler === 'function'
|
||||||
|
? handler(parsed.flags, ctx)
|
||||||
|
: handler);
|
||||||
|
|
||||||
|
// If handler returned an http result directly (auth login/logout)
|
||||||
|
if (result_or_config && 'ok' in result_or_config && 'status' in result_or_config) {
|
||||||
|
printResult(result_or_config, asJson);
|
||||||
|
process.exit(result_or_config.ok ? EXIT.OK : mapStatusToExit(result_or_config.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise it returned { method, route, body? } — execute the request
|
||||||
|
const { method, route, body } = result_or_config;
|
||||||
const result = await httpRequest({
|
const result = await httpRequest({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
apiKey,
|
apiKey,
|
||||||
cookie: profile.cookie,
|
cookie: profile.cookie,
|
||||||
method: cfg.method,
|
method,
|
||||||
route: cfg.route,
|
route,
|
||||||
body: cfg.body,
|
body,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue