network-ai 4.13.1 → 4.14.0

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/bin/console.ts ADDED
@@ -0,0 +1,769 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * network-ai console — Interactive terminal dashboard for the agent runtime.
4
+ *
5
+ * Usage:
6
+ * npx network-ai-console [options]
7
+ * npx network-ai console (via CLI sub-command)
8
+ *
9
+ * Options:
10
+ * --base-path <dir> Base directory for agent sandbox (default: cwd)
11
+ * --auto-approve Auto-approve all operations (DANGEROUS)
12
+ * --allow <patterns> Comma-separated command whitelist (e.g. "npm *,node *,git status")
13
+ * --budget <tokens> Budget ceiling in tokens (default: 100000)
14
+ * --pipe Pipe mode: read JSON commands from stdin, write JSON to stdout
15
+ * --board <name> Named blackboard to use (default: main)
16
+ */
17
+
18
+ import * as path from 'path';
19
+ import { ConsoleUI } from '../lib/console-ui';
20
+ import {
21
+ AgentRuntime,
22
+ RuntimePolicyError,
23
+ RuntimeApprovalError,
24
+ } from '../lib/agent-runtime';
25
+ import { LockedBlackboard } from '../lib/locked-blackboard';
26
+ import { FederatedBudget } from '../lib/federated-budget';
27
+ import { JourneyFSM } from '../lib/fsm-journey';
28
+ import { AdapterRegistry } from '../adapters/adapter-registry';
29
+ import { createSwarmOrchestrator } from '../index';
30
+
31
+ // ── Parse args ────────────────────────────────────────────────────────────────
32
+
33
+ function parseArgs(argv: string[]): {
34
+ basePath: string;
35
+ autoApprove: boolean;
36
+ allowedCommands: string[];
37
+ budgetCeiling: number;
38
+ pipe: boolean;
39
+ board: string;
40
+ } {
41
+ let basePath = process.cwd();
42
+ let autoApprove = false;
43
+ let allowedCommands: string[] = ['npm *', 'node *', 'npx *', 'git status', 'git diff*', 'git log*', 'ls*', 'dir*', 'cat *', 'type *', 'echo *'];
44
+ let budgetCeiling = 100_000;
45
+ let pipe = false;
46
+ let board = 'main';
47
+
48
+ for (let i = 0; i < argv.length; i++) {
49
+ const arg = argv[i];
50
+ if (arg === '--base-path' && argv[i + 1]) {
51
+ basePath = path.resolve(argv[++i]);
52
+ } else if (arg === '--auto-approve') {
53
+ autoApprove = true;
54
+ } else if (arg === '--allow' && argv[i + 1]) {
55
+ allowedCommands = argv[++i].split(',').map(s => s.trim());
56
+ } else if (arg === '--budget' && argv[i + 1]) {
57
+ budgetCeiling = parseInt(argv[++i], 10) || 100_000;
58
+ } else if (arg === '--pipe') {
59
+ pipe = true;
60
+ } else if (arg === '--board' && argv[i + 1]) {
61
+ board = argv[++i];
62
+ }
63
+ }
64
+
65
+ return { basePath, autoApprove, allowedCommands, budgetCeiling, pipe, board };
66
+ }
67
+
68
+ // ── Version ───────────────────────────────────────────────────────────────────
69
+
70
+ const pkg = (() => {
71
+ try { return require('../package.json'); } catch {
72
+ try { return require('../../package.json'); } catch { return { version: '0.0.0' }; }
73
+ }
74
+ })() as { version: string };
75
+
76
+ // ── Main ──────────────────────────────────────────────────────────────────────
77
+
78
+ async function main(): Promise<void> {
79
+ const args = parseArgs(process.argv.slice(2));
80
+
81
+ // In pipe mode, redirect console.log to stderr so stdout is reserved for JSON
82
+ if (args.pipe) {
83
+ console.log = (...a: unknown[]) => process.stderr.write(a.join(' ') + '\n');
84
+ }
85
+
86
+ // ── Shared Orchestrator — same instances as MCP server ───────────────────
87
+
88
+ const orchestrator = createSwarmOrchestrator();
89
+ const blackboard = new LockedBlackboard(args.basePath);
90
+ const budget = new FederatedBudget({ ceiling: args.budgetCeiling });
91
+ const adapters = orchestrator.adapters;
92
+
93
+ // JourneyFSM requires at least one state — use a sensible default
94
+ const fsm = new JourneyFSM({
95
+ states: [
96
+ { name: 'IDLE', authorizedAgents: ['*'], authorizedTools: { '*': ['*'] } },
97
+ { name: 'PLANNING', authorizedAgents: ['*'], authorizedTools: { '*': ['*'] } },
98
+ { name: 'EXECUTING', authorizedAgents: ['*'], authorizedTools: { '*': ['*'] } },
99
+ { name: 'REVIEWING', authorizedAgents: ['*'], authorizedTools: { '*': ['*'] } },
100
+ { name: 'DONE', authorizedAgents: ['orchestrator'], authorizedTools: { orchestrator: ['*'] } },
101
+ ],
102
+ transitions: [
103
+ { from: 'IDLE', event: 'plan', to: 'PLANNING', allowedBy: '*' },
104
+ { from: 'PLANNING', event: 'execute', to: 'EXECUTING', allowedBy: '*' },
105
+ { from: 'EXECUTING', event: 'review', to: 'REVIEWING', allowedBy: '*' },
106
+ { from: 'REVIEWING', event: 'approve', to: 'DONE', allowedBy: '*' },
107
+ { from: 'REVIEWING', event: 'revise', to: 'EXECUTING', allowedBy: '*' },
108
+ { from: 'DONE', event: 'reset', to: 'IDLE', allowedBy: '*' },
109
+ ],
110
+ initialState: 'IDLE',
111
+ });
112
+
113
+ // Create the runtime
114
+ const runtime = new AgentRuntime({
115
+ policy: {
116
+ basePath: args.basePath,
117
+ allowedCommands: args.allowedCommands,
118
+ allowedPaths: ['.'],
119
+ autoApproveReads: true,
120
+ },
121
+ autoApproveAll: args.autoApprove,
122
+ onApproval: args.autoApprove ? undefined : async (req) => {
123
+ // Interactive approval via console
124
+ ui.log(`APPROVAL NEEDED: [${req.type}] ${req.target} (risk: ${req.risk})`, 'approval');
125
+ ui.log(`Type 'approve' or 'deny <reason>'`, 'approval');
126
+
127
+ // Store pending approval for the approve/deny commands
128
+ pendingApproval = {
129
+ resolve: (decision) => decision,
130
+ request: req,
131
+ };
132
+
133
+ return new Promise<{ approved: boolean; approvedBy?: string; reason?: string }>((resolve) => {
134
+ pendingApproval = { resolve, request: req };
135
+ });
136
+ },
137
+ });
138
+
139
+ let pendingApproval: {
140
+ resolve: (decision: { approved: boolean; approvedBy?: string; reason?: string }) => void;
141
+ request: { type: string; target: string; agentId: string };
142
+ } | null = null;
143
+
144
+ // Create the console UI
145
+ const ui = new ConsoleUI({
146
+ title: 'Network-AI',
147
+ version: pkg.version,
148
+ prompt: '> ',
149
+ });
150
+
151
+ // ── Register commands ─────────────────────────────────────────────────────
152
+
153
+ ui.command('status', () => {
154
+ // Sync status from real orchestrator components
155
+ const adapterList = adapters.listAdapters();
156
+ const readyCount = adapterList.filter(a => a.ready).length;
157
+ const spent = budget.getTotalSpent();
158
+ const ceiling = budget.getCeiling();
159
+ const pct = ceiling > 0 ? Math.round((spent / ceiling) * 100) : 0;
160
+
161
+ ui.updateStatus({
162
+ agents: { active: readyCount, total: adapterList.length },
163
+ budget: { usedPercent: pct },
164
+ fsm: { state: fsm.state },
165
+ });
166
+
167
+ const s = ui.getStatus();
168
+ const audit = runtime.getAuditLog();
169
+ ui.log(`Agents: ${s.agents.active}/${s.agents.total} | Budget: ${spent.toLocaleString()}/${ceiling.toLocaleString()} (${pct}%) | FSM: ${fsm.state}`);
170
+ ui.log(`Blackboard keys: ${blackboard.listKeys().length} | Pending changes: ${blackboard.listPendingChanges().length}`);
171
+ ui.log(`Audit entries: ${audit.length} | Shell processes: ${runtime.shell.running}`);
172
+ ui.log(`Pending approvals: ${s.pendingApprovals}`);
173
+ }, 'Show runtime status');
174
+
175
+ ui.command('exec', async (cmdArgs) => {
176
+ if (!cmdArgs) { ui.log('Usage: exec <command>', 'warn'); return; }
177
+ try {
178
+ const result = await runtime.exec(cmdArgs, 'console-user');
179
+ if (result.stdout) ui.log(result.stdout.trim());
180
+ if (result.stderr) ui.log(result.stderr.trim(), 'warn');
181
+ ui.log(`Exit ${result.exitCode} (${result.durationMs}ms)${result.timedOut ? ' [TIMED OUT]' : ''}`, result.exitCode === 0 ? 'success' : 'error');
182
+ } catch (err) {
183
+ if (err instanceof RuntimePolicyError) {
184
+ ui.log(err.message, 'error');
185
+ } else if (err instanceof RuntimeApprovalError) {
186
+ ui.log(err.message, 'warn');
187
+ } else {
188
+ ui.log(String(err), 'error');
189
+ }
190
+ }
191
+ }, 'Execute a shell command (exec <cmd>)');
192
+
193
+ ui.command('read', async (filePath) => {
194
+ if (!filePath) { ui.log('Usage: read <file>', 'warn'); return; }
195
+ const result = await runtime.readFile(filePath.trim(), 'console-user');
196
+ if (result.success && result.content) {
197
+ ui.log(`── ${result.path} ──`);
198
+ const lines = result.content.split('\n');
199
+ const preview = lines.slice(0, 30).join('\n');
200
+ ui.log(preview);
201
+ if (lines.length > 30) ui.log(`... (${lines.length - 30} more lines)`, 'info');
202
+ } else {
203
+ ui.log(result.error ?? 'Failed to read file', 'error');
204
+ }
205
+ }, 'Read a file (read <path>)');
206
+
207
+ ui.command('ls', async (dirPath) => {
208
+ const target = dirPath?.trim() || '.';
209
+ const result = await runtime.listDir(target, 'console-user');
210
+ if (result.success && result.entries) {
211
+ ui.log(`── ${result.path} ──`);
212
+ for (const entry of result.entries) {
213
+ ui.log(` ${entry}`);
214
+ }
215
+ } else {
216
+ ui.log(result.error ?? 'Failed to list directory', 'error');
217
+ }
218
+ }, 'List directory contents (ls [path])');
219
+
220
+ ui.command('approve', (_args) => {
221
+ if (!pendingApproval) {
222
+ ui.log('No pending approval', 'warn');
223
+ return;
224
+ }
225
+ const req = pendingApproval.request;
226
+ pendingApproval.resolve({ approved: true, approvedBy: 'console-user' });
227
+ pendingApproval = null;
228
+ ui.log(`Approved: ${req.type} ${req.target}`, 'success');
229
+ ui.updateStatus({ pendingApprovals: Math.max(0, ui.getStatus().pendingApprovals - 1) });
230
+ }, 'Approve a pending operation');
231
+
232
+ ui.command('deny', (reason) => {
233
+ if (!pendingApproval) {
234
+ ui.log('No pending approval', 'warn');
235
+ return;
236
+ }
237
+ const req = pendingApproval.request;
238
+ pendingApproval.resolve({ approved: false, reason: reason || 'Denied by operator' });
239
+ pendingApproval = null;
240
+ ui.log(`Denied: ${req.type} ${req.target} — ${reason || 'no reason'}`, 'warn');
241
+ ui.updateStatus({ pendingApprovals: Math.max(0, ui.getStatus().pendingApprovals - 1) });
242
+ }, 'Deny a pending operation (deny [reason])');
243
+
244
+ ui.command('audit', (args) => {
245
+ const count = parseInt(args) || 10;
246
+ const entries = runtime.getAuditLog();
247
+ const recent = entries.slice(-count);
248
+ if (recent.length === 0) {
249
+ ui.log('No audit entries yet', 'info');
250
+ return;
251
+ }
252
+ for (const e of recent) {
253
+ const time = e.timestamp.split('T')[1]?.slice(0, 8) ?? '';
254
+ ui.log(`[${time}] ${e.action} | ${e.agentId} | ${e.target} → ${e.result}`);
255
+ }
256
+ }, 'Show recent audit entries (audit [count])');
257
+
258
+ ui.command('policy', (args) => {
259
+ const config = runtime.policy.getConfig();
260
+ if (!args || args === 'show') {
261
+ ui.log(`Base path: ${config.basePath}`);
262
+ ui.log(`Allowed commands: ${config.allowedCommands.join(', ') || '(none)'}`);
263
+ ui.log(`Blocked commands: ${config.blockedCommands.length} patterns`);
264
+ ui.log(`Allowed paths: ${config.allowedPaths.join(', ')}`);
265
+ ui.log(`Max concurrent: ${config.maxConcurrentProcesses}`);
266
+ ui.log(`Timeout: ${config.defaultTimeoutMs}ms`);
267
+ ui.log(`Auto-approve reads: ${config.autoApproveReads}`);
268
+ } else if (args.startsWith('allow ')) {
269
+ const pattern = args.slice(6).trim();
270
+ runtime.policy.allowCommand(pattern);
271
+ ui.log(`Added to allowed commands: ${pattern}`, 'success');
272
+ } else if (args.startsWith('block ')) {
273
+ const pattern = args.slice(6).trim();
274
+ runtime.policy.disallowCommand(pattern);
275
+ ui.log(`Removed from allowed commands: ${pattern}`, 'success');
276
+ } else {
277
+ ui.log('Usage: policy [show|allow <pattern>|block <pattern>]', 'warn');
278
+ }
279
+ }, 'View or modify sandbox policy');
280
+
281
+ // ── Orchestrator commands ─────────────────────────────────────────────────
282
+
283
+ ui.command('agents', async () => {
284
+ const adapterList = adapters.listAdapters();
285
+ if (adapterList.length === 0) {
286
+ ui.log('No adapters registered. Use "spawn" to execute agents via adapters.', 'info');
287
+ return;
288
+ }
289
+ ui.log(`── Registered Adapters (${adapterList.length}) ──`);
290
+ for (const a of adapterList) {
291
+ const status = a.ready ? 'ready' : (a.deferred ? 'deferred' : 'not ready');
292
+ ui.log(` ${a.name} v${a.version} [${status}]`);
293
+ }
294
+
295
+ try {
296
+ const discovered = await adapters.discoverAgents();
297
+ if (discovered.length > 0) {
298
+ ui.log(`── Discovered Agents (${discovered.length}) ──`);
299
+ for (const agent of discovered) {
300
+ ui.log(` ${agent.id} (${agent.adapter}) — ${agent.description ?? 'no description'}`);
301
+ }
302
+ }
303
+ } catch {
304
+ // discoverAgents may fail if no adapters are initialized
305
+ }
306
+ }, 'List registered adapters and discovered agents');
307
+
308
+ ui.command('spawn', async (spawnArgs) => {
309
+ if (!spawnArgs) { ui.log('Usage: spawn <agentId> [input text]', 'warn'); return; }
310
+ const [agentId, ...rest] = spawnArgs.split(/\s+/);
311
+ const input = rest.join(' ') || 'execute';
312
+
313
+ ui.log(`Spawning agent "${agentId}"...`, 'info');
314
+ try {
315
+ const result = await adapters.executeAgent(
316
+ agentId,
317
+ { action: 'execute', params: { input } },
318
+ { agentId: 'console-user', taskId: `console-${Date.now()}`, sessionId: 'console' },
319
+ );
320
+ ui.log(`Agent "${agentId}" completed — success: ${result.success}`, result.success ? 'success' : 'error');
321
+ if (result.data) {
322
+ const text = typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2);
323
+ const lines = text.split('\n');
324
+ const preview = lines.slice(0, 20).join('\n');
325
+ ui.log(preview);
326
+ if (lines.length > 20) ui.log(`... (${lines.length - 20} more lines)`, 'info');
327
+ }
328
+ if (result.error) ui.log(`Error: ${result.error.message}`, 'error');
329
+ } catch (err) {
330
+ ui.log(`Spawn failed: ${err instanceof Error ? err.message : String(err)}`, 'error');
331
+ }
332
+ }, 'Execute an agent (spawn <agentId> [input])');
333
+
334
+ ui.command('stop', async (adapterName) => {
335
+ if (!adapterName) { ui.log('Usage: stop <adapterName>', 'warn'); return; }
336
+ try {
337
+ await adapters.removeAdapter(adapterName.trim());
338
+ ui.log(`Adapter "${adapterName.trim()}" removed`, 'success');
339
+ } catch (err) {
340
+ ui.log(`Stop failed: ${err instanceof Error ? err.message : String(err)}`, 'error');
341
+ }
342
+ }, 'Remove an adapter (stop <adapterName>)');
343
+
344
+ ui.command('bb', (bbArgs) => {
345
+ if (!bbArgs) { ui.log('Usage: bb <read|write|list|delete|propose|validate|commit|pending> [args]', 'warn'); return; }
346
+ const parts = bbArgs.split(/\s+/);
347
+ const sub = parts[0];
348
+
349
+ if (sub === 'list') {
350
+ const keys = blackboard.listKeys();
351
+ if (keys.length === 0) { ui.log('Blackboard is empty', 'info'); return; }
352
+ ui.log(`── Blackboard Keys (${keys.length}) ──`);
353
+ for (const key of keys) ui.log(` ${key}`);
354
+ } else if (sub === 'read') {
355
+ const key = parts[1];
356
+ if (!key) { ui.log('Usage: bb read <key>', 'warn'); return; }
357
+ const entry = blackboard.read(key);
358
+ if (!entry) { ui.log(`Key "${key}" not found`, 'warn'); return; }
359
+ ui.log(`── ${key} ──`);
360
+ ui.log(` Value: ${JSON.stringify(entry.value)}`);
361
+ ui.log(` Source: ${entry.source_agent} | Updated: ${entry.timestamp}`);
362
+ } else if (sub === 'write') {
363
+ const key = parts[1];
364
+ const value = parts.slice(2).join(' ');
365
+ if (!key || !value) { ui.log('Usage: bb write <key> <value>', 'warn'); return; }
366
+ let parsed: unknown;
367
+ try { parsed = JSON.parse(value); } catch { parsed = value; }
368
+ blackboard.write(key, parsed, 'console-user');
369
+ ui.log(`Wrote "${key}" to blackboard`, 'success');
370
+ } else if (sub === 'delete') {
371
+ const key = parts[1];
372
+ if (!key) { ui.log('Usage: bb delete <key>', 'warn'); return; }
373
+ const deleted = blackboard.delete(key);
374
+ ui.log(deleted ? `Deleted "${key}"` : `Key "${key}" not found`, deleted ? 'success' : 'warn');
375
+ } else if (sub === 'propose') {
376
+ const key = parts[1];
377
+ const value = parts.slice(2).join(' ');
378
+ if (!key || !value) { ui.log('Usage: bb propose <key> <value>', 'warn'); return; }
379
+ let parsed: unknown;
380
+ try { parsed = JSON.parse(value); } catch { parsed = value; }
381
+ const changeId = blackboard.propose(key, parsed, 'console-user');
382
+ ui.log(`Proposed change: ${changeId}`, 'success');
383
+ } else if (sub === 'validate') {
384
+ const changeId = parts[1];
385
+ if (!changeId) { ui.log('Usage: bb validate <changeId>', 'warn'); return; }
386
+ const valid = blackboard.validate(changeId, 'console-user');
387
+ ui.log(`Validation: ${valid ? 'passed' : 'failed'}`, valid ? 'success' : 'error');
388
+ } else if (sub === 'commit') {
389
+ const changeId = parts[1];
390
+ if (!changeId) { ui.log('Usage: bb commit <changeId>', 'warn'); return; }
391
+ const result = blackboard.commit(changeId);
392
+ ui.log(`Commit: ${result.success ? 'success' : 'failed'}${result.message ? ' — ' + result.message : ''}`, result.success ? 'success' : 'error');
393
+ } else if (sub === 'pending') {
394
+ const pending = blackboard.listPendingChanges();
395
+ if (pending.length === 0) { ui.log('No pending changes', 'info'); return; }
396
+ ui.log(`── Pending Changes (${pending.length}) ──`);
397
+ for (const p of pending) {
398
+ ui.log(` ${p.change_id}: ${p.key} by ${p.source_agent} (${p.status})`);
399
+ }
400
+ } else {
401
+ ui.log('Usage: bb <read|write|list|delete|propose|validate|commit|pending> [args]', 'warn');
402
+ }
403
+ }, 'Blackboard operations (bb <sub> [args])');
404
+
405
+ ui.command('budget', (budgetArgs) => {
406
+ const sub = budgetArgs?.split(/\s+/)[0];
407
+
408
+ if (!sub || sub === 'show') {
409
+ const spent = budget.getTotalSpent();
410
+ const ceiling = budget.getCeiling();
411
+ const remaining = budget.remaining();
412
+ const pct = ceiling > 0 ? Math.round((spent / ceiling) * 100) : 0;
413
+ ui.log(`── Budget ──`);
414
+ ui.log(` Ceiling: ${ceiling.toLocaleString()} tokens`);
415
+ ui.log(` Spent: ${spent.toLocaleString()} tokens (${pct}%)`);
416
+ ui.log(` Remaining: ${remaining.toLocaleString()} tokens`);
417
+
418
+ const log = budget.getSpendLog();
419
+ const agents = Object.entries(log);
420
+ if (agents.length > 0) {
421
+ ui.log(` Per-agent:`);
422
+ for (const [agentId, tokens] of agents) {
423
+ ui.log(` ${agentId}: ${tokens.toLocaleString()}`);
424
+ }
425
+ }
426
+
427
+ // Sync status bar
428
+ ui.updateStatus({ budget: { usedPercent: pct } });
429
+ } else if (sub === 'spend') {
430
+ const parts = budgetArgs.split(/\s+/);
431
+ const agentId = parts[1];
432
+ const tokens = parseInt(parts[2], 10);
433
+ if (!agentId || !tokens || tokens <= 0) { ui.log('Usage: budget spend <agentId> <tokens>', 'warn'); return; }
434
+ const result = budget.spend(agentId, tokens);
435
+ ui.log(
436
+ result.allowed
437
+ ? `Spent ${tokens} tokens for "${agentId}" (remaining: ${result.remaining})`
438
+ : `Denied: ${result.deniedReason ?? 'ceiling exceeded'} (remaining: ${result.remaining})`,
439
+ result.allowed ? 'success' : 'error',
440
+ );
441
+ const pct = budget.getCeiling() > 0 ? Math.round((budget.getTotalSpent() / budget.getCeiling()) * 100) : 0;
442
+ ui.updateStatus({ budget: { usedPercent: pct } });
443
+ } else if (sub === 'reset') {
444
+ budget.reset();
445
+ ui.log('Budget reset to zero', 'success');
446
+ ui.updateStatus({ budget: { usedPercent: 0 } });
447
+ } else {
448
+ ui.log('Usage: budget [show|spend <agentId> <tokens>|reset]', 'warn');
449
+ }
450
+ }, 'Budget tracking (budget [show|spend|reset])');
451
+
452
+ ui.command('fsm', (fsmArgs) => {
453
+ const parts = fsmArgs?.split(/\s+/) ?? [];
454
+ const sub = parts[0];
455
+
456
+ if (!sub || sub === 'show') {
457
+ ui.log(`── FSM State ──`);
458
+ ui.log(` Current: ${fsm.state}`);
459
+ ui.log(` Time in state: ${fsm.timeInCurrentState}ms`);
460
+ ui.log(` Available events: ${fsm.availableEvents().join(', ') || '(none)'}`);
461
+ ui.updateStatus({ fsm: { state: fsm.state } });
462
+ } else if (sub === 'transition' || sub === 'go') {
463
+ const event = parts[1];
464
+ if (!event) { ui.log('Usage: fsm transition <event>', 'warn'); return; }
465
+ const result = fsm.transition(event, 'console-user');
466
+ if (result.success) {
467
+ ui.log(`Transitioned: ${result.previousState} → ${result.currentState} (event: ${event})`, 'success');
468
+ ui.updateStatus({ fsm: { state: fsm.state } });
469
+ } else {
470
+ ui.log(`Transition failed: ${result.reason ?? 'unknown'}`, 'error');
471
+ }
472
+ } else if (sub === 'events') {
473
+ const events = fsm.availableEvents();
474
+ if (events.length === 0) { ui.log('No available events from current state', 'info'); return; }
475
+ ui.log(`Available events: ${events.join(', ')}`);
476
+ } else if (sub === 'history') {
477
+ const history = fsm.transitionHistory;
478
+ if (history.length === 0) { ui.log('No transition history', 'info'); return; }
479
+ ui.log(`── Transition History (${history.length}) ──`);
480
+ for (const h of history) {
481
+ const enteredAt = new Date(h.enteredAt).toISOString().split('T')[1]?.slice(0, 8) ?? '';
482
+ ui.log(` [${enteredAt}] ${h.state}${h.triggeredBy ? ` (by ${h.triggeredBy})` : ''}`);
483
+ }
484
+ } else if (sub === 'reset') {
485
+ fsm.reset();
486
+ ui.log('FSM reset to initial state', 'success');
487
+ ui.updateStatus({ fsm: { state: fsm.state } });
488
+ } else {
489
+ ui.log('Usage: fsm [show|transition <event>|events|history|reset]', 'warn');
490
+ }
491
+ }, 'FSM workflow control (fsm [show|transition|events|history|reset])');
492
+
493
+ ui.command('health', async () => {
494
+ ui.log('Running health check...', 'info');
495
+ try {
496
+ const results = await adapters.healthCheck();
497
+ const entries = Object.entries(results);
498
+ if (entries.length === 0) { ui.log('No adapters to check', 'info'); return; }
499
+ for (const [name, check] of entries) {
500
+ ui.log(` ${name}: ${check.healthy ? 'healthy' : 'unhealthy'}${check.details ? ' — ' + check.details : ''}`, check.healthy ? 'success' : 'error');
501
+ }
502
+ } catch (err) {
503
+ ui.log(`Health check failed: ${err instanceof Error ? err.message : String(err)}`, 'error');
504
+ }
505
+ }, 'Run adapter health checks');
506
+
507
+ // ── Wire runtime events to feed ──────────────────────────────────────────
508
+
509
+ runtime.on('command:start', (agentId: string, cmd: string) => {
510
+ ui.log(`${agentId} → exec "${cmd}"`, 'info');
511
+ });
512
+
513
+ runtime.on('command:complete', (agentId: string, cmd: string, result: { exitCode: number; durationMs: number }) => {
514
+ const icon = result.exitCode === 0 ? 'success' : 'error';
515
+ ui.log(`${agentId} ← exit ${result.exitCode} (${result.durationMs}ms)`, icon as 'success' | 'error');
516
+ });
517
+
518
+ runtime.on('policy:violation', (agentId: string, target: string, reason: string) => {
519
+ ui.log(`BLOCKED: ${agentId} tried ${target} — ${reason}`, 'error');
520
+ });
521
+
522
+ runtime.on('approval:requested', () => {
523
+ ui.updateStatus({ pendingApprovals: ui.getStatus().pendingApprovals + 1 });
524
+ });
525
+
526
+ // ── Start ────────────────────────────────────────────────────────────────
527
+
528
+ if (args.pipe) {
529
+ // ── Pipe mode: JSON in → JSON out ────────────────────────────────────
530
+ await runPipeMode(ui, { blackboard, budget, fsm, adapters, runtime });
531
+ } else {
532
+ // ── Interactive TUI mode ─────────────────────────────────────────────
533
+ ui.log('Console ready. Type "help" for commands.', 'success');
534
+ await ui.start();
535
+ }
536
+ }
537
+
538
+ // ============================================================================
539
+ // PIPE MODE — JSON protocol for AI agents
540
+ // ============================================================================
541
+
542
+ interface PipeCommand {
543
+ /** Command name (same as console commands: status, bb, budget, fsm, etc.) */
544
+ command: string;
545
+ /** Arguments string (same format as interactive mode) */
546
+ args?: string;
547
+ /** Optional request ID for correlation */
548
+ id?: string | number;
549
+ }
550
+
551
+ interface PipeResponse {
552
+ /** Whether the command succeeded */
553
+ success: boolean;
554
+ /** Command that was executed */
555
+ command: string;
556
+ /** Structured response data */
557
+ data?: unknown;
558
+ /** Error message if failed */
559
+ error?: string;
560
+ /** Correlated request ID */
561
+ id?: string | number;
562
+ }
563
+
564
+ async function runPipeMode(
565
+ ui: ConsoleUI,
566
+ ctx: {
567
+ blackboard: LockedBlackboard;
568
+ budget: FederatedBudget;
569
+ fsm: JourneyFSM;
570
+ adapters: AdapterRegistry;
571
+ runtime: AgentRuntime;
572
+ },
573
+ ): Promise<void> {
574
+ const { createInterface } = await import('readline');
575
+ const rl = createInterface({ input: process.stdin, terminal: false });
576
+
577
+ const respond = (res: PipeResponse): void => {
578
+ process.stdout.write(JSON.stringify(res) + '\n');
579
+ };
580
+
581
+ rl.on('line', async (line: string) => {
582
+ const trimmed = line.trim();
583
+ if (!trimmed) return;
584
+
585
+ let cmd: PipeCommand;
586
+ try {
587
+ cmd = JSON.parse(trimmed);
588
+ } catch {
589
+ respond({ success: false, command: '', error: 'Invalid JSON' });
590
+ return;
591
+ }
592
+
593
+ if (!cmd.command || typeof cmd.command !== 'string') {
594
+ respond({ success: false, command: '', error: 'Missing "command" field', id: cmd.id });
595
+ return;
596
+ }
597
+
598
+ try {
599
+ const data = await executePipeCommand(cmd.command, cmd.args ?? '', ctx);
600
+ respond({ success: true, command: cmd.command, data, id: cmd.id });
601
+ } catch (err) {
602
+ respond({
603
+ success: false,
604
+ command: cmd.command,
605
+ error: err instanceof Error ? err.message : String(err),
606
+ id: cmd.id,
607
+ });
608
+ }
609
+ });
610
+
611
+ rl.on('close', () => process.exit(0));
612
+
613
+ // Keep alive
614
+ await new Promise<void>(() => {});
615
+ }
616
+
617
+ async function executePipeCommand(
618
+ command: string,
619
+ args: string,
620
+ ctx: {
621
+ blackboard: LockedBlackboard;
622
+ budget: FederatedBudget;
623
+ fsm: JourneyFSM;
624
+ adapters: AdapterRegistry;
625
+ runtime: AgentRuntime;
626
+ },
627
+ ): Promise<unknown> {
628
+ const { blackboard, budget, fsm, adapters, runtime } = ctx;
629
+
630
+ switch (command) {
631
+ case 'status': {
632
+ const adapterList = adapters.listAdapters();
633
+ return {
634
+ agents: { active: adapterList.filter(a => a.ready).length, total: adapterList.length },
635
+ budget: { spent: budget.getTotalSpent(), ceiling: budget.getCeiling(), remaining: budget.remaining() },
636
+ fsm: { state: fsm.state, availableEvents: fsm.availableEvents() },
637
+ blackboard: { keys: blackboard.listKeys().length, pending: blackboard.listPendingChanges().length },
638
+ runtime: { shellProcesses: runtime.shell.running, auditEntries: runtime.getAuditLog().length },
639
+ };
640
+ }
641
+
642
+ case 'bb': {
643
+ const parts = args.split(/\s+/);
644
+ const sub = parts[0];
645
+ if (sub === 'list') return { keys: blackboard.listKeys() };
646
+ if (sub === 'read') {
647
+ const entry = blackboard.read(parts[1]);
648
+ if (!entry) throw new Error(`Key "${parts[1]}" not found`);
649
+ return entry;
650
+ }
651
+ if (sub === 'write') {
652
+ let parsed: unknown;
653
+ const value = parts.slice(2).join(' ');
654
+ try { parsed = JSON.parse(value); } catch { parsed = value; }
655
+ blackboard.write(parts[1], parsed, 'pipe-agent');
656
+ return { key: parts[1], written: true };
657
+ }
658
+ if (sub === 'delete') {
659
+ return { key: parts[1], deleted: blackboard.delete(parts[1]) };
660
+ }
661
+ if (sub === 'propose') {
662
+ let parsed: unknown;
663
+ const value = parts.slice(2).join(' ');
664
+ try { parsed = JSON.parse(value); } catch { parsed = value; }
665
+ const changeId = blackboard.propose(parts[1], parsed, 'pipe-agent');
666
+ return { changeId };
667
+ }
668
+ if (sub === 'validate') {
669
+ return { valid: blackboard.validate(parts[1], 'pipe-agent') };
670
+ }
671
+ if (sub === 'commit') {
672
+ return blackboard.commit(parts[1]);
673
+ }
674
+ if (sub === 'pending') {
675
+ return { pending: blackboard.listPendingChanges() };
676
+ }
677
+ if (sub === 'snapshot') {
678
+ return blackboard.getSnapshot();
679
+ }
680
+ throw new Error(`Unknown bb subcommand: ${sub}`);
681
+ }
682
+
683
+ case 'budget': {
684
+ const sub = args.split(/\s+/)[0];
685
+ if (!sub || sub === 'show') {
686
+ return {
687
+ ceiling: budget.getCeiling(),
688
+ spent: budget.getTotalSpent(),
689
+ remaining: budget.remaining(),
690
+ perAgent: budget.getSpendLog(),
691
+ };
692
+ }
693
+ if (sub === 'spend') {
694
+ const parts = args.split(/\s+/);
695
+ return budget.spend(parts[1], parseInt(parts[2], 10));
696
+ }
697
+ if (sub === 'reset') {
698
+ budget.reset();
699
+ return { reset: true };
700
+ }
701
+ throw new Error(`Unknown budget subcommand: ${sub}`);
702
+ }
703
+
704
+ case 'fsm': {
705
+ const parts = args.split(/\s+/);
706
+ const sub = parts[0];
707
+ if (!sub || sub === 'show') {
708
+ return {
709
+ state: fsm.state,
710
+ timeInState: fsm.timeInCurrentState,
711
+ availableEvents: fsm.availableEvents(),
712
+ };
713
+ }
714
+ if (sub === 'transition' || sub === 'go') {
715
+ return fsm.transition(parts[1], parts[2] ?? 'pipe-agent');
716
+ }
717
+ if (sub === 'events') {
718
+ return { events: fsm.availableEvents() };
719
+ }
720
+ if (sub === 'history') {
721
+ return { history: fsm.transitionHistory };
722
+ }
723
+ if (sub === 'reset') {
724
+ fsm.reset();
725
+ return { state: fsm.state };
726
+ }
727
+ throw new Error(`Unknown fsm subcommand: ${sub}`);
728
+ }
729
+
730
+ case 'agents': {
731
+ const adapterList = adapters.listAdapters();
732
+ let discovered: unknown[] = [];
733
+ try { discovered = await adapters.discoverAgents(); } catch { /* ok */ }
734
+ return { adapters: adapterList, discovered };
735
+ }
736
+
737
+ case 'spawn': {
738
+ const [agentId, ...rest] = args.split(/\s+/);
739
+ if (!agentId) throw new Error('Usage: spawn <agentId> [input]');
740
+ return adapters.executeAgent(
741
+ agentId,
742
+ { action: 'execute', params: { input: rest.join(' ') || 'execute' } },
743
+ { agentId: 'pipe-agent', taskId: `pipe-${Date.now()}`, sessionId: 'pipe' },
744
+ );
745
+ }
746
+
747
+ case 'exec': {
748
+ if (!args) throw new Error('Usage: exec <command>');
749
+ return runtime.exec(args, 'pipe-agent');
750
+ }
751
+
752
+ case 'health': {
753
+ return adapters.healthCheck();
754
+ }
755
+
756
+ case 'audit': {
757
+ const count = parseInt(args) || 10;
758
+ return { entries: runtime.getAuditLog().slice(-count) };
759
+ }
760
+
761
+ default:
762
+ throw new Error(`Unknown command: ${command}`);
763
+ }
764
+ }
765
+
766
+ main().catch((err) => {
767
+ console.error('Fatal:', err);
768
+ process.exit(1);
769
+ });