cipher-security 5.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 (75) hide show
  1. package/bin/cipher.js +465 -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 +130 -0
  45. package/lib/commands.js +99 -0
  46. package/lib/complexity.js +377 -0
  47. package/lib/config.js +213 -0
  48. package/lib/gateway/client.js +309 -0
  49. package/lib/gateway/commands.js +830 -0
  50. package/lib/gateway/config-validate.js +109 -0
  51. package/lib/gateway/gateway.js +367 -0
  52. package/lib/gateway/index.js +62 -0
  53. package/lib/gateway/mode.js +309 -0
  54. package/lib/gateway/plugins.js +222 -0
  55. package/lib/gateway/prompt.js +214 -0
  56. package/lib/mcp/server.js +262 -0
  57. package/lib/memory/compressor.js +425 -0
  58. package/lib/memory/engine.js +763 -0
  59. package/lib/memory/evolution.js +668 -0
  60. package/lib/memory/index.js +58 -0
  61. package/lib/memory/orchestrator.js +506 -0
  62. package/lib/memory/retriever.js +515 -0
  63. package/lib/memory/synthesizer.js +333 -0
  64. package/lib/pipeline/async-scanner.js +510 -0
  65. package/lib/pipeline/binary-analysis.js +1043 -0
  66. package/lib/pipeline/dom-xss-scanner.js +435 -0
  67. package/lib/pipeline/github-actions.js +792 -0
  68. package/lib/pipeline/index.js +124 -0
  69. package/lib/pipeline/osint.js +498 -0
  70. package/lib/pipeline/sarif.js +373 -0
  71. package/lib/pipeline/scanner.js +880 -0
  72. package/lib/pipeline/template-manager.js +525 -0
  73. package/lib/pipeline/xss-scanner.js +353 -0
  74. package/lib/setup-wizard.js +229 -0
  75. package/package.json +30 -0
@@ -0,0 +1,353 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+ // CIPHER is a trademark of defconxt.
4
+
5
+ /**
6
+ * Static XSS heuristic scanner — detects DOM XSS sinks and sources without a browser.
7
+ *
8
+ * Covers ~40-60% of reflected/DOM XSS patterns via regex-based sink/source analysis.
9
+ * This is the static layer; full runtime DOM XSS requires Playwright (dom-xss-scanner.js).
10
+ *
11
+ * Ported from pipeline/xss_scanner.py (338 LOC Python).
12
+ */
13
+
14
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
15
+ import { join, extname } from 'node:path';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Sink patterns — where untrusted data reaches dangerous APIs
19
+ // Note: No /g flag on any regex to avoid lastIndex state issues.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /** @type {Array<[string, string, RegExp]>} [name, severity, pattern] */
23
+ const SINK_PATTERNS = [
24
+ ['innerHTML', 'critical', /\.\s*innerHTML\s*=(?!=)/i],
25
+ ['outerHTML', 'critical', /\.\s*outerHTML\s*=(?!=)/i],
26
+ ['document.write', 'critical', /document\s*\.\s*write(ln)?\s*\(/i],
27
+ ['eval', 'critical', /\beval\s*\(/i],
28
+ ['setTimeout-string', 'high', /setTimeout\s*\(\s*['"]/i],
29
+ ['setInterval-string', 'high', /setInterval\s*\(\s*['"]/i],
30
+ ['Function-constructor', 'critical', /\bnew\s+Function\s*\(/i],
31
+ ['insertAdjacentHTML', 'high', /\.insertAdjacentHTML\s*\(/i],
32
+ ['dangerouslySetInnerHTML', 'critical', /dangerouslySetInnerHTML\s*=\s*\{/i],
33
+ ['v-html', 'high', /\bv-html\s*=/i],
34
+ ['[innerHTML]', 'high', /\[innerHTML\]\s*=/i],
35
+ ['bypassSecurityTrust', 'critical', /bypassSecurityTrust(Html|Script|Url|ResourceUrl|Style)/i],
36
+ ['jquery-html', 'high', /\$\([^)]*\)\s*\.\s*html\s*\(/i],
37
+ ['jquery-append-raw', 'medium', /\$\([^)]*\)\s*\.\s*append\s*\(/i],
38
+ ['document.domain', 'high', /document\s*\.\s*domain\s*=/i],
39
+ ['location-assign', 'high', /location\s*\.\s*(href|assign|replace)\s*=/i],
40
+ ['srcdoc', 'high', /\bsrcdoc\s*=/i],
41
+ ['postMessage-star', 'medium', /\.postMessage\s*\([^,]+,\s*['"]?\*/i],
42
+ ];
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Source patterns — where untrusted data enters the application
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /** @type {Array<[string, RegExp]>} [name, pattern] */
49
+ const SOURCE_PATTERNS = [
50
+ ['location.hash', /location\s*\.\s*hash/i],
51
+ ['location.search', /location\s*\.\s*search/i],
52
+ ['location.href', /location\s*\.\s*href(?!\s*=)/i],
53
+ ['document.URL', /document\s*\.\s*URL/i],
54
+ ['document.referrer', /document\s*\.\s*referrer/i],
55
+ ['document.cookie', /document\s*\.\s*cookie/i],
56
+ ['window.name', /window\s*\.\s*name/i],
57
+ ['URLSearchParams', /new\s+URLSearchParams/i],
58
+ ['postMessage-handler', /addEventListener\s*\(\s*['"]message['"]/i],
59
+ ];
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Template injection patterns (server-side)
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /** @type {Array<[string, string, RegExp]>} [name, severity, pattern] */
66
+ const TEMPLATE_SINKS = [
67
+ ['jinja2-raw', 'high', /\{\{.*\|.*safe\s*\}\}/i],
68
+ ['jinja2-autoescape-off', 'critical', /\{%\s*autoescape\s+false/i],
69
+ ['erb-raw', 'high', /<%=\s*raw\s+/i],
70
+ ['php-echo-unescaped', 'high', /<\?=\s*\$_(GET|POST|REQUEST|COOKIE)/i],
71
+ ['asp-response-write', 'high', /Response\.Write\s*\(\s*Request/i],
72
+ ['thymeleaf-utext', 'high', /th:utext\s*=/i],
73
+ ['razor-raw', 'high', /@Html\.Raw\s*\(/i],
74
+ ];
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // File extensions to scan
78
+ // ---------------------------------------------------------------------------
79
+
80
+ const SCANNABLE_EXTENSIONS = new Set([
81
+ '.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte',
82
+ '.html', '.htm', '.php', '.erb', '.ejs', '.hbs',
83
+ '.pug', '.jade', '.py', '.rb', '.java', '.cs',
84
+ '.asp', '.aspx',
85
+ ]);
86
+
87
+ const SKIP_DIRS = new Set(['node_modules', 'vendor', '.venv', '__pycache__']);
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Data classes
91
+ // ---------------------------------------------------------------------------
92
+
93
+ class XSSFinding {
94
+ /**
95
+ * @param {object} opts
96
+ * @param {string} opts.file
97
+ * @param {number} opts.lineNumber
98
+ * @param {string} opts.lineContent
99
+ * @param {string} opts.sinkName
100
+ * @param {string} opts.severity critical | high | medium
101
+ * @param {string} opts.category dom-sink | dom-source | template-injection
102
+ * @param {string|null} [opts.sourceNearby=null]
103
+ */
104
+ constructor(opts) {
105
+ this.file = opts.file;
106
+ this.lineNumber = opts.lineNumber;
107
+ this.lineContent = opts.lineContent;
108
+ this.sinkName = opts.sinkName;
109
+ this.severity = opts.severity;
110
+ this.category = opts.category;
111
+ this.sourceNearby = opts.sourceNearby ?? null;
112
+ }
113
+
114
+ toDict() {
115
+ const d = {
116
+ file: this.file,
117
+ line: this.lineNumber,
118
+ sink: this.sinkName,
119
+ severity: this.severity,
120
+ category: this.category,
121
+ content: this.lineContent.trim().slice(0, 200),
122
+ };
123
+ if (this.sourceNearby) {
124
+ d.source_nearby = this.sourceNearby;
125
+ }
126
+ return d;
127
+ }
128
+ }
129
+
130
+ class XSSScanResult {
131
+ /**
132
+ * @param {object} [opts]
133
+ * @param {number} [opts.filesScanned=0]
134
+ * @param {XSSFinding[]} [opts.findings=[]]
135
+ * @param {Array<object>} [opts.sourcesFound=[]]
136
+ */
137
+ constructor(opts = {}) {
138
+ this.filesScanned = opts.filesScanned ?? 0;
139
+ this.findings = opts.findings ?? [];
140
+ this.sourcesFound = opts.sourcesFound ?? [];
141
+ }
142
+
143
+ get criticalCount() {
144
+ return this.findings.filter((f) => f.severity === 'critical').length;
145
+ }
146
+
147
+ get highCount() {
148
+ return this.findings.filter((f) => f.severity === 'high').length;
149
+ }
150
+
151
+ toDict() {
152
+ return {
153
+ files_scanned: this.filesScanned,
154
+ total_findings: this.findings.length,
155
+ critical: this.criticalCount,
156
+ high: this.highCount,
157
+ medium: this.findings.length - this.criticalCount - this.highCount,
158
+ findings: this.findings.map((f) => f.toDict()),
159
+ sources_found: this.sourcesFound,
160
+ };
161
+ }
162
+
163
+ summary() {
164
+ if (this.findings.length === 0) {
165
+ return `No XSS sinks detected in ${this.filesScanned} files.`;
166
+ }
167
+ return (
168
+ `${this.findings.length} XSS sink(s) in ${this.filesScanned} files: ` +
169
+ `${this.criticalCount} critical, ${this.highCount} high`
170
+ );
171
+ }
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // XSSScanner
176
+ // ---------------------------------------------------------------------------
177
+
178
+ class XSSScanner {
179
+ /**
180
+ * Scan a single file for XSS patterns.
181
+ * @param {string} filepath
182
+ * @returns {XSSFinding[]}
183
+ */
184
+ scanFile(filepath) {
185
+ const ext = extname(filepath).toLowerCase();
186
+ if (!SCANNABLE_EXTENSIONS.has(ext)) return [];
187
+
188
+ let content;
189
+ try {
190
+ content = readFileSync(filepath, 'utf-8');
191
+ } catch {
192
+ return [];
193
+ }
194
+
195
+ const lines = content.split('\n');
196
+ const findings = [];
197
+ /** @type {Map<number, string>} lineNumber → sourceName */
198
+ const sourceLines = new Map();
199
+
200
+ // First pass: find all source locations
201
+ for (let i = 0; i < lines.length; i++) {
202
+ for (const [srcName, srcPat] of SOURCE_PATTERNS) {
203
+ if (srcPat.test(lines[i])) {
204
+ sourceLines.set(i + 1, srcName);
205
+ break;
206
+ }
207
+ }
208
+ }
209
+
210
+ // Second pass: find sinks + correlate nearby sources
211
+ for (let i = 0; i < lines.length; i++) {
212
+ const lineNo = i + 1;
213
+ const line = lines[i];
214
+
215
+ for (const [sinkName, severity, sinkPat] of SINK_PATTERNS) {
216
+ if (sinkPat.test(line)) {
217
+ // Check if a source is within 10 lines
218
+ let nearbySource = null;
219
+ let adjustedSeverity = severity;
220
+ for (const [srcLineNo, srcName] of sourceLines) {
221
+ if (Math.abs(srcLineNo - lineNo) <= 10) {
222
+ nearbySource = srcName;
223
+ adjustedSeverity = 'critical'; // Source + sink = confirmed dangerous
224
+ break;
225
+ }
226
+ }
227
+ findings.push(
228
+ new XSSFinding({
229
+ file: filepath,
230
+ lineNumber: lineNo,
231
+ lineContent: line,
232
+ sinkName,
233
+ severity: adjustedSeverity,
234
+ category: 'dom-sink',
235
+ sourceNearby: nearbySource,
236
+ }),
237
+ );
238
+ }
239
+ }
240
+
241
+ for (const [sinkName, severity, sinkPat] of TEMPLATE_SINKS) {
242
+ if (sinkPat.test(line)) {
243
+ findings.push(
244
+ new XSSFinding({
245
+ file: filepath,
246
+ lineNumber: lineNo,
247
+ lineContent: line,
248
+ sinkName,
249
+ severity,
250
+ category: 'template-injection',
251
+ }),
252
+ );
253
+ }
254
+ }
255
+ }
256
+
257
+ return findings;
258
+ }
259
+
260
+ /**
261
+ * Scan a directory for XSS patterns.
262
+ * @param {string} directory
263
+ * @param {object} [opts]
264
+ * @param {boolean} [opts.recursive=true]
265
+ * @returns {XSSScanResult}
266
+ */
267
+ scanDirectory(directory, opts = {}) {
268
+ const recursive = opts.recursive !== false;
269
+ const result = new XSSScanResult();
270
+
271
+ const walk = (dir) => {
272
+ let entries;
273
+ try {
274
+ entries = readdirSync(dir, { withFileTypes: true });
275
+ } catch {
276
+ return;
277
+ }
278
+ for (const ent of entries.sort((a, b) => a.name.localeCompare(b.name))) {
279
+ if (ent.name.startsWith('.') || SKIP_DIRS.has(ent.name)) continue;
280
+ const full = join(dir, ent.name);
281
+ if (ent.isDirectory() && recursive) {
282
+ walk(full);
283
+ } else if (ent.isFile() && SCANNABLE_EXTENSIONS.has(extname(ent.name).toLowerCase())) {
284
+ result.filesScanned++;
285
+ const findings = this.scanFile(full);
286
+ result.findings.push(...findings);
287
+ }
288
+ }
289
+ };
290
+ walk(directory);
291
+ return result;
292
+ }
293
+
294
+ /**
295
+ * Scan added lines in a unified diff for XSS patterns.
296
+ * @param {string} diffText
297
+ * @param {string} [filename='<diff>']
298
+ * @returns {XSSFinding[]}
299
+ */
300
+ scanDiff(diffText, filename = '<diff>') {
301
+ const findings = [];
302
+ let lineNo = 0;
303
+
304
+ for (const line of diffText.split('\n')) {
305
+ if (line.startsWith('@@')) {
306
+ const m = line.match(/\+(\d+)/);
307
+ lineNo = m ? parseInt(m[1], 10) : 0;
308
+ continue;
309
+ }
310
+ if (line.startsWith('+') && !line.startsWith('+++')) {
311
+ const content = line.slice(1);
312
+ lineNo++;
313
+
314
+ const allSinks = [...SINK_PATTERNS, ...TEMPLATE_SINKS];
315
+ for (const [sinkName, severity, sinkPat] of allSinks) {
316
+ if (sinkPat.test(content)) {
317
+ const isTemplate = TEMPLATE_SINKS.some((t) => t[0] === sinkName);
318
+ findings.push(
319
+ new XSSFinding({
320
+ file: filename,
321
+ lineNumber: lineNo,
322
+ lineContent: content,
323
+ sinkName,
324
+ severity,
325
+ category: isTemplate ? 'template-injection' : 'dom-sink',
326
+ }),
327
+ );
328
+ }
329
+ }
330
+ } else if (!line.startsWith('-')) {
331
+ lineNo++;
332
+ }
333
+ }
334
+ return findings;
335
+ }
336
+ }
337
+
338
+ // ---------------------------------------------------------------------------
339
+ // Exports
340
+ // ---------------------------------------------------------------------------
341
+
342
+ export {
343
+ // Pattern arrays (for testing/inspection)
344
+ SINK_PATTERNS,
345
+ SOURCE_PATTERNS,
346
+ TEMPLATE_SINKS,
347
+ SCANNABLE_EXTENSIONS,
348
+ // Data classes
349
+ XSSFinding,
350
+ XSSScanResult,
351
+ // Scanner
352
+ XSSScanner,
353
+ };
@@ -0,0 +1,229 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+ // CIPHER is a trademark of defconxt.
4
+
5
+ /**
6
+ * setup-wizard.js — Interactive @clack/prompts setup wizard for CIPHER.
7
+ *
8
+ * Implements R002 (interactive wizard) and R006 (craft feel):
9
+ * - Provider selection (Ollama / Claude API / LiteLLM)
10
+ * - Masked API key input
11
+ * - Ollama binary detection
12
+ * - Python availability check
13
+ * - Completion summary with configured settings
14
+ *
15
+ * All prompts write to stderr via @clack/prompts — stdout stays clean
16
+ * for protocol use.
17
+ *
18
+ * @module setup-wizard
19
+ */
20
+
21
+ import * as clack from '@clack/prompts';
22
+ import { execFileSync } from 'node:child_process';
23
+ import { existsSync } from 'node:fs';
24
+ import { join } from 'node:path';
25
+ import { writeConfig, getConfigPaths } from './config.js';
26
+
27
+ /**
28
+ * Check whether a binary exists on $PATH.
29
+ *
30
+ * @param {string} name — binary name to look up
31
+ * @returns {string | null} — absolute path if found, null otherwise
32
+ */
33
+ function whichSync(name) {
34
+ try {
35
+ return execFileSync('which', [name], {
36
+ encoding: 'utf-8',
37
+ timeout: 3000,
38
+ stdio: ['pipe', 'pipe', 'pipe'],
39
+ }).trim();
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Handle @clack/prompts cancellation. Call after every prompt.
47
+ * If the user pressed Ctrl+C, shows a cancel message and exits cleanly.
48
+ *
49
+ * @param {any} value — return value from a @clack/prompts call
50
+ */
51
+ function handleCancel(value) {
52
+ if (clack.isCancel(value)) {
53
+ clack.cancel('Setup cancelled.');
54
+ process.exit(0);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Build the default config sections that every provider path includes.
60
+ * Matches the Python setup wizard's defaults exactly.
61
+ */
62
+ function defaultOllamaSection(model = 'cipher') {
63
+ return {
64
+ base_url: 'http://127.0.0.1:11434',
65
+ model,
66
+ timeout: 300,
67
+ };
68
+ }
69
+
70
+ function defaultClaudeSection(apiKey = '', model = 'claude-sonnet-4-5-20250929') {
71
+ return {
72
+ api_key: apiKey,
73
+ model,
74
+ timeout: 120,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Run the interactive setup wizard.
80
+ *
81
+ * Walks the user through provider selection, backend-specific configuration,
82
+ * and writes a valid config.yaml. Uses @clack/prompts for all I/O (stderr).
83
+ *
84
+ * @returns {Promise<void>}
85
+ */
86
+ export async function runSetupWizard() {
87
+ try {
88
+ // ── Intro ──────────────────────────────────────────────────────────
89
+ clack.intro('CIPHER Setup');
90
+ clack.log.info('Configure your LLM backend to get started with CIPHER.');
91
+
92
+ // ── Python no longer required ────────────────────────────────────
93
+ clack.log.success('CIPHER runs natively on Node.js — no Python required.');
94
+
95
+ // ── Provider selection ─────────────────────────────────────────────
96
+ const provider = await clack.select({
97
+ message: 'Choose your LLM backend',
98
+ options: [
99
+ { value: 'ollama', label: 'Ollama', hint: 'Local inference, free, private (requires GPU)' },
100
+ { value: 'claude', label: 'Claude API', hint: 'Cloud inference, paid, highest quality' },
101
+ { value: 'litellm', label: 'LiteLLM', hint: 'Multi-provider (OpenAI, Gemini, llama.cpp, etc.)' },
102
+ ],
103
+ });
104
+ handleCancel(provider);
105
+
106
+ let config;
107
+
108
+ // ── Ollama path ────────────────────────────────────────────────────
109
+ if (provider === 'ollama') {
110
+ const ollamaPath = whichSync('ollama');
111
+ if (ollamaPath) {
112
+ clack.log.success(`Ollama found at ${ollamaPath}`);
113
+ } else {
114
+ clack.log.info(
115
+ 'Ollama not found on PATH. Install it from https://ollama.com/download'
116
+ );
117
+ }
118
+
119
+ const model = await clack.text({
120
+ message: 'Ollama model name',
121
+ placeholder: 'cipher',
122
+ defaultValue: 'cipher',
123
+ });
124
+ handleCancel(model);
125
+
126
+ config = {
127
+ llm_backend: 'ollama',
128
+ ollama: defaultOllamaSection(model),
129
+ claude: defaultClaudeSection(),
130
+ };
131
+ }
132
+
133
+ // ── Claude path ────────────────────────────────────────────────────
134
+ else if (provider === 'claude') {
135
+ const apiKey = await clack.password({
136
+ message: 'Anthropic API key',
137
+ mask: '•',
138
+ });
139
+ handleCancel(apiKey);
140
+
141
+ const model = await clack.select({
142
+ message: 'Claude model',
143
+ options: [
144
+ { value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5', hint: 'Recommended' },
145
+ { value: 'claude-opus-4-6', label: 'Claude Opus 4' },
146
+ { value: 'claude-haiku-3-5', label: 'Claude Haiku 3.5' },
147
+ ],
148
+ });
149
+ handleCancel(model);
150
+
151
+ config = {
152
+ llm_backend: 'claude',
153
+ ollama: defaultOllamaSection(),
154
+ claude: defaultClaudeSection(apiKey, model),
155
+ };
156
+ }
157
+
158
+ // ── LiteLLM path ──────────────────────────────────────────────────
159
+ else if (provider === 'litellm') {
160
+ const model = await clack.text({
161
+ message: 'LiteLLM model string',
162
+ placeholder: 'gpt-4o, gemini/gemini-2.0-flash, ollama/llama3.1',
163
+ });
164
+ handleCancel(model);
165
+
166
+ const apiKey = await clack.password({
167
+ message: 'API key (leave empty if not needed)',
168
+ mask: '•',
169
+ });
170
+ handleCancel(apiKey);
171
+
172
+ const apiBase = await clack.text({
173
+ message: 'Custom API base URL (leave empty for default)',
174
+ placeholder: '',
175
+ defaultValue: '',
176
+ });
177
+ handleCancel(apiBase);
178
+
179
+ config = {
180
+ llm_backend: 'litellm',
181
+ ollama: defaultOllamaSection(),
182
+ claude: defaultClaudeSection(),
183
+ litellm: {
184
+ model: model || '',
185
+ api_key: apiKey || '',
186
+ api_base: apiBase || '',
187
+ timeout: 120,
188
+ },
189
+ };
190
+ }
191
+
192
+ // ── Write config ──────────────────────────────────────────────────
193
+ const configPath = writeConfig(config);
194
+
195
+ // Also write to project root if pyproject.toml exists (dual-write like Python)
196
+ const { projectConfig } = getConfigPaths();
197
+ const projectRoot = join(projectConfig, '..');
198
+ if (existsSync(join(projectRoot, 'pyproject.toml')) && projectConfig !== configPath) {
199
+ try {
200
+ writeConfig(config, projectConfig);
201
+ } catch {
202
+ // Non-fatal — user config was already written
203
+ }
204
+ }
205
+
206
+ // ── Completion summary ────────────────────────────────────────────
207
+ const summaryLines = [
208
+ `Provider: ${provider}`,
209
+ ];
210
+
211
+ if (provider === 'ollama') {
212
+ summaryLines.push(`Model: ${config.ollama.model}`);
213
+ } else if (provider === 'claude') {
214
+ summaryLines.push(`Model: ${config.claude.model}`);
215
+ } else if (provider === 'litellm') {
216
+ summaryLines.push(`Model: ${config.litellm.model || '(not set)'}`);
217
+ }
218
+
219
+ summaryLines.push(`Config path: ${configPath}`);
220
+
221
+ clack.note(summaryLines.join('\n'), 'Configuration Summary');
222
+ clack.outro('Setup complete! Run cipher --help to get started.');
223
+ } catch (err) {
224
+ // isCancel paths call process.exit(0) directly — this catches
225
+ // unexpected errors only.
226
+ clack.log.error(err.message || String(err));
227
+ process.exit(1);
228
+ }
229
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "cipher-security",
3
+ "version": "5.0.0",
4
+ "description": "CIPHER — AI Security Engineering Platform CLI",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=18"
8
+ },
9
+ "license": "AGPL-3.0-only",
10
+ "bin": {
11
+ "cipher": "bin/cipher.js"
12
+ },
13
+ "files": [
14
+ "bin/",
15
+ "lib/"
16
+ ],
17
+ "scripts": {
18
+ "test": "vitest run"
19
+ },
20
+ "devDependencies": {
21
+ "vitest": "^3"
22
+ },
23
+ "dependencies": {
24
+ "@anthropic-ai/sdk": "^0.80.0",
25
+ "@clack/prompts": "^1.1.0",
26
+ "better-sqlite3": "^12.8.0",
27
+ "openai": "^6.32.0",
28
+ "yaml": "^2.8.2"
29
+ }
30
+ }