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 +125 -50
- package/lib/bot/bot.js +147 -39
- package/lib/gateway/commands.js +120 -7
- package/package.json +2 -1
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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'
|
|
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
|
-
|
|
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 —
|
|
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'],
|
|
35
|
-
[/^#{1,6}\s+/gm, ''],
|
|
36
|
-
[/\*{2}([^*\n]+)\*{2}/g, '$1'],
|
|
37
|
-
[/\*([^*\n]+)\*/g, '$1'],
|
|
38
|
-
[/`([^`\n]+)`/g, '$1'],
|
|
39
|
-
[/\[([^\]]+)\]\([^)]+\)/g, '$1'],
|
|
40
|
-
[/^(\s*)[*-]\s+/gm, '$1• '],
|
|
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
|
-
//
|
|
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
|
-
*
|
|
119
|
-
*
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
}
|
package/lib/gateway/commands.js
CHANGED
|
@@ -821,12 +821,125 @@ export async function handleWeb(args = {}) {
|
|
|
821
821
|
// ---------------------------------------------------------------------------
|
|
822
822
|
|
|
823
823
|
export async function handleSetupSignal() {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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 {
|
|
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 {
|
|
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.
|
|
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
|
}
|