feat(cli): scaffold nc CLI with list-groups command

Adds a transport-agnostic CLI control plane shared between three eventual
callers (host shell, Claude in project, container agent) — though only the
host-side socket transport is wired in this commit. Container DB transport
and approval flow land alongside the first risky command.

- src/cli/frame.ts:        wire format (RequestFrame, ResponseFrame, CallerContext)
- src/cli/registry.ts:     command registry with RiskClass
- src/cli/dispatch.ts:     transport-agnostic dispatcher
- src/cli/transport.ts:    Transport interface
- src/cli/socket-client.ts: SocketTransport against data/nc.sock
- src/cli/socket-server.ts: host-side listener (chmod 0600, line-delimited JSON)
- src/cli/format.ts:       human table / --json output modes
- src/cli/client.ts:       `nc` argv -> frame -> transport -> stdout
- src/cli/commands/list-groups.ts: first command (riskClass: safe)
- bin/nc:                  bash launcher (resolves project root via symlink)
- src/index.ts:            start/stop server + import command barrel

`data/nc.sock` is intentionally separate from `data/cli.sock` (which the
existing chat-style channel adapter still owns).

Verified end-to-end: `nc list-groups`, `nc list groups`, `--json`,
unknown-command error, host-down ENOENT message with start instructions.
typecheck clean; eslint reports only the same `no-catch-all` warnings the
rest of the codebase has.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-29 18:03:16 +03:00
parent ae9bcb7c33
commit 3a3d2ee644
12 changed files with 572 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
// Side-effect imports — each command file calls register() at top level.
// Imported by src/index.ts on host startup so the registry is populated
// before the CLI server accepts connections.
import './list-groups.js';

View File

@@ -0,0 +1,17 @@
import { getAllAgentGroups } from '../../db/agent-groups.js';
import { register } from '../registry.js';
register({
name: 'list-groups',
description: 'List all agent groups.',
riskClass: 'safe',
parseArgs: () => ({}),
handler: async () =>
getAllAgentGroups().map((g) => ({
id: g.id,
name: g.name,
folder: g.folder,
provider: g.agent_provider ?? 'claude',
created_at: g.created_at,
})),
});