cipher-security 2.0.4 → 2.0.6
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/cipher.js +113 -18
- package/lib/commands.js +1 -3
- package/lib/gateway/commands.js +125 -50
- package/lib/gateway/index.js +0 -2
- package/lib/mcp/server.js +241 -14
- package/lib/pipeline/index.js +3 -1
- package/lib/pipeline/osint.js +488 -239
- package/lib/pipeline/scanner.js +67 -3
- package/package.json +1 -1
package/bin/cipher.js
CHANGED
|
@@ -72,7 +72,7 @@ ${B}Commands:${R}
|
|
|
72
72
|
|
|
73
73
|
${GRN}api${R} Start REST API server
|
|
74
74
|
${GRN}mcp${R} Start MCP server (stdio)
|
|
75
|
-
${GRN}bot${R} Start Signal bot
|
|
75
|
+
${GRN}bot${R} Start Signal bot (background)
|
|
76
76
|
|
|
77
77
|
${GRN}workflow${R} Generate CI/CD security workflow
|
|
78
78
|
${GRN}memory-export${R} Export memory to JSON
|
|
@@ -82,8 +82,6 @@ ${B}Commands:${R}
|
|
|
82
82
|
${GRN}plugin${R} Manage plugins
|
|
83
83
|
${GRN}setup-signal${R} Configure Signal integration
|
|
84
84
|
${GRN}score${R} Score response quality
|
|
85
|
-
${GRN}dashboard${R} System dashboard
|
|
86
|
-
${GRN}web${R} Web interface (use cipher api)
|
|
87
85
|
|
|
88
86
|
${B}Options:${R}
|
|
89
87
|
--version, -V Print version and exit
|
|
@@ -194,7 +192,7 @@ if (autonomousMode === '__pending__') {
|
|
|
194
192
|
const knownCommands = new Set([
|
|
195
193
|
// Gateway
|
|
196
194
|
'query', 'ingest', 'status', 'doctor', 'setup-signal',
|
|
197
|
-
'
|
|
195
|
+
'version', 'plugin',
|
|
198
196
|
// Pipeline
|
|
199
197
|
'scan', 'search', 'store', 'diff', 'workflow', 'stats', 'domains',
|
|
200
198
|
'skills', 'score', 'marketplace', 'compliance', 'leaderboard',
|
|
@@ -456,11 +454,74 @@ if (mode === 'native') {
|
|
|
456
454
|
mcp.startStdio();
|
|
457
455
|
// ── Bot command: Signal bot ──────────────────────────────────────────
|
|
458
456
|
} else if (command === 'bot') {
|
|
459
|
-
const { banner, header, info, warn, success, divider } = await import('../lib/brand.js');
|
|
457
|
+
const { banner, header, info, warn, success, divider, error: brandError } = await import('../lib/brand.js');
|
|
460
458
|
const { loadConfig, configExists } = await import('../lib/config.js');
|
|
459
|
+
const { existsSync, readFileSync, unlinkSync } = await import('node:fs');
|
|
460
|
+
const { join } = await import('node:path');
|
|
461
|
+
const { homedir } = await import('node:os');
|
|
462
|
+
|
|
463
|
+
const pidFile = join(homedir(), '.cipher', 'bot.pid');
|
|
464
|
+
const logFile = join(homedir(), '.cipher', 'bot.log');
|
|
465
|
+
const subAction = commandArgs[0];
|
|
466
|
+
|
|
467
|
+
// ── cipher bot stop ────────────────────────────────────────────
|
|
468
|
+
if (subAction === 'stop') {
|
|
469
|
+
if (!existsSync(pidFile)) {
|
|
470
|
+
header('Signal Bot');
|
|
471
|
+
warn('Bot is not running (no PID file).');
|
|
472
|
+
process.exit(0);
|
|
473
|
+
}
|
|
474
|
+
const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
475
|
+
try {
|
|
476
|
+
process.kill(pid, 'SIGTERM');
|
|
477
|
+
unlinkSync(pidFile);
|
|
478
|
+
header('Signal Bot');
|
|
479
|
+
success(`Bot stopped (PID ${pid}).`);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
// Process already gone
|
|
482
|
+
unlinkSync(pidFile);
|
|
483
|
+
header('Signal Bot');
|
|
484
|
+
warn(`Bot process ${pid} not found — cleaned up stale PID file.`);
|
|
485
|
+
}
|
|
486
|
+
process.exit(0);
|
|
487
|
+
}
|
|
461
488
|
|
|
462
|
-
|
|
463
|
-
|
|
489
|
+
// ── cipher bot status ──────────────────────────────────────────
|
|
490
|
+
if (subAction === 'status') {
|
|
491
|
+
header('Signal Bot');
|
|
492
|
+
if (!existsSync(pidFile)) {
|
|
493
|
+
warn('Bot is not running.');
|
|
494
|
+
} else {
|
|
495
|
+
const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
496
|
+
try {
|
|
497
|
+
process.kill(pid, 0); // signal 0 = check if alive
|
|
498
|
+
success(`Bot is running (PID ${pid}).`);
|
|
499
|
+
info(`Log: ${logFile}`);
|
|
500
|
+
} catch {
|
|
501
|
+
warn(`Bot PID ${pid} is stale — not running.`);
|
|
502
|
+
unlinkSync(pidFile);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
process.exit(0);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── cipher bot (start) ─────────────────────────────────────────
|
|
509
|
+
// Fork as foreground if --foreground, otherwise daemonize
|
|
510
|
+
const foreground = commandArgs.includes('--foreground') || commandArgs.includes('-f');
|
|
511
|
+
|
|
512
|
+
// Check if already running (skip in foreground mode — we ARE the child)
|
|
513
|
+
if (!foreground && existsSync(pidFile)) {
|
|
514
|
+
const existingPid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
515
|
+
try {
|
|
516
|
+
process.kill(existingPid, 0);
|
|
517
|
+
header('Signal Bot');
|
|
518
|
+
warn(`Bot is already running (PID ${existingPid}).`);
|
|
519
|
+
info('Run: cipher bot stop');
|
|
520
|
+
process.exit(1);
|
|
521
|
+
} catch {
|
|
522
|
+
unlinkSync(pidFile); // stale PID file
|
|
523
|
+
}
|
|
524
|
+
}
|
|
464
525
|
|
|
465
526
|
// Load signal config
|
|
466
527
|
let signalCfg = {};
|
|
@@ -480,6 +541,7 @@ if (mode === 'native') {
|
|
|
480
541
|
: signalCfg.whitelist || [];
|
|
481
542
|
|
|
482
543
|
if (!svc || !phone) {
|
|
544
|
+
header('Signal Bot');
|
|
483
545
|
divider();
|
|
484
546
|
warn('Signal is not configured.');
|
|
485
547
|
info('Run: cipher setup-signal');
|
|
@@ -487,19 +549,52 @@ if (mode === 'native') {
|
|
|
487
549
|
process.exit(1);
|
|
488
550
|
}
|
|
489
551
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
success(`
|
|
552
|
+
if (foreground) {
|
|
553
|
+
banner(pkg.version);
|
|
554
|
+
header('Signal Bot (foreground)');
|
|
555
|
+
divider();
|
|
556
|
+
success(`Service: ${svc}`);
|
|
557
|
+
success(`Phone: ${phone}`);
|
|
558
|
+
if (whitelist.length) {
|
|
559
|
+
success(`Whitelist: ${whitelist.join(', ')}`);
|
|
560
|
+
} else {
|
|
561
|
+
warn('Whitelist: none (all senders allowed)');
|
|
562
|
+
}
|
|
563
|
+
divider();
|
|
564
|
+
info('Running in foreground (Ctrl+C to stop)...');
|
|
565
|
+
|
|
566
|
+
const { runBot } = await import('../lib/bot/bot.js');
|
|
567
|
+
await runBot({ signalService: svc, phoneNumber: phone, whitelist, sessionTimeout: signalCfg.session_timeout || 3600 });
|
|
495
568
|
} else {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
569
|
+
// Daemonize: spawn detached child with --foreground flag
|
|
570
|
+
const { spawn } = await import('node:child_process');
|
|
571
|
+
const { openSync, writeFileSync: writePid, mkdirSync } = await import('node:fs');
|
|
572
|
+
|
|
573
|
+
mkdirSync(join(homedir(), '.cipher'), { recursive: true });
|
|
574
|
+
const logFd = openSync(logFile, 'a');
|
|
575
|
+
|
|
576
|
+
const child = spawn(process.execPath, [
|
|
577
|
+
...process.argv.slice(1), '--foreground',
|
|
578
|
+
], {
|
|
579
|
+
detached: true,
|
|
580
|
+
stdio: ['ignore', logFd, logFd],
|
|
581
|
+
env: { ...process.env },
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
writePid(pidFile, String(child.pid), 'utf-8');
|
|
585
|
+
child.unref();
|
|
500
586
|
|
|
501
|
-
|
|
502
|
-
|
|
587
|
+
header('Signal Bot');
|
|
588
|
+
divider();
|
|
589
|
+
success(`Service: ${svc}`);
|
|
590
|
+
success(`Phone: ${phone}`);
|
|
591
|
+
divider();
|
|
592
|
+
success(`Bot started in background (PID ${child.pid}).`);
|
|
593
|
+
info(`Log: ${logFile}`);
|
|
594
|
+
info('Run: cipher bot stop — stop the bot');
|
|
595
|
+
info('Run: cipher bot status — check if running');
|
|
596
|
+
divider();
|
|
597
|
+
}
|
|
503
598
|
// ── Query command: streaming via Gateway or non-streaming via handler ──
|
|
504
599
|
} else if (command === 'query') {
|
|
505
600
|
const queryText = commandArgs.filter(a => !a.startsWith('-')).join(' ');
|
package/lib/commands.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* commands.js — Command routing table for the CIPHER CLI.
|
|
7
7
|
*
|
|
8
|
-
* Maps all
|
|
8
|
+
* Maps all 28 CLI commands to one of three dispatch modes:
|
|
9
9
|
* - native: Dispatched directly through Node.js handler functions (no Python)
|
|
10
10
|
* - passthrough: Legacy mode — no commands use this as of v2.0
|
|
11
11
|
* (full terminal access for Rich panels, Textual TUIs, long-running services)
|
|
@@ -49,8 +49,6 @@ export const COMMAND_MODES = {
|
|
|
49
49
|
// ── Passthrough commands (direct Python spawn, full terminal) ──────────
|
|
50
50
|
// These were Python-dependent — now ported to Node.js.
|
|
51
51
|
scan: { mode: 'native', description: 'Run a security scan' },
|
|
52
|
-
dashboard: { mode: 'native', description: 'System dashboard' },
|
|
53
|
-
web: { mode: 'native', description: 'Web interface (use `cipher api` instead)' },
|
|
54
52
|
bot: { mode: 'native', description: 'Manage bot integrations (long-running service)' },
|
|
55
53
|
mcp: { mode: 'native', description: 'MCP server tools (long-running service)' },
|
|
56
54
|
api: { mode: 'native', description: 'API management (long-running server)' },
|
package/lib/gateway/commands.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// CIPHER is a trademark of defconxt.
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* commands.js — Node.js handler functions for all
|
|
6
|
+
* commands.js — Node.js handler functions for all 28 CLI commands.
|
|
7
7
|
*
|
|
8
8
|
* Each handler accepts a plain args object and returns a plain JS object.
|
|
9
9
|
* No Rich formatting, no Typer framework — just data. The rendering
|
|
@@ -261,14 +261,25 @@ export async function handleMemoryExport(args = {}) {
|
|
|
261
261
|
const { CipherMemory } = await import('../memory/index.js');
|
|
262
262
|
const memory = new CipherMemory(defaultMemoryDir());
|
|
263
263
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
// Query all entries directly — memory.search('') returns nothing for empty queries
|
|
265
|
+
const rows = memory.symbolic.db.prepare(
|
|
266
|
+
'SELECT * FROM entries WHERE is_archived = 0 ORDER BY created_at DESC LIMIT 10000'
|
|
267
|
+
).all();
|
|
268
|
+
let entries = rows.map(r => ({
|
|
269
|
+
entry_id: r.entry_id,
|
|
270
|
+
content: r.content,
|
|
271
|
+
memory_type: r.memory_type,
|
|
272
|
+
severity: r.severity,
|
|
273
|
+
engagement_id: r.engagement_id,
|
|
274
|
+
mitre_attack: r.mitre_attack ? JSON.parse(r.mitre_attack) : [],
|
|
275
|
+
targets: r.targets ? JSON.parse(r.targets) : [],
|
|
276
|
+
tags: r.tags ? JSON.parse(r.tags) : [],
|
|
277
|
+
created_at: r.created_at,
|
|
278
|
+
}));
|
|
266
279
|
if (args.engagement) {
|
|
267
|
-
entries = entries.filter(e =>
|
|
268
|
-
(e.engagement_id || e.engagementId) === args.engagement
|
|
269
|
-
);
|
|
280
|
+
entries = entries.filter(e => e.engagement_id === args.engagement);
|
|
270
281
|
}
|
|
271
|
-
const output = args
|
|
282
|
+
const output = getFlagArg(args, 'output', '--output', 'cipher-memory-export.json');
|
|
272
283
|
const data = { entries, count: entries.length };
|
|
273
284
|
writeFileSync(output, JSON.stringify(data, null, 2), 'utf-8');
|
|
274
285
|
debug(`memory-export: ${entries.length} entries → ${output}`);
|
|
@@ -388,14 +399,26 @@ export async function handleStatus(args = {}) {
|
|
|
388
399
|
*/
|
|
389
400
|
export async function handleDiff(args = {}) {
|
|
390
401
|
let diffText = getArg(args, 'file');
|
|
391
|
-
|
|
402
|
+
|
|
403
|
+
// Check for stdin marker '-' — getArg filters it as a flag, so check raw args
|
|
404
|
+
const isStdin = Array.isArray(args) && args.includes('-');
|
|
405
|
+
|
|
406
|
+
if (!diffText && !isStdin) {
|
|
392
407
|
return { error: true, message: 'Usage: cipher diff <file.patch>\n cat changes.diff | cipher diff -' };
|
|
393
408
|
}
|
|
394
409
|
const { SecurityDiffAnalyzer } = await import('../pipeline/index.js');
|
|
395
410
|
const analyzer = new SecurityDiffAnalyzer();
|
|
396
411
|
|
|
412
|
+
// Read from stdin when argument is '-'
|
|
413
|
+
if (isStdin) {
|
|
414
|
+
const chunks = [];
|
|
415
|
+
for await (const chunk of process.stdin) {
|
|
416
|
+
chunks.push(chunk);
|
|
417
|
+
}
|
|
418
|
+
diffText = Buffer.concat(chunks).toString('utf-8');
|
|
419
|
+
}
|
|
397
420
|
// If it looks like a file path (no newlines, exists on disk), read it
|
|
398
|
-
if (diffText && !diffText.includes('\n') && existsSync(diffText)) {
|
|
421
|
+
else if (diffText && !diffText.includes('\n') && existsSync(diffText)) {
|
|
399
422
|
diffText = readFileSync(diffText, 'utf-8');
|
|
400
423
|
}
|
|
401
424
|
|
|
@@ -472,23 +495,50 @@ export async function handleSarif(args = {}) {
|
|
|
472
495
|
export async function handleOsint(args = {}) {
|
|
473
496
|
const target = getArg(args, 'target');
|
|
474
497
|
if (!target) {
|
|
475
|
-
return { error: true, message: 'Usage: cipher osint <
|
|
498
|
+
return { error: true, message: 'Usage: cipher osint <target> [--type domain|ip|username|email|url]\n\nInvestigation types:\n domain DNS, WHOIS, cert transparency, Wayback Machine, web tech, IP geolocation\n ip Reverse DNS, IP classification, geolocation\n username Search 400+ platforms via Sherlock\n email Check account existence on 120+ services via Holehe\n url Wayback Machine + archive.today snapshots' };
|
|
476
499
|
}
|
|
477
500
|
const { OSINTPipeline } = await import('../pipeline/index.js');
|
|
478
501
|
const pipeline = new OSINTPipeline();
|
|
479
|
-
const type = getFlagArg(args, 'type', '--type',
|
|
502
|
+
const type = getFlagArg(args, 'type', '--type', null);
|
|
503
|
+
|
|
504
|
+
// Auto-detect type if not specified
|
|
505
|
+
let investigationType = type;
|
|
506
|
+
if (!investigationType) {
|
|
507
|
+
if (target.includes('@')) investigationType = 'email';
|
|
508
|
+
else if (target.startsWith('http://') || target.startsWith('https://')) investigationType = 'url';
|
|
509
|
+
else if (/^\d+\.\d+\.\d+\.\d+$/.test(target) || (target.includes(':') && !target.includes('/'))) investigationType = 'ip';
|
|
510
|
+
else if (target.includes('/')) investigationType = 'url';
|
|
511
|
+
else if (target.includes('.')) investigationType = 'domain';
|
|
512
|
+
else investigationType = 'username';
|
|
513
|
+
}
|
|
480
514
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
:
|
|
515
|
+
let results;
|
|
516
|
+
switch (investigationType) {
|
|
517
|
+
case 'ip':
|
|
518
|
+
results = await pipeline.investigateIp(target);
|
|
519
|
+
break;
|
|
520
|
+
case 'username':
|
|
521
|
+
results = pipeline.investigateUsername(target);
|
|
522
|
+
break;
|
|
523
|
+
case 'email':
|
|
524
|
+
results = pipeline.investigateEmail(target);
|
|
525
|
+
break;
|
|
526
|
+
case 'url':
|
|
527
|
+
results = await pipeline.investigateUrl(target);
|
|
528
|
+
break;
|
|
529
|
+
case 'domain':
|
|
530
|
+
default:
|
|
531
|
+
results = await pipeline.investigateDomain(target);
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
484
534
|
|
|
485
535
|
const data = {
|
|
486
536
|
target,
|
|
487
|
-
type,
|
|
537
|
+
type: investigationType,
|
|
488
538
|
results: results.map(r => (typeof r.toDict === 'function' ? r.toDict() : r)),
|
|
489
539
|
summary: pipeline.summary(),
|
|
490
540
|
};
|
|
491
|
-
debug(`osint: ${target} (${
|
|
541
|
+
debug(`osint: ${target} (${investigationType}) → ${results.length} results`);
|
|
492
542
|
return data;
|
|
493
543
|
}
|
|
494
544
|
|
|
@@ -908,19 +958,74 @@ function countFiles(dir, filename, subdir) {
|
|
|
908
958
|
export async function handleScan(args = {}) {
|
|
909
959
|
const target = Array.isArray(args) ? args.find(a => !a.startsWith('-')) : args.target;
|
|
910
960
|
if (!target) {
|
|
911
|
-
return { error: true, message: 'Usage: cipher scan <target> [--profile <profile>]' };
|
|
961
|
+
return { error: true, message: 'Usage: cipher scan <target> [--profile <profile>]\n\nProfiles: quick, standard, pentest, recon, full\n quick Tech detection + known CVEs only (~30s)\n standard Common vulns + misconfigs (~2-5 min)\n pentest Full pentest template set (~5-15 min)\n recon Discovery + exposure (~2-5 min)\n full All templates (~15-30 min)' };
|
|
912
962
|
}
|
|
913
963
|
try {
|
|
914
964
|
const { NucleiRunner, ScanProfile } = await import('../pipeline/scanner.js');
|
|
915
965
|
const runner = new NucleiRunner();
|
|
966
|
+
|
|
967
|
+
if (!runner.available) {
|
|
968
|
+
return { error: true, message: 'nuclei is not installed.\nInstall: go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest' };
|
|
969
|
+
}
|
|
970
|
+
|
|
916
971
|
const profileArg = Array.isArray(args) ? (args.find((a, i) => args[i - 1] === '--profile') || 'standard') : (args.profile || 'standard');
|
|
917
|
-
|
|
972
|
+
|
|
973
|
+
// Quick profile — fast tech detection + critical CVEs only
|
|
974
|
+
let scanOpts;
|
|
975
|
+
if (profileArg === 'quick') {
|
|
976
|
+
// Quick mode: use specific fast templates instead of tag-based matching
|
|
977
|
+
// This avoids nuclei loading 900+ templates for the 'tech' tag
|
|
978
|
+
const quickTemplates = [
|
|
979
|
+
'http/technologies/tech-detect.yaml',
|
|
980
|
+
'http/technologies/waf-detect.yaml',
|
|
981
|
+
'dns/dns-waf-detect.yaml',
|
|
982
|
+
];
|
|
983
|
+
// Resolve template paths — check nuclei's default template dir
|
|
984
|
+
const { homedir: hd } = await import('node:os');
|
|
985
|
+
const { existsSync: exists } = await import('node:fs');
|
|
986
|
+
const { join: pjoin } = await import('node:path');
|
|
987
|
+
const templateBase = pjoin(hd(), 'nuclei-templates');
|
|
988
|
+
const resolvedPaths = quickTemplates
|
|
989
|
+
.map(t => pjoin(templateBase, t))
|
|
990
|
+
.filter(t => exists(t));
|
|
991
|
+
|
|
992
|
+
if (resolvedPaths.length > 0) {
|
|
993
|
+
scanOpts = { templatePaths: resolvedPaths, timeout: 30 };
|
|
994
|
+
} else {
|
|
995
|
+
// Fallback: use tags if template dir not found
|
|
996
|
+
scanOpts = {
|
|
997
|
+
profile: { tags: ['tech'], severity: ['info'], rateLimit: 150, bulkSize: 50, concurrency: 50, requestTimeout: 5, headless: false },
|
|
998
|
+
timeout: 120,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
process.stderr.write(' ● Scanning (quick — tech detection, ~5-10s)...\n');
|
|
1002
|
+
} else {
|
|
1003
|
+
scanOpts = { profile: ScanProfile.fromDomain(profileArg) };
|
|
1004
|
+
process.stderr.write(` ● Scanning (${profileArg} profile — this may take several minutes)...\n`);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const result = scanOpts.templatePaths
|
|
1008
|
+
? await runner.scanWithTemplates(target, scanOpts)
|
|
1009
|
+
: await runner.scan(target, scanOpts);
|
|
1010
|
+
|
|
1011
|
+
process.stderr.write(` ✔ Scan complete: ${(result.findings || []).length} finding(s) in ${result.durationSeconds || '?'}s\n`);
|
|
1012
|
+
|
|
918
1013
|
return {
|
|
919
1014
|
output: JSON.stringify({
|
|
920
1015
|
target,
|
|
921
1016
|
profile: profileArg,
|
|
922
|
-
|
|
923
|
-
|
|
1017
|
+
findings_count: (result.findings || []).length,
|
|
1018
|
+
findings: (result.findings || []).map(f => ({
|
|
1019
|
+
name: f.name || f.templateId,
|
|
1020
|
+
severity: f.severity,
|
|
1021
|
+
matched_at: f.matchedAt || f.host,
|
|
1022
|
+
template: f.templateId,
|
|
1023
|
+
description: f.description || '',
|
|
1024
|
+
})),
|
|
1025
|
+
stats: result.stats || {},
|
|
1026
|
+
duration_seconds: result.durationSeconds || 0,
|
|
1027
|
+
status: result.success ? 'completed' : 'completed_with_errors',
|
|
1028
|
+
errors: (result.errors || []).length > 0 ? result.errors : undefined,
|
|
924
1029
|
}, null, 2),
|
|
925
1030
|
};
|
|
926
1031
|
} catch (err) {
|
|
@@ -928,36 +1033,6 @@ export async function handleScan(args = {}) {
|
|
|
928
1033
|
}
|
|
929
1034
|
}
|
|
930
1035
|
|
|
931
|
-
// ---------------------------------------------------------------------------
|
|
932
|
-
// Dashboard command — stub for Node.js TUI (Textual TUI not ported)
|
|
933
|
-
// ---------------------------------------------------------------------------
|
|
934
|
-
|
|
935
|
-
export async function handleDashboard() {
|
|
936
|
-
const brand = await import('../brand.js');
|
|
937
|
-
brand.header('Dashboard');
|
|
938
|
-
brand.divider();
|
|
939
|
-
brand.info('Dashboard TUI is not yet available.');
|
|
940
|
-
brand.info('Use: cipher status \u2014 system status');
|
|
941
|
-
brand.info('Use: cipher doctor \u2014 health check');
|
|
942
|
-
brand.info('Use: cipher api \u2014 REST API server');
|
|
943
|
-
brand.divider();
|
|
944
|
-
return {};
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// ---------------------------------------------------------------------------
|
|
948
|
-
// Web command — delegates to API server
|
|
949
|
-
// ---------------------------------------------------------------------------
|
|
950
|
-
|
|
951
|
-
export async function handleWeb(args = {}) {
|
|
952
|
-
const brand = await import('../brand.js');
|
|
953
|
-
brand.header('Web Interface');
|
|
954
|
-
brand.divider();
|
|
955
|
-
brand.info('The web interface has been consolidated into the API server.');
|
|
956
|
-
brand.info('Use: cipher api --no-auth --port 8443');
|
|
957
|
-
brand.divider();
|
|
958
|
-
return {};
|
|
959
|
-
}
|
|
960
|
-
|
|
961
1036
|
// ---------------------------------------------------------------------------
|
|
962
1037
|
// Setup Signal command — Signal bot configuration
|
|
963
1038
|
// ---------------------------------------------------------------------------
|