cipher-security 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.
Files changed (76) hide show
  1. package/bin/cipher.js +566 -0
  2. package/lib/api/billing.js +321 -0
  3. package/lib/api/compliance.js +693 -0
  4. package/lib/api/controls.js +1401 -0
  5. package/lib/api/index.js +49 -0
  6. package/lib/api/marketplace.js +467 -0
  7. package/lib/api/openai-proxy.js +383 -0
  8. package/lib/api/server.js +685 -0
  9. package/lib/autonomous/feedback-loop.js +554 -0
  10. package/lib/autonomous/framework.js +512 -0
  11. package/lib/autonomous/index.js +97 -0
  12. package/lib/autonomous/leaderboard.js +594 -0
  13. package/lib/autonomous/modes/architect.js +412 -0
  14. package/lib/autonomous/modes/blue.js +386 -0
  15. package/lib/autonomous/modes/incident.js +684 -0
  16. package/lib/autonomous/modes/privacy.js +369 -0
  17. package/lib/autonomous/modes/purple.js +294 -0
  18. package/lib/autonomous/modes/recon.js +250 -0
  19. package/lib/autonomous/parallel.js +587 -0
  20. package/lib/autonomous/researcher.js +583 -0
  21. package/lib/autonomous/runner.js +955 -0
  22. package/lib/autonomous/scheduler.js +615 -0
  23. package/lib/autonomous/task-parser.js +127 -0
  24. package/lib/autonomous/validators/forensic.js +266 -0
  25. package/lib/autonomous/validators/osint.js +216 -0
  26. package/lib/autonomous/validators/privacy.js +296 -0
  27. package/lib/autonomous/validators/purple.js +298 -0
  28. package/lib/autonomous/validators/sigma.js +248 -0
  29. package/lib/autonomous/validators/threat-model.js +363 -0
  30. package/lib/benchmark/agent.js +119 -0
  31. package/lib/benchmark/baselines.js +43 -0
  32. package/lib/benchmark/builder.js +143 -0
  33. package/lib/benchmark/config.js +35 -0
  34. package/lib/benchmark/coordinator.js +91 -0
  35. package/lib/benchmark/index.js +20 -0
  36. package/lib/benchmark/llm.js +58 -0
  37. package/lib/benchmark/models.js +137 -0
  38. package/lib/benchmark/reporter.js +103 -0
  39. package/lib/benchmark/runner.js +103 -0
  40. package/lib/benchmark/sandbox.js +96 -0
  41. package/lib/benchmark/scorer.js +32 -0
  42. package/lib/benchmark/solver.js +166 -0
  43. package/lib/benchmark/tools.js +62 -0
  44. package/lib/bot/bot.js +238 -0
  45. package/lib/brand.js +105 -0
  46. package/lib/commands.js +100 -0
  47. package/lib/complexity.js +377 -0
  48. package/lib/config.js +213 -0
  49. package/lib/gateway/client.js +309 -0
  50. package/lib/gateway/commands.js +991 -0
  51. package/lib/gateway/config-validate.js +109 -0
  52. package/lib/gateway/gateway.js +367 -0
  53. package/lib/gateway/index.js +62 -0
  54. package/lib/gateway/mode.js +309 -0
  55. package/lib/gateway/plugins.js +222 -0
  56. package/lib/gateway/prompt.js +214 -0
  57. package/lib/mcp/server.js +262 -0
  58. package/lib/memory/compressor.js +425 -0
  59. package/lib/memory/engine.js +763 -0
  60. package/lib/memory/evolution.js +668 -0
  61. package/lib/memory/index.js +58 -0
  62. package/lib/memory/orchestrator.js +506 -0
  63. package/lib/memory/retriever.js +515 -0
  64. package/lib/memory/synthesizer.js +333 -0
  65. package/lib/pipeline/async-scanner.js +510 -0
  66. package/lib/pipeline/binary-analysis.js +1043 -0
  67. package/lib/pipeline/dom-xss-scanner.js +435 -0
  68. package/lib/pipeline/github-actions.js +792 -0
  69. package/lib/pipeline/index.js +124 -0
  70. package/lib/pipeline/osint.js +498 -0
  71. package/lib/pipeline/sarif.js +373 -0
  72. package/lib/pipeline/scanner.js +880 -0
  73. package/lib/pipeline/template-manager.js +525 -0
  74. package/lib/pipeline/xss-scanner.js +353 -0
  75. package/lib/setup-wizard.js +288 -0
  76. package/package.json +31 -0
package/bin/cipher.js ADDED
@@ -0,0 +1,566 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 defconxt. All rights reserved.
3
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
4
+ // CIPHER is a trademark of defconxt.
5
+
6
+ /**
7
+ * cipher — CLI entry point for the CIPHER security engineering platform.
8
+ *
9
+ * Fast path: --version / -V / --help / -h use only node:fs + node:path
10
+ * (zero dynamic imports, <200ms cold start).
11
+ *
12
+ * Command path: dynamically imports python-bridge.js, spawns the Python
13
+ * gateway, routes JSON-RPC commands, prints results, and exits.
14
+ */
15
+
16
+ import { readFileSync } from 'node:fs';
17
+ import { resolve, dirname } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { spawn } from 'node:child_process';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Fast path — no dynamic imports, must complete in <200ms
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const args = process.argv.slice(2);
26
+ const __dirname = dirname(fileURLToPath(import.meta.url));
27
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8'));
28
+
29
+ if (args[0] === '--version' || args[0] === '-V') {
30
+ console.log(pkg.version);
31
+ process.exit(0);
32
+ }
33
+
34
+ if (args[0] === '--help' || args[0] === '-h') {
35
+ const CYN = '\x1b[36m';
36
+ const B = '\x1b[1m';
37
+ const D = '\x1b[2m';
38
+ const R = '\x1b[0m';
39
+ const GRN = '\x1b[32m';
40
+
41
+ console.log(`${CYN}${B}
42
+ ██████╗██╗██████╗ ██╗ ██╗███████╗██████╗
43
+ ██╔════╝██║██╔══██╗██║ ██║██╔════╝██╔══██╗
44
+ ██║ ██║██████╔╝███████║█████╗ ██████╔╝
45
+ ██║ ██║██╔═══╝ ██╔══██║██╔══╝ ██╔══██╗
46
+ ╚██████╗██║██║ ██║ ██║███████╗██║ ██║
47
+ ╚═════╝╚═╝╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝${R}
48
+ ${D}Claude Integrated Privacy & Hardening Expert Resource${R} ${D}v${pkg.version}${R}
49
+
50
+ ${B}Usage:${R} cipher [command] [options]
51
+ cipher <query> ${D}Freeform security query${R}
52
+
53
+ ${B}Commands:${R}
54
+ ${GRN}query${R} Run a security query
55
+ ${GRN}scan${R} Run a security scan
56
+ ${GRN}compliance${R} Run compliance checks (39 frameworks)
57
+ ${GRN}search${R} Search security data
58
+ ${GRN}osint${R} OSINT intelligence tools
59
+ ${GRN}diff${R} Analyze git diff for security issues
60
+ ${GRN}sarif${R} SARIF report tools
61
+
62
+ ${GRN}status${R} Show system status
63
+ ${GRN}doctor${R} Diagnose installation health
64
+ ${GRN}version${R} Print version information
65
+ ${GRN}setup${R} Run setup wizard
66
+ ${GRN}update${R} Update CIPHER to latest version
67
+
68
+ ${GRN}domains${R} List skill domains
69
+ ${GRN}skills${R} Search skills
70
+ ${GRN}store${R} Store findings in memory
71
+ ${GRN}stats${R} Show statistics
72
+ ${GRN}leaderboard${R} Skill effectiveness metrics
73
+ ${GRN}marketplace${R} Browse skill marketplace
74
+
75
+ ${GRN}api${R} Start REST API server
76
+ ${GRN}mcp${R} Start MCP server (stdio)
77
+ ${GRN}bot${R} Start Signal bot
78
+
79
+ ${GRN}workflow${R} Generate CI/CD security workflow
80
+ ${GRN}memory-export${R} Export memory to JSON
81
+ ${GRN}memory-import${R} Import memory from JSON
82
+ ${GRN}feedback${R} Run skill improvement loop
83
+ ${GRN}ingest${R} Re-index knowledge base
84
+ ${GRN}plugin${R} Manage plugins
85
+ ${GRN}setup-signal${R} Configure Signal integration
86
+ ${GRN}score${R} Score response quality
87
+ ${GRN}dashboard${R} System dashboard
88
+ ${GRN}web${R} Web interface (use cipher api)
89
+
90
+ ${B}Options:${R}
91
+ --version, -V Print version and exit
92
+ --help, -h Print this help and exit
93
+ --autonomous Run in autonomous mode (cipher blue --autonomous "task")
94
+ --backend Override LLM backend (ollama, claude, litellm)
95
+ --no-stream Disable streaming output
96
+
97
+ ${B}Environment:${R}
98
+ CIPHER_DEBUG=1 Enable verbose debug logging
99
+
100
+ ${D}Documentation: https://github.com/defconxt/CIPHER${R}`);
101
+ process.exit(0);
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // No arguments: check if config exists → wizard or help
106
+ // ---------------------------------------------------------------------------
107
+
108
+ if (args.length === 0) {
109
+ try {
110
+ const { configExists } = await import('../lib/config.js');
111
+ if (!configExists()) {
112
+ const { runSetupWizard } = await import('../lib/setup-wizard.js');
113
+ await runSetupWizard();
114
+ process.exit(0);
115
+ }
116
+ } catch (err) {
117
+ console.error(`Setup wizard failed: ${err.message || err}`);
118
+ process.exit(1);
119
+ }
120
+ // Config exists but no command — show help hint
121
+ console.error('No command specified. Run `cipher --help` for usage.');
122
+ process.exit(1);
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Node.js-native commands — handled before the Python bridge is loaded
127
+ // ---------------------------------------------------------------------------
128
+
129
+ const nativeCommands = new Set(['setup']);
130
+
131
+ if (nativeCommands.has(args[0])) {
132
+ try {
133
+ const { runSetupWizard } = await import('../lib/setup-wizard.js');
134
+ await runSetupWizard();
135
+ process.exit(0);
136
+ } catch (err) {
137
+ console.error(`Setup wizard failed: ${err.message || err}`);
138
+ process.exit(1);
139
+ }
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Extract query-specific flags before general command parsing
144
+ // ---------------------------------------------------------------------------
145
+
146
+ let queryBackendOverride = null;
147
+ let queryNoStream = false;
148
+ let autonomousMode = null;
149
+
150
+ /** Set of valid autonomous mode names (lowercased). */
151
+ const MODE_NAMES = new Set(['red', 'blue', 'incident', 'purple', 'recon', 'privacy', 'architect']);
152
+
153
+ /** Clean args with --backend, --no-stream, and --autonomous extracted */
154
+ const cleanedArgs = [];
155
+ for (let i = 0; i < args.length; i++) {
156
+ if (args[i] === '--backend' && i + 1 < args.length) {
157
+ queryBackendOverride = args[i + 1];
158
+ i++; // skip value
159
+ } else if (args[i] === '--no-stream') {
160
+ queryNoStream = true;
161
+ } else if (args[i] === '--autonomous') {
162
+ // --autonomous flag found — look for a mode name in the remaining args.
163
+ // The mode name can be adjacent (cipher --autonomous blue "task")
164
+ // or positional-first (cipher blue --autonomous "task").
165
+ // We'll collect it after the loop from cleanedArgs.
166
+ autonomousMode = '__pending__';
167
+ } else {
168
+ cleanedArgs.push(args[i]);
169
+ }
170
+ }
171
+
172
+ // If --autonomous was found, extract the mode name from cleaned positional args
173
+ if (autonomousMode === '__pending__') {
174
+ // Check the first positional arg (non-flag) for a mode name
175
+ const firstPositional = cleanedArgs.find(a => !a.startsWith('-'));
176
+ if (firstPositional && MODE_NAMES.has(firstPositional.toLowerCase())) {
177
+ autonomousMode = firstPositional.toLowerCase();
178
+ // Remove the mode name from cleanedArgs so parseCommand() doesn't see it
179
+ const idx = cleanedArgs.indexOf(firstPositional);
180
+ cleanedArgs.splice(idx, 1);
181
+ } else {
182
+ console.error(
183
+ `Error: --autonomous requires a valid mode name.\n` +
184
+ `Available modes: ${[...MODE_NAMES].sort().join(', ')}\n\n` +
185
+ `Usage: cipher <mode> --autonomous "task description"\n` +
186
+ `Example: cipher blue --autonomous "Generate Sigma rules for T1059.001"`
187
+ );
188
+ process.exit(1);
189
+ }
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Command routing — dynamic imports below this point
194
+ // ---------------------------------------------------------------------------
195
+
196
+ const knownCommands = new Set([
197
+ // Gateway
198
+ 'query', 'ingest', 'status', 'doctor', 'setup-signal',
199
+ 'dashboard', 'web', 'version', 'plugin',
200
+ // Pipeline
201
+ 'scan', 'search', 'store', 'diff', 'workflow', 'stats', 'domains',
202
+ 'skills', 'score', 'marketplace', 'compliance', 'leaderboard',
203
+ 'feedback', 'memory-export', 'memory-import', 'sarif', 'osint', 'update',
204
+ // Services
205
+ 'bot', 'mcp', 'api',
206
+ ]);
207
+
208
+ /**
209
+ * Parse argv into a command name and remaining args.
210
+ * First non-flag argument is checked against knownCommands.
211
+ * If not a known command, treat all non-flag words as a bare query.
212
+ *
213
+ * @param {string[]} argv
214
+ * @returns {{ command: string, commandArgs: string[] }}
215
+ */
216
+ function parseCommand(argv) {
217
+ const flags = [];
218
+ const positional = [];
219
+
220
+ for (const arg of argv) {
221
+ if (arg.startsWith('-')) {
222
+ flags.push(arg);
223
+ } else {
224
+ positional.push(arg);
225
+ }
226
+ }
227
+
228
+ if (positional.length === 0) {
229
+ // No positional args — show help
230
+ console.error('No command specified. Run `cipher --help` for usage.');
231
+ process.exit(1);
232
+ }
233
+
234
+ const first = positional[0];
235
+
236
+ if (knownCommands.has(first)) {
237
+ return {
238
+ command: first,
239
+ commandArgs: [...positional.slice(1), ...flags],
240
+ };
241
+ }
242
+
243
+ // Bare query — join all positional words as query text
244
+ return {
245
+ command: 'query',
246
+ commandArgs: [...positional, ...flags],
247
+ };
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // Bridge result formatter
252
+ // ---------------------------------------------------------------------------
253
+
254
+ /**
255
+ * Format and print a bridge command result, then exit if needed.
256
+ *
257
+ * Result shapes from bridge.py:
258
+ * 1. {output, exit_code, error: true} — command error: print output to stderr, exit with code
259
+ * 2. {output, exit_code} — text-mode result: print output directly
260
+ * 3. Plain object (no output field) — structured JSON: pretty-print as JSON
261
+ * 4. String — legacy: print directly
262
+ * 5. null/undefined — no output
263
+ *
264
+ * @param {*} result - Bridge command result
265
+ */
266
+ async function formatBridgeResult(result, commandName = '') {
267
+ if (result === null || result === undefined) {
268
+ return;
269
+ }
270
+
271
+ if (typeof result === 'string') {
272
+ console.log(result);
273
+ return;
274
+ }
275
+
276
+ if (typeof result === 'object') {
277
+ // Error result — print to stderr and exit with error code
278
+ if (result.error === true) {
279
+ if (result.message) {
280
+ process.stderr.write(result.message + '\n');
281
+ } else if (result.output) {
282
+ process.stderr.write(result.output + '\n');
283
+ }
284
+ process.exit(result.exit_code || 1);
285
+ }
286
+
287
+ // Branded output for specific commands
288
+ if (commandName === 'version' && result.version) {
289
+ const { formatVersion } = await import('../lib/brand.js');
290
+ process.stderr.write(formatVersion(result.version) + '\n');
291
+ console.log(JSON.stringify(result, null, 2));
292
+ return;
293
+ }
294
+
295
+ if (commandName === 'doctor' && result.checks) {
296
+ const { header, formatDoctorChecks, divider, success, warn } = await import('../lib/brand.js');
297
+ header('Health Check');
298
+ divider();
299
+ process.stderr.write(formatDoctorChecks(result.checks) + '\n');
300
+ divider();
301
+ if (result.healthy) {
302
+ success(`All checks passed (${result.summary})`);
303
+ } else {
304
+ warn(result.summary);
305
+ }
306
+ console.log(JSON.stringify(result, null, 2));
307
+ return;
308
+ }
309
+
310
+ if (commandName === 'status' && result.status) {
311
+ const { header, status: statusDot, divider } = await import('../lib/brand.js');
312
+ header('Status');
313
+ divider();
314
+ statusDot('Backend', result.backend ? 'ok' : 'warn', result.backend || 'not configured');
315
+ statusDot('Config', result.config?.valid ? 'ok' : 'fail', result.config?.valid ? 'valid' : 'invalid or missing');
316
+ if (result.memory) {
317
+ statusDot('Memory', 'ok', `${result.memory.total} entries (${result.memory.active} active)`);
318
+ }
319
+ divider();
320
+ console.log(JSON.stringify(result, null, 2));
321
+ return;
322
+ }
323
+
324
+ // Text-mode result — print output directly (pre-formatted from CliRunner)
325
+ if ('output' in result) {
326
+ if (result.output) {
327
+ console.log(result.output);
328
+ }
329
+ if (result.exit_code && result.exit_code !== 0) {
330
+ process.exit(result.exit_code);
331
+ }
332
+ return;
333
+ }
334
+
335
+ // Structured JSON result — pretty-print (skip empty objects from branded commands)
336
+ if (Object.keys(result).length > 0) {
337
+ console.log(JSON.stringify(result, null, 2));
338
+ }
339
+ return;
340
+ }
341
+
342
+ // Fallback for unexpected types
343
+ console.log(String(result));
344
+ }
345
+
346
+ // ---------------------------------------------------------------------------
347
+ // Dispatch helpers + autonomous/standard routing
348
+ // ---------------------------------------------------------------------------
349
+
350
+ const { command, commandArgs } = parseCommand(cleanedArgs);
351
+
352
+ const debug = process.env.CIPHER_DEBUG === '1';
353
+
354
+
355
+ // ---------------------------------------------------------------------------
356
+ // Autonomous mode dispatch — bypasses normal command routing entirely
357
+ // ---------------------------------------------------------------------------
358
+
359
+ if (autonomousMode) {
360
+ // Build task text from remaining non-flag args
361
+ const taskText = commandArgs.filter(a => !a.startsWith('-')).join(' ')
362
+ || (command === 'query' ? commandArgs.join(' ') : '');
363
+
364
+ if (debug) {
365
+ process.stderr.write(
366
+ `[bridge:node] Autonomous dispatch: mode=${autonomousMode} task="${taskText}"\n`
367
+ );
368
+ }
369
+
370
+ try {
371
+ const { runAutonomous } = await import('../lib/autonomous/runner.js');
372
+ const { parseTaskInput } = await import('../lib/autonomous/task-parser.js');
373
+
374
+ const taskInput = parseTaskInput(autonomousMode.toUpperCase(), taskText);
375
+
376
+ const result = await runAutonomous(
377
+ autonomousMode,
378
+ taskInput,
379
+ queryBackendOverride,
380
+ );
381
+
382
+ // Format result
383
+ if (result.error) {
384
+ process.stderr.write(`Error: ${result.error}\n`);
385
+ process.exit(1);
386
+ }
387
+
388
+ // Print output
389
+ if (result.outputText) {
390
+ process.stdout.write(result.outputText + '\n');
391
+ }
392
+
393
+ // Print validation warnings to stderr
394
+ if (result.validation && !result.validation.valid) {
395
+ process.stderr.write('Validation warnings:\n');
396
+ for (const err of (result.validation.errors || [])) {
397
+ process.stderr.write(` - ${err}\n`);
398
+ }
399
+ }
400
+ } catch (err) {
401
+ const msg = err.message || String(err);
402
+ process.stderr.write(`Error: ${msg}\n`);
403
+ process.exit(1);
404
+ }
405
+
406
+ process.exit(0);
407
+ }
408
+
409
+ // ---------------------------------------------------------------------------
410
+ // Standard dispatch: passthrough vs native
411
+ // ---------------------------------------------------------------------------
412
+
413
+ const { getCommandMode } = await import('../lib/commands.js');
414
+ const mode = getCommandMode(command);
415
+
416
+ if (mode === 'native') {
417
+ // ── Native dispatch — Node.js handler functions (no Python) ────────────
418
+ if (debug) {
419
+ process.stderr.write(`[bridge:node] Dispatching command=${command} mode=native\n`);
420
+ }
421
+
422
+ /**
423
+ * Convert kebab-case command name to handler function name.
424
+ * 'memory-export' → 'handleMemoryExport'
425
+ * 'stats' → 'handleStats'
426
+ *
427
+ * @param {string} cmd
428
+ * @returns {string}
429
+ */
430
+ function toHandlerName(cmd) {
431
+ return 'handle' + cmd
432
+ .split('-')
433
+ .map(w => w[0].toUpperCase() + w.slice(1))
434
+ .join('');
435
+ }
436
+
437
+ try {
438
+ // ── API server command: long-running HTTP server (no Python) ──────────
439
+ if (command === 'api') {
440
+ const { createAPIServer, APIConfig } = await import('../lib/api/server.js');
441
+ const apiPort = commandArgs.find((a, i) => commandArgs[i - 1] === '--port') || '8443';
442
+ const apiHost = commandArgs.find((a, i) => commandArgs[i - 1] === '--host') || '127.0.0.1';
443
+ const noAuth = commandArgs.includes('--no-auth');
444
+ const cfg = new APIConfig({
445
+ port: parseInt(apiPort, 10),
446
+ host: apiHost,
447
+ requireAuth: !noAuth,
448
+ });
449
+ const api = createAPIServer(cfg);
450
+ const actualPort = await api.start();
451
+ console.log(`CIPHER API listening on ${cfg.host}:${actualPort}`);
452
+ // Keep process alive — Ctrl+C to stop
453
+ process.on('SIGINT', async () => {
454
+ await api.stop();
455
+ console.log('\nShutdown complete.');
456
+ process.exit(0);
457
+ });
458
+ process.on('SIGTERM', async () => {
459
+ await api.stop();
460
+ process.exit(0);
461
+ });
462
+ // ── MCP server command: JSON-RPC over stdio ──────────────────────────
463
+ } else if (command === 'mcp') {
464
+ const { CipherMCPServer } = await import('../lib/mcp/server.js');
465
+ const mcp = new CipherMCPServer();
466
+ mcp.startStdio();
467
+ // ── Bot command: Signal bot ──────────────────────────────────────────
468
+ } else if (command === 'bot') {
469
+ const { banner, header, info, warn, success, divider } = await import('../lib/brand.js');
470
+ const { loadConfig, configExists } = await import('../lib/config.js');
471
+
472
+ banner(pkg.version);
473
+ header('Signal Bot');
474
+
475
+ // Load signal config
476
+ let signalCfg = {};
477
+ if (configExists()) {
478
+ try {
479
+ const cfg = loadConfig();
480
+ signalCfg = cfg.signal || {};
481
+ } catch { /* ignore */ }
482
+ }
483
+
484
+ const svc = commandArgs.find((a, i) => commandArgs[i - 1] === '--service')
485
+ || process.env.SIGNAL_SERVICE || signalCfg.signal_service || '';
486
+ const phone = commandArgs.find((a, i) => commandArgs[i - 1] === '--phone')
487
+ || process.env.SIGNAL_PHONE_NUMBER || signalCfg.phone_number || '';
488
+ const whitelist = process.env.SIGNAL_WHITELIST
489
+ ? process.env.SIGNAL_WHITELIST.split(',').map(n => n.trim())
490
+ : signalCfg.whitelist || [];
491
+
492
+ if (!svc || !phone) {
493
+ divider();
494
+ warn('Signal is not configured.');
495
+ info('Run: cipher setup-signal');
496
+ divider();
497
+ process.exit(1);
498
+ }
499
+
500
+ divider();
501
+ success(`Service: ${svc}`);
502
+ success(`Phone: ${phone}`);
503
+ if (whitelist.length) {
504
+ success(`Whitelist: ${whitelist.join(', ')}`);
505
+ } else {
506
+ warn('Whitelist: none (all senders allowed)');
507
+ }
508
+ divider();
509
+ info('Starting bot... (Ctrl+C to stop)');
510
+
511
+ const { runBot } = await import('../lib/bot/bot.js');
512
+ await runBot({ signalService: svc, phoneNumber: phone, whitelist, sessionTimeout: signalCfg.session_timeout || 3600 });
513
+ // ── Query command: streaming via Gateway or non-streaming via handler ──
514
+ } else if (command === 'query') {
515
+ const queryText = commandArgs.filter(a => !a.startsWith('-')).join(' ');
516
+
517
+ if (queryNoStream) {
518
+ // --no-stream: use handleQuery for non-streaming response
519
+ const { handleQuery } = await import('../lib/gateway/commands.js');
520
+ const result = await handleQuery({ message: queryText });
521
+ await formatBridgeResult(result, command);
522
+ } else {
523
+ // Streaming path — Gateway.sendStream() → async iterator → stdout
524
+ const { Gateway } = await import('../lib/gateway/gateway.js');
525
+ const gw = new Gateway({
526
+ backendOverride: queryBackendOverride || undefined,
527
+ rag: true,
528
+ });
529
+
530
+ for await (const token of gw.sendStream(queryText)) {
531
+ process.stdout.write(token);
532
+ }
533
+ process.stdout.write('\n');
534
+ }
535
+ } else {
536
+ // ── Non-query native commands ──────────────────────────────────────
537
+ const handlers = await import('../lib/gateway/commands.js');
538
+ const handlerName = toHandlerName(command);
539
+ const handler = handlers[handlerName];
540
+
541
+ if (!handler) {
542
+ console.error(`Unknown native command: ${command}`);
543
+ process.exit(1);
544
+ }
545
+
546
+ const result = await handler(commandArgs);
547
+
548
+ // Native handlers return {error: true, message: "..."} for errors.
549
+ // formatBridgeResult expects {error: true, output: "...", exit_code: N}.
550
+ // Bridge the shape difference here.
551
+ if (result && result.error === true && result.message && !('output' in result)) {
552
+ process.stderr.write(result.message + '\n');
553
+ process.exit(result.exit_code || 1);
554
+ } else {
555
+ await formatBridgeResult(result, command);
556
+ }
557
+ }
558
+ } catch (err) {
559
+ console.error(`Error: ${err.message || err}`);
560
+ process.exit(1);
561
+ }
562
+ } else {
563
+ // Unknown command mode — should not happen
564
+ console.error(`Unknown command: ${command}. Run \`cipher --help\` for usage.`);
565
+ process.exit(1);
566
+ }