cipher-security 5.1.0 → 5.3.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/cipher.js CHANGED
@@ -23,61 +23,81 @@ import { spawn } from 'node:child_process';
23
23
  // ---------------------------------------------------------------------------
24
24
 
25
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'));
26
28
 
27
29
  if (args[0] === '--version' || args[0] === '-V') {
28
- const __dirname = dirname(fileURLToPath(import.meta.url));
29
- const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8'));
30
30
  console.log(pkg.version);
31
31
  process.exit(0);
32
32
  }
33
33
 
34
34
  if (args[0] === '--help' || args[0] === '-h') {
35
- console.log(`CIPHER AI Security Engineering Platform
36
-
37
- Usage: cipher [command] [options]
38
- cipher <query> Freeform security query
39
-
40
- Commands:
41
- query Run a security query
42
- ingest Ingest security data
43
- status Show system status
44
- doctor Diagnose installation health
45
- setup Run setup wizard
46
- setup-signal Configure Signal integration
47
- dashboard Open the dashboard
48
- web Start the web interface
49
- version Print version information
50
- plugin Manage plugins
51
- scan Run a security scan
52
- search Search security data
53
- store Manage data stores
54
- diff Compare security states
55
- workflow Manage workflows
56
- stats Show statistics
57
- domains Manage domains
58
- skills Manage skills
59
- score Show security score
60
- marketplace Browse marketplace
61
- compliance Run compliance checks
62
- leaderboard Show leaderboard
63
- feedback Submit feedback
64
- memory-export Export memory
65
- memory-import Import memory
66
- sarif SARIF report tools
67
- osint OSINT intelligence tools
68
- update Update CIPHER to latest version
69
- bot Manage bot integrations
70
- mcp MCP server tools
71
- api API management
72
-
73
- Options:
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}
74
91
  --version, -V Print version and exit
75
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
76
96
 
77
- Environment:
97
+ ${B}Environment:${R}
78
98
  CIPHER_DEBUG=1 Enable verbose debug logging
79
99
 
80
- Documentation: https://github.com/defconxt/CIPHER`);
100
+ ${D}Documentation: https://github.com/defconxt/CIPHER${R}`);
81
101
  process.exit(0);
82
102
  }
83
103
 
@@ -274,7 +294,7 @@ async function formatBridgeResult(result, commandName = '') {
274
294
 
275
295
  if (commandName === 'doctor' && result.checks) {
276
296
  const { header, formatDoctorChecks, divider, success, warn } = await import('../lib/brand.js');
277
- header('Health Check', result.checks.find(c => c.name === 'Node.js')?.detail || '');
297
+ header('Health Check');
278
298
  divider();
279
299
  process.stderr.write(formatDoctorChecks(result.checks) + '\n');
280
300
  divider();
@@ -287,6 +307,20 @@ async function formatBridgeResult(result, commandName = '') {
287
307
  return;
288
308
  }
289
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
+
290
324
  // Text-mode result — print output directly (pre-formatted from CliRunner)
291
325
  if ('output' in result) {
292
326
  if (result.output) {
@@ -298,8 +332,10 @@ async function formatBridgeResult(result, commandName = '') {
298
332
  return;
299
333
  }
300
334
 
301
- // Structured JSON result — pretty-print
302
- console.log(JSON.stringify(result, null, 2));
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
+ }
303
339
  return;
304
340
  }
305
341
 
@@ -430,11 +466,50 @@ if (mode === 'native') {
430
466
  mcp.startStdio();
431
467
  // ── Bot command: Signal bot ──────────────────────────────────────────
432
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
+
433
511
  const { runBot } = await import('../lib/bot/bot.js');
434
- runBot({
435
- signalService: commandArgs.find((a, i) => commandArgs[i - 1] === '--service') || '',
436
- phoneNumber: commandArgs.find((a, i) => commandArgs[i - 1] === '--phone') || '',
437
- });
512
+ await runBot({ signalService: svc, phoneNumber: phone, whitelist, sessionTimeout: signalCfg.session_timeout || 3600 });
438
513
  // ── Query command: streaming via Gateway or non-streaming via handler ──
439
514
  } else if (command === 'query') {
440
515
  const queryText = commandArgs.filter(a => !a.startsWith('-')).join(' ');
package/lib/bot/bot.js CHANGED
@@ -2,20 +2,18 @@
2
2
  // Licensed under AGPL-3.0 — see LICENSE file for details.
3
3
 
4
4
  /**
5
- * CIPHER Signal Bot — format utilities and session manager.
5
+ * CIPHER Signal Bot — receives messages via signal-cli-rest-api WebSocket,
6
+ * dispatches through the gateway, and replies.
6
7
  */
7
8
 
9
+ import { WebSocket } from 'ws';
10
+
8
11
  // ---------------------------------------------------------------------------
9
12
  // Prefix mapping
10
13
  // ---------------------------------------------------------------------------
11
14
 
12
15
  const PREFIX_RE = /^(RED|BLUE|PURPLE|PRIVACY|RECON|INCIDENT|ARCHITECT):\s*/i;
13
16
 
14
- /**
15
- * Map 'MODE: query' short-prefix format to '[MODE: MODE] query'.
16
- * @param {string} text
17
- * @returns {string}
18
- */
19
17
  export function mapPrefix(text) {
20
18
  const m = text.match(PREFIX_RE);
21
19
  if (m) {
@@ -31,20 +29,15 @@ export function mapPrefix(text) {
31
29
  // ---------------------------------------------------------------------------
32
30
 
33
31
  const MD_PATTERNS = [
34
- [/```[^\n]*\n([\s\S]*?)```/g, '$1'], // Fenced code blocks
35
- [/^#{1,6}\s+/gm, ''], // Headers
36
- [/\*{2}([^*\n]+)\*{2}/g, '$1'], // Bold
37
- [/\*([^*\n]+)\*/g, '$1'], // Italic
38
- [/`([^`\n]+)`/g, '$1'], // Inline code
39
- [/\[([^\]]+)\]\([^)]+\)/g, '$1'], // Links
40
- [/^(\s*)[*-]\s+/gm, '$1• '], // Bullets
32
+ [/```[^\n]*\n([\s\S]*?)```/g, '$1'],
33
+ [/^#{1,6}\s+/gm, ''],
34
+ [/\*{2}([^*\n]+)\*{2}/g, '$1'],
35
+ [/\*([^*\n]+)\*/g, '$1'],
36
+ [/`([^`\n]+)`/g, '$1'],
37
+ [/\[([^\]]+)\]\([^)]+\)/g, '$1'],
38
+ [/^(\s*)[*-]\s+/gm, '$1• '],
41
39
  ];
42
40
 
43
- /**
44
- * Strip markdown formatting for plaintext Signal delivery.
45
- * @param {string} text
46
- * @returns {string}
47
- */
48
41
  export function stripMarkdown(text) {
49
42
  for (const [pattern, replacement] of MD_PATTERNS) {
50
43
  text = text.replace(pattern, replacement);
@@ -56,15 +49,7 @@ export function stripMarkdown(text) {
56
49
  // Session manager
57
50
  // ---------------------------------------------------------------------------
58
51
 
59
- /**
60
- * In-memory conversation history manager keyed by sender.
61
- */
62
52
  export class SessionManager {
63
- /**
64
- * @param {object} [opts]
65
- * @param {number} [opts.timeoutSeconds=3600]
66
- * @param {number} [opts.maxPairs=20]
67
- */
68
53
  constructor({ timeoutSeconds = 3600, maxPairs = 20 } = {}) {
69
54
  this._sessions = new Map();
70
55
  this._timeout = timeoutSeconds * 1000;
@@ -92,9 +77,7 @@ export class SessionManager {
92
77
  }
93
78
  }
94
79
 
95
- reset(sender) {
96
- this._sessions.delete(sender);
97
- }
80
+ reset(sender) { this._sessions.delete(sender); }
98
81
 
99
82
  cleanup() {
100
83
  const now = Date.now();
@@ -109,22 +92,147 @@ export class SessionManager {
109
92
  }
110
93
 
111
94
  // ---------------------------------------------------------------------------
112
- // Bot runner (placeholder — requires signal-cli subprocess)
95
+ // Signal REST API client
96
+ // ---------------------------------------------------------------------------
97
+
98
+ async function sendMessage(signalService, phoneNumber, recipient, message) {
99
+ const resp = await fetch(`${signalService}/v2/send`, {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({
103
+ message,
104
+ number: phoneNumber,
105
+ recipients: [recipient],
106
+ }),
107
+ });
108
+ if (!resp.ok) {
109
+ throw new Error(`Send failed: ${resp.status} ${await resp.text()}`);
110
+ }
111
+ }
112
+
113
+ async function sendReaction(signalService, phoneNumber, recipient, emoji, targetTimestamp) {
114
+ try {
115
+ await fetch(`${signalService}/v1/reactions/${phoneNumber}`, {
116
+ method: 'POST',
117
+ headers: { 'Content-Type': 'application/json' },
118
+ body: JSON.stringify({
119
+ reaction: emoji,
120
+ recipient,
121
+ timestamp: targetTimestamp,
122
+ }),
123
+ });
124
+ } catch { /* best-effort */ }
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Bot runner
113
129
  // ---------------------------------------------------------------------------
114
130
 
115
131
  /**
116
132
  * Start the CIPHER Signal bot.
117
133
  *
118
- * In the full implementation, this would:
119
- * 1. Load config from config.yaml
120
- * 2. Start signal-cli subprocess for receiving messages
121
- * 3. Register CipherCommand handler
122
- * 4. Run with exponential-backoff watchdog
134
+ * Connects to signal-cli-rest-api WebSocket, listens for messages,
135
+ * dispatches through the gateway, and replies.
123
136
  *
124
137
  * @param {object} config
138
+ * @param {string} config.signalService - signal-cli-rest-api URL
139
+ * @param {string} config.phoneNumber - bot's phone number
140
+ * @param {string[]} [config.whitelist] - allowed sender numbers
141
+ * @param {number} [config.sessionTimeout] - session timeout in seconds
125
142
  */
126
- export function runBot(config) {
127
- console.log(`CIPHER Bot starting (service: ${config.signalService}, phone: ${config.phoneNumber})`);
128
- console.log('Bot requires signal-cli see docs for setup.');
129
- // The actual bot would use signal-cli subprocess here
143
+ export async function runBot(config) {
144
+ const { signalService, phoneNumber, whitelist = [], sessionTimeout = 3600 } = config;
145
+ const sessions = new SessionManager({ timeoutSeconds: sessionTimeout });
146
+ const whitelistSet = new Set(whitelist);
147
+
148
+ const wsUrl = `${signalService.replace('http', 'ws')}/v1/receive/${phoneNumber}`;
149
+
150
+ let reconnectDelay = 1000;
151
+ const maxDelay = 60000;
152
+
153
+ function connect() {
154
+ const ws = new WebSocket(wsUrl);
155
+
156
+ ws.on('open', () => {
157
+ process.stderr.write(` ● Connected to ${wsUrl}\n`);
158
+ reconnectDelay = 1000;
159
+ });
160
+
161
+ ws.on('message', async (data) => {
162
+ try {
163
+ const envelope = JSON.parse(data.toString());
164
+ await handleEnvelope(envelope, config, sessions, whitelistSet);
165
+ } catch (err) {
166
+ process.stderr.write(` ✖ Message error: ${err.message}\n`);
167
+ }
168
+ });
169
+
170
+ ws.on('close', () => {
171
+ process.stderr.write(` ⚠ WebSocket closed. Reconnecting in ${reconnectDelay / 1000}s...\n`);
172
+ setTimeout(connect, reconnectDelay);
173
+ reconnectDelay = Math.min(reconnectDelay * 2, maxDelay);
174
+ });
175
+
176
+ ws.on('error', (err) => {
177
+ process.stderr.write(` ✖ WebSocket error: ${err.message}\n`);
178
+ });
179
+ }
180
+
181
+ connect();
182
+ }
183
+
184
+ async function handleEnvelope(envelope, config, sessions, whitelistSet) {
185
+ const msg = envelope.envelope?.dataMessage;
186
+ if (!msg || !msg.message) return;
187
+
188
+ const sender = envelope.envelope?.source;
189
+ if (!sender) return;
190
+
191
+ const text = msg.message.trim();
192
+ if (!text) return;
193
+
194
+ // Whitelist check
195
+ if (whitelistSet.size > 0 && !whitelistSet.has(sender)) {
196
+ return;
197
+ }
198
+
199
+ const timestamp = msg.timestamp;
200
+
201
+ // /reset command
202
+ if (text === '/reset') {
203
+ sessions.reset(sender);
204
+ await sendMessage(config.signalService, config.phoneNumber, sender, 'Session cleared.');
205
+ return;
206
+ }
207
+
208
+ // React with brain emoji to acknowledge
209
+ await sendReaction(config.signalService, config.phoneNumber, sender, '🧠', timestamp);
210
+
211
+ // Map prefix and get history
212
+ const mapped = mapPrefix(text);
213
+ const history = sessions.get(sender);
214
+
215
+ // Dispatch through gateway
216
+ let response;
217
+ try {
218
+ const { Gateway } = await import('../gateway/gateway.js');
219
+ const gw = new Gateway({ rag: true });
220
+ response = await gw.send(mapped, { history });
221
+ } catch (err) {
222
+ response = `Error: ${err.message}`;
223
+ }
224
+
225
+ // Update session and reply
226
+ sessions.update(sender, mapped, response);
227
+ const reply = stripMarkdown(response);
228
+
229
+ // Signal has a ~6000 char limit per message
230
+ if (reply.length > 5500) {
231
+ const chunks = reply.match(/.{1,5500}/gs) || [reply];
232
+ for (const chunk of chunks) {
233
+ await sendMessage(config.signalService, config.phoneNumber, sender, chunk);
234
+ }
235
+ } else {
236
+ await sendMessage(config.signalService, config.phoneNumber, sender, reply);
237
+ }
130
238
  }
@@ -821,12 +821,125 @@ export async function handleWeb(args = {}) {
821
821
  // ---------------------------------------------------------------------------
822
822
 
823
823
  export async function handleSetupSignal() {
824
- return {
825
- output: JSON.stringify({
826
- status: 'info',
827
- message: 'Signal setup: Set SIGNAL_SERVICE, SIGNAL_PHONE_NUMBER, and SIGNAL_WHITELIST environment variables, then run `cipher bot`.',
828
- }, null, 2),
824
+ const clack = await import('@clack/prompts');
825
+ const { execSync } = await import('node:child_process');
826
+ const { writeConfig, loadConfig, configExists } = await import('../config.js');
827
+ const brand = await import('../brand.js');
828
+
829
+ brand.banner();
830
+ clack.intro('Signal Bot Setup');
831
+
832
+ // Step 1: Check if signal-cli-rest-api is running
833
+ let signalService = 'http://localhost:8080';
834
+ let apiHealthy = false;
835
+
836
+ clack.log.step('Checking for signal-cli-rest-api...');
837
+
838
+ try {
839
+ const resp = execSync(`curl -sf ${signalService}/v1/about`, { encoding: 'utf-8', timeout: 5000 });
840
+ const info = JSON.parse(resp);
841
+ clack.log.success(`Signal API running (v${info.version}, mode: ${info.mode})`);
842
+ apiHealthy = true;
843
+ } catch {
844
+ clack.log.warn('Signal API not detected at localhost:8080');
845
+ clack.log.info(
846
+ 'Start it with Docker:\n\n' +
847
+ ' docker compose --profile signal up -d signal-api\n\n' +
848
+ 'Or manually:\n\n' +
849
+ ' docker run -d --name signal-api \\\n' +
850
+ ' -p 8080:8080 -e MODE=json-rpc \\\n' +
851
+ ' -v signal-data:/home/.local/share/signal-cli \\\n' +
852
+ ' bbernhard/signal-cli-rest-api:0.98'
853
+ );
854
+
855
+ const customUrl = await clack.text({
856
+ message: 'Signal API URL (or press Enter to skip)',
857
+ placeholder: 'http://localhost:8080',
858
+ defaultValue: '',
859
+ });
860
+ if (clack.isCancel(customUrl)) { clack.outro('Cancelled.'); process.exit(0); }
861
+
862
+ if (customUrl) {
863
+ signalService = customUrl;
864
+ try {
865
+ const resp = execSync(`curl -sf ${signalService}/v1/about`, { encoding: 'utf-8', timeout: 5000 });
866
+ clack.log.success('Signal API reachable.');
867
+ apiHealthy = true;
868
+ } catch {
869
+ clack.log.error('Cannot reach Signal API at that URL.');
870
+ clack.outro('Fix the Signal API first, then run cipher setup-signal again.');
871
+ process.exit(1);
872
+ }
873
+ } else {
874
+ clack.outro('Start the Signal API first, then run cipher setup-signal again.');
875
+ process.exit(1);
876
+ }
877
+ }
878
+
879
+ // Step 2: Check registered phone numbers
880
+ let phoneNumber = '';
881
+ try {
882
+ const accounts = JSON.parse(execSync(`curl -sf ${signalService}/v1/accounts`, { encoding: 'utf-8', timeout: 5000 }));
883
+ if (accounts.length > 0) {
884
+ if (accounts.length === 1) {
885
+ phoneNumber = accounts[0];
886
+ clack.log.success(`Registered number: ${phoneNumber}`);
887
+ } else {
888
+ const selected = await clack.select({
889
+ message: 'Select the phone number for the bot',
890
+ options: accounts.map(a => ({ value: a, label: a })),
891
+ });
892
+ if (clack.isCancel(selected)) { clack.outro('Cancelled.'); process.exit(0); }
893
+ phoneNumber = selected;
894
+ }
895
+ } else {
896
+ clack.log.warn('No phone numbers registered with signal-cli.');
897
+ clack.log.info(
898
+ 'Register a number:\n\n' +
899
+ ` curl -X POST ${signalService}/v1/register/<YOUR_NUMBER>\n` +
900
+ ` curl -X POST ${signalService}/v1/verify/<YOUR_NUMBER> -d '{"token":"<CODE>"}'`
901
+ );
902
+ clack.outro('Register a number first, then run cipher setup-signal again.');
903
+ process.exit(1);
904
+ }
905
+ } catch (err) {
906
+ clack.log.error('Failed to query accounts: ' + err.message);
907
+ process.exit(1);
908
+ }
909
+
910
+ // Step 3: Whitelist
911
+ const whitelistInput = await clack.text({
912
+ message: 'Allowed phone numbers (comma-separated, E.164 format)',
913
+ placeholder: '+15551234567,+15559876543',
914
+ defaultValue: '',
915
+ });
916
+ if (clack.isCancel(whitelistInput)) { clack.outro('Cancelled.'); process.exit(0); }
917
+
918
+ const whitelist = whitelistInput
919
+ ? whitelistInput.split(',').map(n => n.trim()).filter(Boolean)
920
+ : [];
921
+
922
+ // Step 4: Save config
923
+ let existingConfig = {};
924
+ try {
925
+ if (configExists()) existingConfig = loadConfig();
926
+ } catch { /* ignore */ }
927
+
928
+ const config = {
929
+ ...existingConfig,
930
+ signal: {
931
+ signal_service: signalService,
932
+ phone_number: phoneNumber,
933
+ whitelist,
934
+ session_timeout: 3600,
935
+ },
829
936
  };
937
+
938
+ const configPath = writeConfig(config);
939
+
940
+ clack.log.success('Signal configuration saved.');
941
+ clack.outro(`Run cipher bot to start. Config: ${configPath}`);
942
+ return {};
830
943
  }
831
944
 
832
945
  // ---------------------------------------------------------------------------
@@ -857,7 +970,7 @@ export async function handleUpdate(args = {}) {
857
970
 
858
971
  if (latest === currentVersion) {
859
972
  brand.success(`Already up to date (${currentVersion})`);
860
- return { output: JSON.stringify({ current: currentVersion, latest, upToDate: true }, null, 2) };
973
+ return {};
861
974
  }
862
975
 
863
976
  brand.warn(`New version available: ${latest}`);
@@ -870,7 +983,7 @@ export async function handleUpdate(args = {}) {
870
983
  });
871
984
 
872
985
  brand.success(`Updated ${currentVersion} → ${latest}`);
873
- return { output: JSON.stringify({ previous: currentVersion, updated: latest, success: true }, null, 2) };
986
+ return {};
874
987
  } catch (err) {
875
988
  brand.error(`Update failed: ${err.message}`);
876
989
  return { error: true, message: `Update failed: ${err.message}` };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cipher-security",
3
- "version": "5.1.0",
3
+ "version": "5.3.0",
4
4
  "description": "CIPHER — AI Security Engineering Platform CLI",
5
5
  "type": "module",
6
6
  "engines": {
@@ -25,6 +25,7 @@
25
25
  "@clack/prompts": "^1.1.0",
26
26
  "better-sqlite3": "^12.8.0",
27
27
  "openai": "^6.32.0",
28
+ "ws": "^8.19.0",
28
29
  "yaml": "^2.8.2"
29
30
  }
30
31
  }