0nmcp 1.6.0 → 2.0.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/cli.js CHANGED
@@ -11,8 +11,12 @@
11
11
  * npx 0nmcp init Initialize ~/.0n directory
12
12
  * npx 0nmcp connect Interactive connection setup
13
13
  * npx 0nmcp list List connected services
14
+ * npx 0nmcp commands List named RUNs from SWITCH file
15
+ * npx 0nmcp shell Interactive REPL (type /command)
16
+ * npx 0nmcp <alias> Run a named command from SWITCH file
14
17
  * npx 0nmcp migrate Migrate from ~/.0nmcp to ~/.0n
15
18
  * npx 0nmcp engine Engine commands (import, verify, platforms, export, open)
19
+ * npx 0nmcp app Application commands (run, build, inspect, validate, list)
16
20
  *
17
21
  * ═══════════════════════════════════════════════════════════════════════════
18
22
  */
@@ -23,6 +27,8 @@ import { fileURLToPath } from 'url';
23
27
  import fs from 'fs';
24
28
  import os from 'os';
25
29
  import readline from 'readline';
30
+ import { loadCommands, listCommands } from './commands.js';
31
+ import { runCommand } from './command-runner.js';
26
32
 
27
33
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
34
 
@@ -87,6 +93,40 @@ ${c.bright}Engine commands:${c.reset}
87
93
  ${c.cyan}npx 0nmcp engine export${c.reset} Export .0n bundle (AI Brain)
88
94
  ${c.cyan}npx 0nmcp engine open <bundle>${c.reset} Open/inspect a .0n bundle
89
95
 
96
+ ${c.bright}Vault Container commands (Patent Pending #63/990,046):${c.reset}
97
+
98
+ ${c.cyan}npx 0nmcp vault create${c.reset} Create a .0nv vault container
99
+ ${c.cyan}npx 0nmcp vault open <file>${c.reset} Open/decrypt a .0nv container
100
+ ${c.cyan}npx 0nmcp vault inspect <file>${c.reset} Inspect container metadata (no passphrase)
101
+ ${c.cyan}npx 0nmcp vault verify <file>${c.reset} Verify Seal of Truth + signature
102
+ ${c.cyan}npx 0nmcp vault escrow create${c.reset} Generate escrow keypairs
103
+ ${c.cyan}npx 0nmcp vault escrow unwrap${c.reset} Unwrap container with escrow key
104
+ ${c.cyan}npx 0nmcp vault transfer <file>${c.reset} Register a vault transfer
105
+ ${c.cyan}npx 0nmcp vault revoke <id>${c.reset} Revoke a transfer ID
106
+
107
+ ${c.bright}Application commands:${c.reset}
108
+
109
+ ${c.cyan}npx 0nmcp app run <file>${c.reset} Start application server
110
+ ${c.cyan}npx 0nmcp app build${c.reset} Build application bundle
111
+ ${c.cyan}npx 0nmcp app inspect <file>${c.reset} Show application metadata
112
+ ${c.cyan}npx 0nmcp app validate <file>${c.reset} Validate application structure
113
+ ${c.cyan}npx 0nmcp app list${c.reset} List installed applications
114
+
115
+ ${c.bright}Named Runs (Hotkeys):${c.reset}
116
+
117
+ ${c.cyan}npx 0nmcp commands${c.reset} List named RUNs from SWITCH file
118
+ ${c.cyan}npx 0nmcp <alias>${c.reset} Execute a named RUN
119
+ ${c.cyan}npx 0nmcp shell${c.reset} Interactive REPL (type /command)
120
+
121
+ Define commands in ~/.0n/0n-setup.0n:
122
+
123
+ {
124
+ "commands": {
125
+ "launch": { "type": "pipeline", "steps": [{ "action": "verify" }] },
126
+ "score": { "type": "workflow", "workflow": "lead-scoring" }
127
+ }
128
+ }
129
+
90
130
  ${c.bright}Serve options:${c.reset}
91
131
 
92
132
  ${c.cyan}npx 0nmcp serve --port 3000 --host 0.0.0.0${c.reset}
@@ -217,6 +257,20 @@ ${c.bright}Links:${c.reset}
217
257
  }
218
258
  }
219
259
 
260
+ // Vault Container
261
+ if (command === 'vault') {
262
+ console.log(BANNER);
263
+ await handleVault(args.slice(1));
264
+ return;
265
+ }
266
+
267
+ // App
268
+ if (command === 'app') {
269
+ console.log(BANNER);
270
+ await handleApp(args.slice(1));
271
+ return;
272
+ }
273
+
220
274
  // Engine
221
275
  if (command === 'engine') {
222
276
  console.log(BANNER);
@@ -233,9 +287,57 @@ ${c.bright}Links:${c.reset}
233
287
  return;
234
288
  }
235
289
 
290
+ // Commands — list named RUNs from SWITCH file
291
+ if (command === 'commands' || command === 'aliases') {
292
+ console.log(BANNER);
293
+ const cmds = listCommands();
294
+ if (cmds.length === 0) {
295
+ console.log(`${c.yellow}No commands defined.${c.reset}`);
296
+ console.log(`\nAdd a ${c.cyan}"commands"${c.reset} section to ${c.cyan}~/.0n/0n-setup.0n${c.reset}`);
297
+ console.log(`\nExample:`);
298
+ console.log(` {`);
299
+ console.log(` "commands": {`);
300
+ console.log(` "launch": { "type": "pipeline", "steps": [{ "action": "verify" }] }`);
301
+ console.log(` }`);
302
+ console.log(` }`);
303
+ return;
304
+ }
305
+ console.log(`${c.bright}Named Runs:${c.reset}\n`);
306
+ for (const cmd of cmds) {
307
+ const typeTag = cmd.type === 'pipeline' ? `${c.blue}pipeline${c.reset}` : `${c.green}workflow${c.reset}`;
308
+ console.log(` ${c.cyan}0nmcp ${cmd.alias}${c.reset} ${cmd.description || cmd.name} [${typeTag}]`);
309
+ }
310
+ console.log(`\n${c.bright}Tip:${c.reset} Run ${c.cyan}0nmcp shell${c.reset} for an interactive REPL where you can type ${c.cyan}/launch${c.reset}, ${c.cyan}/hello${c.reset}, etc.`);
311
+ return;
312
+ }
313
+
314
+ // Shell — interactive REPL
315
+ if (command === 'shell' || command === 'repl') {
316
+ console.log(BANNER);
317
+ await startShell();
318
+ return;
319
+ }
320
+
321
+ // Dynamic command aliases from SWITCH file
322
+ const switchCommands = loadCommands();
323
+ if (switchCommands.has(command)) {
324
+ console.log(BANNER);
325
+ const def = switchCommands.get(command);
326
+ const cliArgs = args.slice(1);
327
+ await runCommand(command, def, cliArgs);
328
+ return;
329
+ }
330
+
236
331
  // Unknown command
237
332
  console.log(`${c.red}Unknown command: ${command}${c.reset}`);
238
- console.log(`Run ${c.cyan}npx 0nmcp help${c.reset} for usage`);
333
+ const switchCmds = listCommands();
334
+ if (switchCmds.length > 0) {
335
+ console.log(`\n${c.bright}Available named RUNs:${c.reset}`);
336
+ for (const cmd of switchCmds) {
337
+ console.log(` ${c.cyan}0nmcp ${cmd.alias}${c.reset} ${cmd.description || cmd.name}`);
338
+ }
339
+ }
340
+ console.log(`\nRun ${c.cyan}npx 0nmcp help${c.reset} for usage`);
239
341
  process.exit(1);
240
342
  }
241
343
 
@@ -256,6 +358,7 @@ function initDotOn() {
256
358
  path.join(DOT_ON_DIR, 'snapshots'),
257
359
  path.join(DOT_ON_DIR, 'history'),
258
360
  path.join(DOT_ON_DIR, 'cache'),
361
+ path.join(DOT_ON_DIR, 'apps'),
259
362
  ];
260
363
 
261
364
  console.log(`${c.bright}Initializing ~/.0n directory...${c.reset}\n`);
@@ -437,6 +540,229 @@ async function interactiveConnect() {
437
540
  console.log(` Saved to: ${filePath}`);
438
541
  }
439
542
 
543
+ // ═══════════════════════════════════════════════════════════
544
+ // Vault Container Commands (Patent Pending #63/990,046)
545
+ // ═══════════════════════════════════════════════════════════
546
+
547
+ function promptInput(prompt) {
548
+ return new Promise(resolve => {
549
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
550
+ rl.question(prompt, answer => { rl.close(); resolve(answer); });
551
+ });
552
+ }
553
+
554
+ async function handleVault(args) {
555
+ const sub = args[0];
556
+
557
+ if (!sub || sub === 'help') {
558
+ console.log(`
559
+ ${c.bright}Vault Container Commands${c.reset} ${c.yellow}(Patent Pending #63/990,046)${c.reset}
560
+
561
+ ${c.cyan}vault create${c.reset} Create a .0nv vault container
562
+ --layers <names> Comma-separated layer names (default: all)
563
+ --passphrase <pass> Encryption passphrase
564
+ --output <file> Output file path
565
+
566
+ ${c.cyan}vault open <file>${c.reset} Open/decrypt a .0nv container
567
+ --passphrase <pass> Decryption passphrase
568
+ --layer <name> Only decrypt this layer
569
+
570
+ ${c.cyan}vault inspect <file>${c.reset} Inspect without decrypting
571
+
572
+ ${c.cyan}vault verify <file>${c.reset} Verify Seal of Truth + Ed25519 signature
573
+
574
+ ${c.cyan}vault escrow create${c.reset} Generate escrow keypairs
575
+ --parties <n> Number of parties (1-8)
576
+
577
+ ${c.cyan}vault transfer <file>${c.reset} Register a transfer
578
+ --to <recipient> Recipient identifier
579
+
580
+ ${c.cyan}vault revoke <id>${c.reset} Revoke a transfer ID
581
+
582
+ ${c.bright}7 Semantic Layers:${c.reset}
583
+ workflows, credentials, env_vars, mcp_configs,
584
+ site_profiles, ai_brain, audit_trail
585
+ `);
586
+ return;
587
+ }
588
+
589
+ if (sub === 'create') {
590
+ const { assembleContainer, saveContainer } = await import('./vault/container.js');
591
+ const { LAYER_NAMES } = await import('./vault/layers.js');
592
+
593
+ const passphrase = getFlag(args, '--passphrase') || await promptInput('Passphrase: ');
594
+ const output = getFlag(args, '--output');
595
+ const layerStr = getFlag(args, '--layers');
596
+ const requestedLayers = layerStr ? layerStr.split(',').map(s => s.trim()) : LAYER_NAMES;
597
+
598
+ // Build layer data from ~/.0n directory
599
+ const layers = {};
600
+ for (const name of requestedLayers) {
601
+ if (!LAYER_NAMES.includes(name)) {
602
+ console.log(`${c.red}Invalid layer: ${name}${c.reset}`);
603
+ process.exit(1);
604
+ }
605
+ layers[name] = { placeholder: true, created: new Date().toISOString() };
606
+ }
607
+
608
+ console.log(`\n${c.bright}Creating vault container...${c.reset}`);
609
+ console.log(` Layers: ${Object.keys(layers).join(', ')}`);
610
+
611
+ try {
612
+ const result = await assembleContainer({ layers, passphrase });
613
+ const homePath = path.join(os.homedir(), '.0n', 'vault');
614
+ if (!fs.existsSync(homePath)) fs.mkdirSync(homePath, { recursive: true });
615
+ const filePath = output || path.join(homePath, `${result.transferId}.0nv`);
616
+ saveContainer(result.buffer, filePath);
617
+
618
+ console.log(`\n${c.green}✓ Container created${c.reset}`);
619
+ console.log(` Transfer ID: ${c.cyan}${result.transferId}${c.reset}`);
620
+ console.log(` Seal of Truth: ${c.cyan}${result.sealHex}${c.reset}`);
621
+ console.log(` Layers: ${result.layerCount}`);
622
+ console.log(` Size: ${result.buffer.length} bytes`);
623
+ console.log(` File: ${c.yellow}${filePath}${c.reset}`);
624
+ } catch (err) {
625
+ console.log(`${c.red}Error: ${err.message}${c.reset}`);
626
+ process.exit(1);
627
+ }
628
+ return;
629
+ }
630
+
631
+ if (sub === 'open') {
632
+ const file = args[1];
633
+ if (!file) { console.log(`${c.red}Usage: vault open <file.0nv>${c.reset}`); process.exit(1); }
634
+
635
+ const { disassembleContainer, loadContainer } = await import('./vault/container.js');
636
+ const passphrase = getFlag(args, '--passphrase') || await promptInput('Passphrase: ');
637
+ const layer = getFlag(args, '--layer');
638
+
639
+ try {
640
+ const buffer = loadContainer(file);
641
+ const result = await disassembleContainer(buffer, passphrase, layer ? [layer] : null);
642
+
643
+ console.log(`\n${c.green}✓ Container opened${c.reset}`);
644
+ console.log(` Transfer ID: ${c.cyan}${result.metadata.transferId}${c.reset}`);
645
+ console.log(` Seal valid: ${result.seal.valid ? c.green + '✓' : c.red + '✗'}${c.reset}`);
646
+ console.log(` Sig valid: ${result.signature.valid ? c.green + '✓' : c.red + '✗'}${c.reset}`);
647
+ console.log(` Layers:`);
648
+ for (const [name, data] of Object.entries(result.layers)) {
649
+ const preview = JSON.stringify(data).substring(0, 80);
650
+ console.log(` ${c.cyan}${name}${c.reset}: ${preview}...`);
651
+ }
652
+ } catch (err) {
653
+ console.log(`${c.red}Error: ${err.message}${c.reset}`);
654
+ process.exit(1);
655
+ }
656
+ return;
657
+ }
658
+
659
+ if (sub === 'inspect') {
660
+ const file = args[1];
661
+ if (!file) { console.log(`${c.red}Usage: vault inspect <file.0nv>${c.reset}`); process.exit(1); }
662
+
663
+ const { inspectContainer, loadContainer } = await import('./vault/container.js');
664
+ try {
665
+ const buffer = loadContainer(file);
666
+ const info = inspectContainer(buffer);
667
+ console.log(`\n${c.bright}Container Inspection${c.reset}`);
668
+ console.log(JSON.stringify(info, null, 2));
669
+ } catch (err) {
670
+ console.log(`${c.red}Error: ${err.message}${c.reset}`);
671
+ process.exit(1);
672
+ }
673
+ return;
674
+ }
675
+
676
+ if (sub === 'verify') {
677
+ const file = args[1];
678
+ if (!file) { console.log(`${c.red}Usage: vault verify <file.0nv>${c.reset}`); process.exit(1); }
679
+
680
+ const { inspectContainer, loadContainer } = await import('./vault/container.js');
681
+ try {
682
+ const buffer = loadContainer(file);
683
+ const info = inspectContainer(buffer);
684
+ const verified = info.seal.valid && info.signature.valid;
685
+
686
+ console.log(`\n${verified ? c.green + '✓ VERIFIED' : c.red + '✗ FAILED'}${c.reset}`);
687
+ console.log(` Seal of Truth: ${info.seal.valid ? c.green + 'Valid' : c.red + 'Invalid'}${c.reset} (${info.seal.algorithm})`);
688
+ console.log(` Signature: ${info.signature.valid ? c.green + 'Valid' : c.red + 'Invalid'}${c.reset} (${info.signature.algorithm})`);
689
+ console.log(` Transfer ID: ${info.metadata.transferId}`);
690
+ console.log(` Created: ${info.metadata.created}`);
691
+ console.log(` Patent: ${info.patent}`);
692
+ } catch (err) {
693
+ console.log(`${c.red}Error: ${err.message}${c.reset}`);
694
+ process.exit(1);
695
+ }
696
+ return;
697
+ }
698
+
699
+ if (sub === 'escrow') {
700
+ const escrowSub = args[1];
701
+ if (escrowSub === 'create') {
702
+ const { generatePartyKeys } = await import('./vault/escrow.js');
703
+ const count = parseInt(getFlag(args, '--parties') || '3');
704
+
705
+ console.log(`\n${c.bright}Generating ${count} escrow keypairs...${c.reset}`);
706
+ const parties = [];
707
+ for (let i = 0; i < count; i++) {
708
+ const party = generatePartyKeys();
709
+ parties.push(party);
710
+ console.log(`\n ${c.cyan}Party ${i + 1}${c.reset} (${party.partyId}):`);
711
+ console.log(` Public: ${party.publicKey.toString('hex').substring(0, 32)}...`);
712
+ console.log(` Secret: ${party.secretKey.toString('hex').substring(0, 32)}...`);
713
+ }
714
+ console.log(`\n${c.green}✓ ${count} keypairs generated${c.reset}`);
715
+ console.log(` ${c.yellow}Save secret keys securely. Share public keys for container creation.${c.reset}`);
716
+ }
717
+ return;
718
+ }
719
+
720
+ if (sub === 'transfer') {
721
+ const file = args[1];
722
+ if (!file) { console.log(`${c.red}Usage: vault transfer <file.0nv>${c.reset}`); process.exit(1); }
723
+
724
+ const { inspectContainer, loadContainer } = await import('./vault/container.js');
725
+ const { registerTransfer } = await import('./vault/registry.js');
726
+ const recipient = getFlag(args, '--to');
727
+
728
+ try {
729
+ const buffer = loadContainer(file);
730
+ const info = inspectContainer(buffer);
731
+ const result = registerTransfer(info.metadata.transferId, info.seal.hash, { recipient });
732
+
733
+ if (result.success) {
734
+ console.log(`\n${c.green}✓ Transfer registered${c.reset}`);
735
+ console.log(` Transfer ID: ${c.cyan}${info.metadata.transferId}${c.reset}`);
736
+ if (recipient) console.log(` Recipient: ${recipient}`);
737
+ } else {
738
+ console.log(`\n${c.red}✗ ${result.error}${c.reset}`);
739
+ }
740
+ } catch (err) {
741
+ console.log(`${c.red}Error: ${err.message}${c.reset}`);
742
+ process.exit(1);
743
+ }
744
+ return;
745
+ }
746
+
747
+ if (sub === 'revoke') {
748
+ const transferId = args[1];
749
+ if (!transferId) { console.log(`${c.red}Usage: vault revoke <transferId>${c.reset}`); process.exit(1); }
750
+
751
+ const { revokeTransfer } = await import('./vault/registry.js');
752
+ const result = revokeTransfer(transferId);
753
+
754
+ if (result.success) {
755
+ console.log(`\n${c.green}✓ Transfer revoked: ${transferId}${c.reset}`);
756
+ } else {
757
+ console.log(`\n${c.red}✗ ${result.error}${c.reset}`);
758
+ }
759
+ return;
760
+ }
761
+
762
+ console.log(`${c.red}Unknown vault command: ${sub}${c.reset}`);
763
+ console.log(`Run ${c.cyan}0nmcp vault help${c.reset} for usage.`);
764
+ }
765
+
440
766
  async function handleEngine(args) {
441
767
  const sub = args[0];
442
768
 
@@ -677,4 +1003,344 @@ async function handleEngine(args) {
677
1003
  process.exit(1);
678
1004
  }
679
1005
 
1006
+ async function handleApp(args) {
1007
+ const sub = args[0];
1008
+
1009
+ if (!sub || sub === 'help') {
1010
+ console.log(`${c.bright}Application Engine — Build & Run .0n Applications${c.reset}\n`);
1011
+ console.log(` ${c.cyan}run <file>${c.reset} Start application server`);
1012
+ console.log(` ${c.cyan}build${c.reset} Build application bundle`);
1013
+ console.log(` ${c.cyan}inspect <file>${c.reset} Show application metadata`);
1014
+ console.log(` ${c.cyan}validate <file>${c.reset} Validate application structure`);
1015
+ console.log(` ${c.cyan}list${c.reset} List installed applications\n`);
1016
+ return;
1017
+ }
1018
+
1019
+ if (sub === 'run') {
1020
+ const appFile = args[1];
1021
+ if (!appFile) {
1022
+ console.log(`${c.red}Usage: npx 0nmcp app run <file.0n> [--port 3000] [--passphrase <pw>]${c.reset}`);
1023
+ process.exit(1);
1024
+ }
1025
+
1026
+ const port = Number(getFlag(args, '--port', 3000));
1027
+ let passphrase = getFlag(args, '--passphrase', null);
1028
+
1029
+ if (!passphrase) {
1030
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1031
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve));
1032
+ passphrase = await ask('Application passphrase: ');
1033
+ rl.close();
1034
+ }
1035
+
1036
+ if (!passphrase) {
1037
+ console.log(`${c.red}Passphrase required.${c.reset}`);
1038
+ process.exit(1);
1039
+ }
1040
+
1041
+ try {
1042
+ const { openApplication } = await import('./engine/app-builder.js');
1043
+ const { Application } = await import('./engine/application.js');
1044
+ const { ApplicationServer } = await import('./engine/app-server.js');
1045
+
1046
+ const bundle = openApplication(appFile, passphrase);
1047
+ const application = new Application(bundle, { passphrase });
1048
+ const server = new ApplicationServer(application);
1049
+
1050
+ await server.start({ port });
1051
+
1052
+ // Keep alive
1053
+ process.on('SIGINT', async () => {
1054
+ console.log(`\n${c.yellow}Shutting down...${c.reset}`);
1055
+ await server.stop();
1056
+ process.exit(0);
1057
+ });
1058
+ } catch (err) {
1059
+ console.log(`${c.red}Error: ${err.message}${c.reset}`);
1060
+ process.exit(1);
1061
+ }
1062
+ return;
1063
+ }
1064
+
1065
+ if (sub === 'build') {
1066
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1067
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve));
1068
+
1069
+ try {
1070
+ const appName = await ask('Application name: ') || '0n Application';
1071
+ const passphrase = await ask('Bundle passphrase: ');
1072
+ if (!passphrase) {
1073
+ console.log(`${c.red}Passphrase required.${c.reset}`);
1074
+ rl.close();
1075
+ process.exit(1);
1076
+ }
1077
+ rl.close();
1078
+
1079
+ const { createApplication } = await import('./engine/app-builder.js');
1080
+
1081
+ // Load local connections
1082
+ const connectionsDir = path.join(DOT_ON_DIR, 'connections');
1083
+ const connections = {};
1084
+ if (fs.existsSync(connectionsDir)) {
1085
+ const files = fs.readdirSync(connectionsDir).filter(f => f.endsWith('.0n') || f.endsWith('.0n.json'));
1086
+ for (const file of files) {
1087
+ try {
1088
+ const data = JSON.parse(fs.readFileSync(path.join(connectionsDir, file), 'utf8'));
1089
+ if (data.$0n?.sealed) continue;
1090
+ connections[data.service] = {
1091
+ credentials: data.auth?.credentials || {},
1092
+ name: data.$0n?.name || data.service,
1093
+ authType: data.auth?.type || 'api_key',
1094
+ environment: data.environment || 'production',
1095
+ };
1096
+ } catch { /* skip */ }
1097
+ }
1098
+ }
1099
+
1100
+ // Load local workflows
1101
+ const { loadLocalWorkflows } = await import('./engine/index.js');
1102
+ const workflows = loadLocalWorkflows();
1103
+
1104
+ console.log(`\n${c.bright}Building application...${c.reset}\n`);
1105
+
1106
+ const result = createApplication({
1107
+ name: appName,
1108
+ passphrase,
1109
+ connections,
1110
+ workflows,
1111
+ });
1112
+
1113
+ console.log(`${c.green}${c.bright}Application built!${c.reset}\n`);
1114
+ console.log(` Path: ${result.path}`);
1115
+ console.log(` Connections: ${result.manifest.connection_count}`);
1116
+ console.log(` Workflows: ${result.manifest.workflow_count}`);
1117
+ console.log(`\n${c.bright}Run with:${c.reset} ${c.cyan}npx 0nmcp app run ${result.path}${c.reset}`);
1118
+ } catch (err) {
1119
+ console.log(`${c.red}Error: ${err.message}${c.reset}`);
1120
+ process.exit(1);
1121
+ }
1122
+ return;
1123
+ }
1124
+
1125
+ if (sub === 'inspect') {
1126
+ const appFile = args[1];
1127
+ if (!appFile) {
1128
+ console.log(`${c.red}Usage: npx 0nmcp app inspect <file.0n>${c.reset}`);
1129
+ process.exit(1);
1130
+ }
1131
+
1132
+ try {
1133
+ const { inspectApplication } = await import('./engine/app-builder.js');
1134
+ const info = inspectApplication(appFile);
1135
+
1136
+ console.log(`${c.bright}Application: ${info.name}${c.reset}\n`);
1137
+ console.log(` Version: ${info.version}`);
1138
+ console.log(` Author: ${info.author || '—'}`);
1139
+ console.log(` Created: ${info.created}`);
1140
+ if (info.description) console.log(` Description: ${info.description}`);
1141
+ console.log(`\n Connections: ${info.connections.map(c2 => c2.service).join(', ') || 'none'}`);
1142
+ console.log(` Workflows: ${info.workflows.join(', ') || 'none'}`);
1143
+ console.log(` Operations: ${info.operations.join(', ') || 'none'}`);
1144
+ console.log(` Endpoints: ${info.endpoints.join(', ') || 'none'}`);
1145
+ console.log(` Automations: ${info.automations.join(', ') || 'none'}`);
1146
+ } catch (err) {
1147
+ console.log(`${c.red}Error: ${err.message}${c.reset}`);
1148
+ process.exit(1);
1149
+ }
1150
+ return;
1151
+ }
1152
+
1153
+ if (sub === 'validate') {
1154
+ const appFile = args[1];
1155
+ if (!appFile) {
1156
+ console.log(`${c.red}Usage: npx 0nmcp app validate <file.0n>${c.reset}`);
1157
+ process.exit(1);
1158
+ }
1159
+
1160
+ try {
1161
+ const { validateApplication } = await import('./engine/app-builder.js');
1162
+ const data = JSON.parse(fs.readFileSync(appFile, 'utf8'));
1163
+ const result = validateApplication(data);
1164
+
1165
+ if (result.valid) {
1166
+ console.log(`${c.green}${c.bright}Application is valid!${c.reset}`);
1167
+ } else {
1168
+ console.log(`${c.red}${c.bright}Validation failed:${c.reset}\n`);
1169
+ for (const err of result.errors) {
1170
+ console.log(` ${c.red}●${c.reset} ${err}`);
1171
+ }
1172
+ }
1173
+
1174
+ if (result.warnings?.length > 0) {
1175
+ console.log(`\n${c.yellow}Warnings:${c.reset}`);
1176
+ for (const w of result.warnings) {
1177
+ console.log(` ${c.yellow}○${c.reset} ${w}`);
1178
+ }
1179
+ }
1180
+ } catch (err) {
1181
+ console.log(`${c.red}Error: ${err.message}${c.reset}`);
1182
+ process.exit(1);
1183
+ }
1184
+ return;
1185
+ }
1186
+
1187
+ if (sub === 'list') {
1188
+ const appsDir = path.join(DOT_ON_DIR, 'apps');
1189
+ if (!fs.existsSync(appsDir)) {
1190
+ console.log(`${c.yellow}No applications installed.${c.reset}`);
1191
+ return;
1192
+ }
1193
+
1194
+ const files = fs.readdirSync(appsDir).filter(f => f.endsWith('.0n') || f.endsWith('.0n.json'));
1195
+ if (files.length === 0) {
1196
+ console.log(`${c.yellow}No applications installed.${c.reset}`);
1197
+ return;
1198
+ }
1199
+
1200
+ console.log(`${c.bright}Installed Applications:${c.reset}\n`);
1201
+
1202
+ for (const file of files) {
1203
+ try {
1204
+ const data = JSON.parse(fs.readFileSync(path.join(appsDir, file), 'utf8'));
1205
+ if (data.$0n?.type !== 'application') continue;
1206
+
1207
+ const wfCount = Object.keys(data.workflows || {}).length;
1208
+ const epCount = Object.keys(data.endpoints || {}).length;
1209
+ console.log(` ${c.green}●${c.reset} ${c.bright}${data.$0n.name || file}${c.reset} v${data.$0n.version || '1.0.0'}`);
1210
+ console.log(` ${wfCount} workflows, ${epCount} endpoints`);
1211
+ console.log(` ${file}`);
1212
+ console.log('');
1213
+ } catch {
1214
+ console.log(` ${c.red}●${c.reset} ${file} (error reading)`);
1215
+ }
1216
+ }
1217
+ return;
1218
+ }
1219
+
1220
+ console.log(`${c.red}Unknown app command: ${sub}${c.reset}`);
1221
+ console.log(`Run ${c.cyan}npx 0nmcp app help${c.reset} for usage`);
1222
+ process.exit(1);
1223
+ }
1224
+
1225
+ async function startShell() {
1226
+ const cmds = loadCommands();
1227
+ const aliases = [...cmds.keys()];
1228
+ const builtins = ['help', 'commands', 'list', 'exit', 'quit'];
1229
+ const allCompletions = [...builtins, ...aliases];
1230
+
1231
+ console.log(`${c.bright}0nMCP Interactive Shell${c.reset}`);
1232
+ console.log(`Type ${c.cyan}/command${c.reset} to run a named RUN, ${c.cyan}/commands${c.reset} to list, ${c.cyan}/exit${c.reset} to quit.\n`);
1233
+
1234
+ if (aliases.length > 0) {
1235
+ console.log(`${c.bright}Available commands:${c.reset} ${aliases.map(a => c.cyan + '/' + a + c.reset).join(', ')}\n`);
1236
+ }
1237
+
1238
+ const rl = readline.createInterface({
1239
+ input: process.stdin,
1240
+ output: process.stdout,
1241
+ prompt: `${c.green}0n>${c.reset} `,
1242
+ completer: (line) => {
1243
+ const input = line.startsWith('/') ? line.slice(1) : line;
1244
+ const hits = allCompletions.filter(c2 => c2.startsWith(input));
1245
+ return [hits.map(h => '/' + h), line];
1246
+ },
1247
+ });
1248
+
1249
+ rl.prompt();
1250
+
1251
+ rl.on('line', async (line) => {
1252
+ const input = line.trim();
1253
+ if (!input) {
1254
+ rl.prompt();
1255
+ return;
1256
+ }
1257
+
1258
+ // Strip leading / if present
1259
+ const raw = input.startsWith('/') ? input.slice(1) : input;
1260
+ const parts = raw.split(/\s+/);
1261
+ const cmd = parts[0];
1262
+ const cmdArgs = parts.slice(1);
1263
+
1264
+ if (cmd === 'exit' || cmd === 'quit') {
1265
+ console.log(`\n${c.cyan}Goodbye!${c.reset}`);
1266
+ rl.close();
1267
+ process.exit(0);
1268
+ }
1269
+
1270
+ if (cmd === 'help') {
1271
+ console.log(`\n${c.bright}Shell Commands:${c.reset}`);
1272
+ console.log(` ${c.cyan}/commands${c.reset} List all named RUNs`);
1273
+ console.log(` ${c.cyan}/list${c.reset} Show connected services`);
1274
+ console.log(` ${c.cyan}/exit${c.reset} Exit the shell`);
1275
+ if (aliases.length > 0) {
1276
+ console.log(`\n${c.bright}Named RUNs:${c.reset}`);
1277
+ for (const a of aliases) {
1278
+ const def = cmds.get(a);
1279
+ console.log(` ${c.cyan}/${a}${c.reset} ${def.description || def.name || ''}`);
1280
+ }
1281
+ }
1282
+ console.log('');
1283
+ rl.prompt();
1284
+ return;
1285
+ }
1286
+
1287
+ if (cmd === 'commands') {
1288
+ if (aliases.length === 0) {
1289
+ console.log(`\n${c.yellow}No commands defined in SWITCH file.${c.reset}\n`);
1290
+ } else {
1291
+ console.log(`\n${c.bright}Named RUNs:${c.reset}`);
1292
+ for (const a of aliases) {
1293
+ const def = cmds.get(a);
1294
+ console.log(` ${c.cyan}/${a}${c.reset} ${def.description || def.name || ''}`);
1295
+ }
1296
+ console.log('');
1297
+ }
1298
+ rl.prompt();
1299
+ return;
1300
+ }
1301
+
1302
+ if (cmd === 'list') {
1303
+ const connectionsDir = path.join(DOT_ON_DIR, 'connections');
1304
+ if (fs.existsSync(connectionsDir)) {
1305
+ const files = fs.readdirSync(connectionsDir).filter(f => f.endsWith('.0n'));
1306
+ console.log(`\n${c.bright}Connected Services:${c.reset}`);
1307
+ for (const file of files) {
1308
+ try {
1309
+ const data = JSON.parse(fs.readFileSync(path.join(connectionsDir, file), 'utf8'));
1310
+ console.log(` ${c.green}●${c.reset} ${data.$0n?.name || data.service}`);
1311
+ } catch {
1312
+ console.log(` ${c.red}●${c.reset} ${file} (error)`);
1313
+ }
1314
+ }
1315
+ console.log('');
1316
+ } else {
1317
+ console.log(`\n${c.yellow}No connections found.${c.reset}\n`);
1318
+ }
1319
+ rl.prompt();
1320
+ return;
1321
+ }
1322
+
1323
+ // Try to run as a named command
1324
+ if (cmds.has(cmd)) {
1325
+ console.log('');
1326
+ try {
1327
+ await runCommand(cmd, cmds.get(cmd), cmdArgs);
1328
+ } catch (err) {
1329
+ console.log(`${c.red}Error: ${err.message}${c.reset}`);
1330
+ }
1331
+ console.log('');
1332
+ rl.prompt();
1333
+ return;
1334
+ }
1335
+
1336
+ console.log(`${c.yellow}Unknown command: ${cmd}${c.reset}`);
1337
+ console.log(`Type ${c.cyan}/help${c.reset} for available commands.`);
1338
+ rl.prompt();
1339
+ });
1340
+
1341
+ rl.on('close', () => {
1342
+ process.exit(0);
1343
+ });
1344
+ }
1345
+
680
1346
  main().catch(console.error);