mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
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:
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# nc — NanoClaw CLI launcher.
|
||||
#
|
||||
# Resolves the project root from this script's location, cd's there so the
|
||||
# host-resolved DATA_DIR matches the running host, and execs the TS entry
|
||||
# via tsx. Symlink this file into a directory on your PATH (or alias `nc`
|
||||
# to its full path) to invoke from anywhere:
|
||||
#
|
||||
# ln -s "$(pwd)/bin/nc" /usr/local/bin/nc
|
||||
# # or
|
||||
# alias nc="$(pwd)/bin/nc"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT="${BASH_SOURCE[0]}"
|
||||
# Resolve symlinks so PROJECT_ROOT points at the real checkout.
|
||||
while [ -h "$SCRIPT" ]; do
|
||||
DIR="$(cd -P "$(dirname "$SCRIPT")" && pwd)"
|
||||
SCRIPT="$(readlink "$SCRIPT")"
|
||||
[[ "$SCRIPT" != /* ]] && SCRIPT="$DIR/$SCRIPT"
|
||||
done
|
||||
SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
exec pnpm exec tsx src/cli/client.ts "$@"
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* `nc` binary entry point.
|
||||
*
|
||||
* Parses argv, builds a request frame, sends it via the picked transport,
|
||||
* formats the response, exits non-zero on error.
|
||||
*
|
||||
* Usage:
|
||||
* nc <command> [--key value ...] [--json]
|
||||
*
|
||||
* Examples:
|
||||
* nc list-groups
|
||||
* nc list groups # space-separated form is auto-joined
|
||||
* nc list-groups --json
|
||||
*/
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
import { formatResponse } from './format.js';
|
||||
import type { RequestFrame } from './frame.js';
|
||||
import { SocketTransport } from './socket-client.js';
|
||||
import type { Transport } from './transport.js';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const argv = process.argv.slice(2);
|
||||
|
||||
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { command, args, json } = parseArgv(argv);
|
||||
const req: RequestFrame = { id: randomUUID(), command, args };
|
||||
const transport: Transport = pickTransport();
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await transport.sendFrame(req);
|
||||
} catch (e) {
|
||||
process.stderr.write(formatTransportError(e));
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
process.stdout.write(formatResponse(res, json ? 'json' : 'human'));
|
||||
process.exit(res.ok ? 0 : 1);
|
||||
}
|
||||
|
||||
function pickTransport(): Transport {
|
||||
// Container DB transport will land alongside the agent-runner change.
|
||||
// For now: host-only — the only callers are a shell user or Claude in
|
||||
// the project.
|
||||
return new SocketTransport();
|
||||
}
|
||||
|
||||
function parseArgv(argv: string[]): {
|
||||
command: string;
|
||||
args: Record<string, unknown>;
|
||||
json: boolean;
|
||||
} {
|
||||
const positional: string[] = [];
|
||||
const args: Record<string, unknown> = {};
|
||||
let json = false;
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === '--json') {
|
||||
json = true;
|
||||
continue;
|
||||
}
|
||||
if (a.startsWith('--')) {
|
||||
const key = a.slice(2);
|
||||
const next = argv[i + 1];
|
||||
if (next === undefined || next.startsWith('--')) {
|
||||
args[key] = true;
|
||||
} else {
|
||||
args[key] = next;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
positional.push(a);
|
||||
}
|
||||
|
||||
if (positional.length === 0) {
|
||||
process.stderr.write('nc: missing command\n');
|
||||
printUsage();
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// Allow `nc list groups` as well as `nc list-groups`. Server rejects
|
||||
// unknowns, so the naive join is safe — at worst the user gets an
|
||||
// unknown-command error.
|
||||
const command =
|
||||
positional.length >= 2 ? `${positional[0]}-${positional[1]}` : positional[0];
|
||||
|
||||
return { command, args, json };
|
||||
}
|
||||
|
||||
function printUsage(): void {
|
||||
process.stdout.write(
|
||||
[
|
||||
'Usage: nc <command> [--key value ...] [--json]',
|
||||
'',
|
||||
'Commands:',
|
||||
' list-groups List all agent groups.',
|
||||
'',
|
||||
'Run `nc <command> --json` for machine-readable output.',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
}
|
||||
|
||||
function formatTransportError(e: unknown): string {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
if (msg.includes('ENOENT') || msg.includes('ECONNREFUSED')) {
|
||||
return [
|
||||
`nc: cannot reach NanoClaw host (${msg}).`,
|
||||
`Is the host running? Start it with: pnpm run dev`,
|
||||
`Or, if installed as a service:`,
|
||||
` macOS: launchctl kickstart -k gui/$(id -u)/com.nanoclaw`,
|
||||
` Linux: systemctl --user restart nanoclaw`,
|
||||
``,
|
||||
].join('\n');
|
||||
}
|
||||
return `nc: transport error: ${msg}\n`;
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(
|
||||
`nc: unexpected error: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
);
|
||||
process.exit(2);
|
||||
});
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
})),
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Transport-agnostic dispatcher. Both the socket server (host caller) and
|
||||
* — once it lands — the per-session DB poller (container caller) call
|
||||
* dispatch() with the same frame and a transport-supplied CallerContext.
|
||||
*
|
||||
* Approval gating for risky calls from the container is the only branch
|
||||
* that differs by caller. Host callers and `safe` commands run inline.
|
||||
*/
|
||||
import type { CallerContext, ErrorCode, RequestFrame, ResponseFrame } from './frame.js';
|
||||
import { lookup } from './registry.js';
|
||||
|
||||
export async function dispatch(
|
||||
req: RequestFrame,
|
||||
ctx: CallerContext,
|
||||
): Promise<ResponseFrame> {
|
||||
const cmd = lookup(req.command);
|
||||
if (!cmd) {
|
||||
return err(req.id, 'unknown-command', `no command "${req.command}"`);
|
||||
}
|
||||
|
||||
// Agent + risky → approval flow. Wired alongside the first risky command;
|
||||
// until then, return a clear pending-shaped error so the contract is visible.
|
||||
if (ctx.caller !== 'host' && cmd.riskClass !== 'safe') {
|
||||
return err(
|
||||
req.id,
|
||||
'approval-pending',
|
||||
'Approval flow not yet wired. (Will be added when the first risky command lands.)',
|
||||
);
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = cmd.parseArgs(req.args);
|
||||
} catch (e) {
|
||||
return err(req.id, 'invalid-args', errMsg(e));
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await cmd.handler(parsed, ctx);
|
||||
return { id: req.id, ok: true, data };
|
||||
} catch (e) {
|
||||
return err(req.id, 'handler-error', errMsg(e));
|
||||
}
|
||||
}
|
||||
|
||||
function err(id: string, code: ErrorCode, message: string): ResponseFrame {
|
||||
return { id, ok: false, error: { code, message } };
|
||||
}
|
||||
|
||||
function errMsg(e: unknown): string {
|
||||
return e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Output formatting for the `nc` binary. Two modes:
|
||||
* - human (default): a small auto-table for arrays of flat records,
|
||||
* JSON.stringify for everything else, plain "error: ..." line for !ok.
|
||||
* - json: the response frame, pretty-printed.
|
||||
*
|
||||
* The MCP / agent side will always pass --json so it parses the frame
|
||||
* itself. The DB transport (when it lands) skips this layer entirely —
|
||||
* the agent sees frames directly.
|
||||
*/
|
||||
import type { ResponseFrame } from './frame.js';
|
||||
|
||||
export type FormatMode = 'human' | 'json';
|
||||
|
||||
export function formatResponse(res: ResponseFrame, mode: FormatMode): string {
|
||||
if (mode === 'json') return JSON.stringify(res, null, 2) + '\n';
|
||||
|
||||
if (!res.ok) {
|
||||
return `error (${res.error.code}): ${res.error.message}\n`;
|
||||
}
|
||||
return formatHuman(res.data) + '\n';
|
||||
}
|
||||
|
||||
function formatHuman(data: unknown): string {
|
||||
if (data === null || data === undefined) return '';
|
||||
if (typeof data === 'string') return data;
|
||||
if (Array.isArray(data) && data.every(isFlatRecord)) {
|
||||
return renderTable(data as Record<string, unknown>[]);
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
function isFlatRecord(x: unknown): x is Record<string, unknown> {
|
||||
if (!x || typeof x !== 'object') return false;
|
||||
for (const v of Object.values(x as Record<string, unknown>)) {
|
||||
if (v !== null && typeof v === 'object') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderTable(rows: Record<string, unknown>[]): string {
|
||||
if (rows.length === 0) return '(no rows)';
|
||||
const cols = Object.keys(rows[0]);
|
||||
const widths = cols.map((c) =>
|
||||
Math.max(c.length, ...rows.map((r) => String(r[c] ?? '').length)),
|
||||
);
|
||||
const fmtRow = (vals: string[]): string =>
|
||||
vals.map((v, i) => v.padEnd(widths[i])).join(' ');
|
||||
const lines = [
|
||||
fmtRow(cols),
|
||||
fmtRow(widths.map((w) => '─'.repeat(w))),
|
||||
...rows.map((r) => fmtRow(cols.map((c) => String(r[c] ?? '')))),
|
||||
];
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Wire format shared between the socket transport (host caller) and — when
|
||||
* it lands — the DB transport (container agent caller).
|
||||
*
|
||||
* Same JSON whether it goes over a socket as a line or sits in a
|
||||
* `frame_json TEXT` column on a session DB. Caller identity is NOT carried
|
||||
* in the frame — it's filled in by whichever server-side adapter received
|
||||
* the bytes (see CallerContext).
|
||||
*/
|
||||
|
||||
export type RequestFrame = {
|
||||
/** Correlation key set by the client. */
|
||||
id: string;
|
||||
/** Registry name, e.g. "list-groups". */
|
||||
command: string;
|
||||
/** Command-specific. Each command's parseArgs validates. */
|
||||
args: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ResponseFrame =
|
||||
| { id: string; ok: true; data: unknown }
|
||||
| { id: string; ok: false; error: { code: ErrorCode; message: string } };
|
||||
|
||||
export type ErrorCode =
|
||||
| 'unknown-command'
|
||||
| 'invalid-args'
|
||||
| 'permission-denied'
|
||||
| 'approval-pending'
|
||||
| 'not-found'
|
||||
| 'handler-error'
|
||||
| 'transport-error';
|
||||
|
||||
/**
|
||||
* Filled in by the transport adapter on the server side. Handlers read
|
||||
* caller identity from here, never from the frame.
|
||||
*/
|
||||
export type CallerContext =
|
||||
| { caller: 'host' }
|
||||
| {
|
||||
caller: 'agent';
|
||||
sessionId: string;
|
||||
agentGroupId: string;
|
||||
messagingGroupId: string;
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Command registry — single source of truth for what `nc` can do.
|
||||
*
|
||||
* Each command file under `commands/` calls `register()` at top level,
|
||||
* and `commands/index.ts` imports them all for side effects so the
|
||||
* registry is populated before the host's CLI server accepts connections.
|
||||
*/
|
||||
import type { CallerContext } from './frame.js';
|
||||
|
||||
export type RiskClass = 'safe' | 'requires-admin' | 'requires-owner';
|
||||
|
||||
export type CommandDef<TArgs = unknown, TData = unknown> = {
|
||||
name: string;
|
||||
description: string;
|
||||
riskClass: RiskClass;
|
||||
/** Validates `frame.args` and produces the typed handler input. Throws on invalid. */
|
||||
parseArgs: (raw: Record<string, unknown>) => TArgs;
|
||||
handler: (args: TArgs, ctx: CallerContext) => Promise<TData>;
|
||||
};
|
||||
|
||||
const registry = new Map<string, CommandDef>();
|
||||
|
||||
export function register<TArgs, TData>(def: CommandDef<TArgs, TData>): void {
|
||||
if (registry.has(def.name)) {
|
||||
throw new Error(`CLI command "${def.name}" already registered`);
|
||||
}
|
||||
registry.set(def.name, def as CommandDef);
|
||||
}
|
||||
|
||||
export function lookup(name: string): CommandDef | undefined {
|
||||
return registry.get(name);
|
||||
}
|
||||
|
||||
export function listCommands(): CommandDef[] {
|
||||
return [...registry.values()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* SocketTransport — client side. Used by the `nc` binary when running on
|
||||
* the host (i.e. invoked from a shell or by Claude in the project).
|
||||
*
|
||||
* Wire format: line-delimited JSON. One request per connection; the server
|
||||
* writes one response and closes.
|
||||
*/
|
||||
import net from 'net';
|
||||
import path from 'path';
|
||||
|
||||
import { DATA_DIR } from '../config.js';
|
||||
import type { RequestFrame, ResponseFrame } from './frame.js';
|
||||
import type { Transport } from './transport.js';
|
||||
|
||||
export const DEFAULT_SOCKET_PATH = path.join(DATA_DIR, 'nc.sock');
|
||||
|
||||
export class SocketTransport implements Transport {
|
||||
constructor(private readonly socketPath: string = DEFAULT_SOCKET_PATH) {}
|
||||
|
||||
async sendFrame(req: RequestFrame): Promise<ResponseFrame> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = net.createConnection(this.socketPath);
|
||||
let buffer = '';
|
||||
let settled = false;
|
||||
|
||||
const settle = (
|
||||
action: 'resolve' | 'reject',
|
||||
valueOrErr: ResponseFrame | Error,
|
||||
): void => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
try {
|
||||
client.end();
|
||||
} catch (_e) {
|
||||
// best-effort
|
||||
}
|
||||
if (action === 'resolve') resolve(valueOrErr as ResponseFrame);
|
||||
else reject(valueOrErr as Error);
|
||||
};
|
||||
|
||||
client.on('connect', () => {
|
||||
client.write(JSON.stringify(req) + '\n');
|
||||
});
|
||||
|
||||
client.on('data', (chunk) => {
|
||||
buffer += chunk.toString('utf8');
|
||||
const idx = buffer.indexOf('\n');
|
||||
if (idx < 0) return;
|
||||
const line = buffer.slice(0, idx);
|
||||
try {
|
||||
const frame = JSON.parse(line) as ResponseFrame;
|
||||
settle('resolve', frame);
|
||||
} catch (e) {
|
||||
settle(
|
||||
'reject',
|
||||
new Error(
|
||||
`malformed response from host: ${e instanceof Error ? e.message : String(e)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
client.on('error', (err) => settle('reject', err));
|
||||
client.on('close', () => {
|
||||
if (!settled) {
|
||||
settle('reject', new Error('host closed connection before sending response'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Host-side socket listener. Started from src/index.ts, accepts one frame
|
||||
* per connection, calls dispatch() with caller='host', writes the response
|
||||
* frame, closes.
|
||||
*
|
||||
* Lives at data/nc.sock (separate from data/cli.sock, which the existing
|
||||
* chat-style CLI channel adapter owns). Socket file is chmod 0600 — only
|
||||
* the user that started the host can connect.
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import net from 'net';
|
||||
|
||||
import { log } from '../log.js';
|
||||
import { dispatch } from './dispatch.js';
|
||||
import type { CallerContext, RequestFrame, ResponseFrame } from './frame.js';
|
||||
import { DEFAULT_SOCKET_PATH } from './socket-client.js';
|
||||
|
||||
let server: net.Server | null = null;
|
||||
|
||||
export async function startCliServer(socketPath: string = DEFAULT_SOCKET_PATH): Promise<void> {
|
||||
// Stale-socket cleanup — a previous run that crashed may have left the
|
||||
// file behind, and net.createServer refuses to bind to an existing path.
|
||||
try {
|
||||
fs.unlinkSync(socketPath);
|
||||
} catch (err) {
|
||||
const e = err as NodeJS.ErrnoException;
|
||||
if (e.code !== 'ENOENT') {
|
||||
log.warn('Failed to unlink stale nc socket (will try to bind anyway)', { socketPath, err });
|
||||
}
|
||||
}
|
||||
|
||||
const s = net.createServer((conn) => handleConnection(conn));
|
||||
server = s;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
s.once('error', reject);
|
||||
s.listen(socketPath, () => {
|
||||
try {
|
||||
fs.chmodSync(socketPath, 0o600);
|
||||
} catch (err) {
|
||||
log.warn('Failed to chmod nc socket (continuing)', { socketPath, err });
|
||||
}
|
||||
log.info('nc CLI server listening', { socketPath });
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function stopCliServer(): Promise<void> {
|
||||
if (!server) return;
|
||||
const s = server;
|
||||
server = null;
|
||||
await new Promise<void>((resolve) => s.close(() => resolve()));
|
||||
}
|
||||
|
||||
function handleConnection(conn: net.Socket): void {
|
||||
let buffer = '';
|
||||
conn.on('data', (chunk) => {
|
||||
buffer += chunk.toString('utf8');
|
||||
let idx: number;
|
||||
while ((idx = buffer.indexOf('\n')) >= 0) {
|
||||
const line = buffer.slice(0, idx).trim();
|
||||
buffer = buffer.slice(idx + 1);
|
||||
if (!line) continue;
|
||||
void handleFrame(conn, line);
|
||||
}
|
||||
});
|
||||
conn.on('error', (err) => {
|
||||
log.warn('nc CLI server connection error', { err });
|
||||
});
|
||||
}
|
||||
|
||||
async function handleFrame(conn: net.Socket, line: string): Promise<void> {
|
||||
let req: RequestFrame;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(line);
|
||||
if (!isRequestFrame(parsed)) throw new Error('bad request shape');
|
||||
req = parsed;
|
||||
} catch (e) {
|
||||
write(conn, {
|
||||
id: 'unknown',
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'transport-error',
|
||||
message: `bad frame: ${e instanceof Error ? e.message : String(e)}`,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Host caller — connecting to data/nc.sock requires file-system access
|
||||
// to a 0600 socket owned by the host user, so we treat the socket path
|
||||
// itself as the auth boundary.
|
||||
const ctx: CallerContext = { caller: 'host' };
|
||||
const res = await dispatch(req, ctx);
|
||||
write(conn, res);
|
||||
}
|
||||
|
||||
function write(conn: net.Socket, frame: ResponseFrame): void {
|
||||
try {
|
||||
conn.write(JSON.stringify(frame) + '\n');
|
||||
conn.end();
|
||||
} catch (err) {
|
||||
log.warn('Failed to write nc CLI response', { err });
|
||||
}
|
||||
}
|
||||
|
||||
function isRequestFrame(x: unknown): x is RequestFrame {
|
||||
if (!x || typeof x !== 'object') return false;
|
||||
const o = x as Record<string, unknown>;
|
||||
return (
|
||||
typeof o.id === 'string' &&
|
||||
typeof o.command === 'string' &&
|
||||
typeof o.args === 'object' &&
|
||||
o.args !== null
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Client-side transport interface. The `nc` binary picks one of these and
|
||||
* calls sendFrame; the caller doesn't know whether bytes traveled over a
|
||||
* Unix socket (host) or through outbound.db / inbound.db rows (container).
|
||||
*/
|
||||
import type { RequestFrame, ResponseFrame } from './frame.js';
|
||||
|
||||
export interface Transport {
|
||||
sendFrame(req: RequestFrame): Promise<ResponseFrame>;
|
||||
}
|
||||
@@ -52,6 +52,11 @@ import './channels/index.js';
|
||||
// append registry-based modules. Imported for side effects (registrations).
|
||||
import './modules/index.js';
|
||||
|
||||
// CLI command barrel — populates the `nc` registry before the CLI server
|
||||
// accepts connections.
|
||||
import './cli/commands/index.js';
|
||||
import { startCliServer, stopCliServer } from './cli/socket-server.js';
|
||||
|
||||
import type { ChannelAdapter, ChannelSetup } from './channels/adapter.js';
|
||||
import { initChannelAdapters, teardownChannelAdapters, getChannelAdapter } from './channels/channel-registry.js';
|
||||
|
||||
@@ -159,6 +164,9 @@ async function main(): Promise<void> {
|
||||
startHostSweep();
|
||||
log.info('Host sweep started');
|
||||
|
||||
// 7. Start the `nc` CLI socket server (data/nc.sock).
|
||||
await startCliServer();
|
||||
|
||||
log.info('NanoClaw running');
|
||||
}
|
||||
|
||||
@@ -174,6 +182,7 @@ async function shutdown(signal: string): Promise<void> {
|
||||
}
|
||||
stopDeliveryPolls();
|
||||
stopHostSweep();
|
||||
await stopCliServer();
|
||||
await teardownChannelAdapters();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user