cipher-security 2.0.2 → 2.0.4
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 +16 -26
- package/lib/api/billing.js +3 -0
- package/lib/api/marketplace.js +3 -0
- package/lib/autonomous/feedback-loop.js +9 -9
- package/lib/autonomous/leaderboard.js +2 -0
- package/lib/autonomous/researcher.js +5 -5
- package/lib/bot/bot.js +1 -1
- package/lib/commands.js +2 -2
- package/lib/config.js +4 -4
- package/lib/gateway/commands.js +204 -62
- package/lib/gateway/index.js +5 -0
- package/lib/gateway/prompt.js +4 -4
- package/lib/mcp/server.js +11 -1
- package/lib/memory/compressor.js +1 -1
- package/lib/memory/engine.js +15 -11
- package/lib/pipeline/github-actions.js +6 -6
- package/lib/setup-wizard.js +1 -10
- package/package.json +1 -1
- package/lib/complexity.js +0 -377
package/bin/cipher.js
CHANGED
|
@@ -9,15 +9,13 @@
|
|
|
9
9
|
* Fast path: --version / -V / --help / -h use only node:fs + node:path
|
|
10
10
|
* (zero dynamic imports, <200ms cold start).
|
|
11
11
|
*
|
|
12
|
-
* Command path: dynamically imports
|
|
13
|
-
*
|
|
12
|
+
* Command path: dynamically imports gateway/commands.js handlers,
|
|
13
|
+
* dispatches to native Node.js functions, prints results.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { readFileSync } from 'node:fs';
|
|
17
17
|
import { resolve, dirname } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
-
import { spawn } from 'node:child_process';
|
|
20
|
-
|
|
21
19
|
// ---------------------------------------------------------------------------
|
|
22
20
|
// Fast path — no dynamic imports, must complete in <200ms
|
|
23
21
|
// ---------------------------------------------------------------------------
|
|
@@ -123,7 +121,7 @@ if (args.length === 0) {
|
|
|
123
121
|
}
|
|
124
122
|
|
|
125
123
|
// ---------------------------------------------------------------------------
|
|
126
|
-
//
|
|
124
|
+
// Native commands — handled before dynamic imports
|
|
127
125
|
// ---------------------------------------------------------------------------
|
|
128
126
|
|
|
129
127
|
const nativeCommands = new Set(['setup']);
|
|
@@ -214,36 +212,30 @@ const knownCommands = new Set([
|
|
|
214
212
|
* @returns {{ command: string, commandArgs: string[] }}
|
|
215
213
|
*/
|
|
216
214
|
function parseCommand(argv) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
for (const arg of argv) {
|
|
221
|
-
if (arg.startsWith('-')) {
|
|
222
|
-
flags.push(arg);
|
|
223
|
-
} else {
|
|
224
|
-
positional.push(arg);
|
|
225
|
-
}
|
|
215
|
+
if (argv.length === 0) {
|
|
216
|
+
console.error('No command specified. Run `cipher --help` for usage.');
|
|
217
|
+
process.exit(1);
|
|
226
218
|
}
|
|
227
219
|
|
|
228
|
-
|
|
229
|
-
|
|
220
|
+
const first = argv[0];
|
|
221
|
+
|
|
222
|
+
if (first.startsWith('-')) {
|
|
223
|
+
// No command, just flags — treat as error
|
|
230
224
|
console.error('No command specified. Run `cipher --help` for usage.');
|
|
231
225
|
process.exit(1);
|
|
232
226
|
}
|
|
233
227
|
|
|
234
|
-
const first = positional[0];
|
|
235
|
-
|
|
236
228
|
if (knownCommands.has(first)) {
|
|
237
229
|
return {
|
|
238
230
|
command: first,
|
|
239
|
-
commandArgs:
|
|
231
|
+
commandArgs: argv.slice(1), // Pass all args as-is, preserve flag-value order
|
|
240
232
|
};
|
|
241
233
|
}
|
|
242
234
|
|
|
243
|
-
// Bare query — join
|
|
235
|
+
// Bare query — join everything as query text
|
|
244
236
|
return {
|
|
245
237
|
command: 'query',
|
|
246
|
-
commandArgs:
|
|
238
|
+
commandArgs: argv,
|
|
247
239
|
};
|
|
248
240
|
}
|
|
249
241
|
|
|
@@ -254,7 +246,7 @@ function parseCommand(argv) {
|
|
|
254
246
|
/**
|
|
255
247
|
* Format and print a bridge command result, then exit if needed.
|
|
256
248
|
*
|
|
257
|
-
* Result shapes from
|
|
249
|
+
* Result shapes from gateway/commands.js:
|
|
258
250
|
* 1. {output, exit_code, error: true} — command error: print output to stderr, exit with code
|
|
259
251
|
* 2. {output, exit_code} — text-mode result: print output directly
|
|
260
252
|
* 3. Plain object (no output field) — structured JSON: pretty-print as JSON
|
|
@@ -350,8 +342,6 @@ async function formatBridgeResult(result, commandName = '') {
|
|
|
350
342
|
const { command, commandArgs } = parseCommand(cleanedArgs);
|
|
351
343
|
|
|
352
344
|
const debug = process.env.CIPHER_DEBUG === '1';
|
|
353
|
-
|
|
354
|
-
|
|
355
345
|
// ---------------------------------------------------------------------------
|
|
356
346
|
// Autonomous mode dispatch — bypasses normal command routing entirely
|
|
357
347
|
// ---------------------------------------------------------------------------
|
|
@@ -414,7 +404,7 @@ const { getCommandMode } = await import('../lib/commands.js');
|
|
|
414
404
|
const mode = getCommandMode(command);
|
|
415
405
|
|
|
416
406
|
if (mode === 'native') {
|
|
417
|
-
// ── Native dispatch —
|
|
407
|
+
// ── Native dispatch — handler functions ────────────────────────────────
|
|
418
408
|
if (debug) {
|
|
419
409
|
process.stderr.write(`[bridge:node] Dispatching command=${command} mode=native\n`);
|
|
420
410
|
}
|
|
@@ -435,7 +425,7 @@ if (mode === 'native') {
|
|
|
435
425
|
}
|
|
436
426
|
|
|
437
427
|
try {
|
|
438
|
-
// ── API server command: long-running HTTP server
|
|
428
|
+
// ── API server command: long-running HTTP server ──────────────────────
|
|
439
429
|
if (command === 'api') {
|
|
440
430
|
const { createAPIServer, APIConfig } = await import('../lib/api/server.js');
|
|
441
431
|
const apiPort = commandArgs.find((a, i) => commandArgs[i - 1] === '--port') || '8443';
|
package/lib/api/billing.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
2
|
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
3
|
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* CIPHER Billing Engine — usage-based metering middleware.
|
|
6
9
|
*
|
package/lib/api/marketplace.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
2
|
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
3
|
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* CIPHER Skill Marketplace — publish, discover, and install CIPHER skills.
|
|
6
9
|
*
|
|
@@ -172,20 +172,20 @@ export class SkillQualityAnalyzer {
|
|
|
172
172
|
scores.section_coverage = 0;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
// Check agent.
|
|
176
|
-
const
|
|
177
|
-
if (existsSync(
|
|
178
|
-
const agentContent = readFileSync(
|
|
175
|
+
// Check agent.js
|
|
176
|
+
const agentJs = join(fullPath, 'scripts', 'agent.js');
|
|
177
|
+
if (existsSync(agentJs)) {
|
|
178
|
+
const agentContent = readFileSync(agentJs, 'utf-8');
|
|
179
179
|
const agentLines = agentContent.trim().split('\n');
|
|
180
180
|
if (agentLines.length < SkillQualityAnalyzer.MIN_AGENT_PY_LINES) {
|
|
181
|
-
issues.push(`agent.
|
|
181
|
+
issues.push(`agent.js too short (${agentLines.length} lines)`);
|
|
182
182
|
}
|
|
183
183
|
scores.agent_quality = Math.min(agentLines.length / 50, 1.0);
|
|
184
|
-
if (!agentContent.includes('
|
|
185
|
-
if (!agentContent.includes('json')) issues.push('agent.
|
|
186
|
-
if (!agentContent.includes('
|
|
184
|
+
if (!agentContent.includes('process.argv')) issues.push('agent.js missing CLI dispatch');
|
|
185
|
+
if (!agentContent.includes('json')) issues.push('agent.js missing JSON output');
|
|
186
|
+
if (!agentContent.includes('process.argv')) issues.push('agent.js missing CLI entry point');
|
|
187
187
|
} else {
|
|
188
|
-
issues.push('scripts/agent.
|
|
188
|
+
issues.push('scripts/agent.js missing');
|
|
189
189
|
scores.agent_quality = 0;
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -306,8 +306,8 @@ ${description}
|
|
|
306
306
|
## Verification
|
|
307
307
|
|
|
308
308
|
\`\`\`bash
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
node scripts/agent.js analyze --target example
|
|
310
|
+
node scripts/agent.js report --format json
|
|
311
311
|
\`\`\`
|
|
312
312
|
|
|
313
313
|
## References
|
|
@@ -373,7 +373,7 @@ function _generateApiReference(domain, techniqueName) {
|
|
|
373
373
|
|
|
374
374
|
| Command | Description |
|
|
375
375
|
|---|---|
|
|
376
|
-
| \`
|
|
376
|
+
| \`node agent.js analyze --target <t>\` | Analyze a target for ${techniqueName} indicators |
|
|
377
377
|
|
|
378
378
|
## Options
|
|
379
379
|
|
|
@@ -406,7 +406,7 @@ export class AutonomousResearcher {
|
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
/**
|
|
409
|
-
* Create a complete hypothesis with SKILL.md, agent.
|
|
409
|
+
* Create a complete hypothesis with SKILL.md, agent.js, and api-reference.md.
|
|
410
410
|
* @param {string} domain
|
|
411
411
|
* @param {string} techniqueName
|
|
412
412
|
* @returns {ResearchHypothesis}
|
|
@@ -507,7 +507,7 @@ export class AutonomousResearcher {
|
|
|
507
507
|
mkdirSync(refsDir, { recursive: true });
|
|
508
508
|
|
|
509
509
|
writeFileSync(join(base, 'SKILL.md'), hypothesis.skillContent, 'utf-8');
|
|
510
|
-
const agentPath = join(scriptsDir, 'agent.
|
|
510
|
+
const agentPath = join(scriptsDir, 'agent.js');
|
|
511
511
|
writeFileSync(agentPath, hypothesis.agentScript, 'utf-8');
|
|
512
512
|
try { chmodSync(agentPath, 0o755); } catch { /* ok */ }
|
|
513
513
|
writeFileSync(join(refsDir, 'api-reference.md'), hypothesis.referenceContent, 'utf-8');
|
package/lib/bot/bot.js
CHANGED
|
@@ -217,7 +217,7 @@ async function handleEnvelope(envelope, config, sessions, whitelistSet) {
|
|
|
217
217
|
try {
|
|
218
218
|
const { Gateway } = await import('../gateway/gateway.js');
|
|
219
219
|
const gw = new Gateway({ rag: true });
|
|
220
|
-
response = await gw.send(mapped,
|
|
220
|
+
response = await gw.send(mapped, history);
|
|
221
221
|
} catch (err) {
|
|
222
222
|
response = `Error: ${err.message}`;
|
|
223
223
|
}
|
package/lib/commands.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* commands.js — Command routing table for the CIPHER CLI.
|
|
7
7
|
*
|
|
8
|
-
* Maps all
|
|
8
|
+
* Maps all 30 CLI commands to one of three dispatch modes:
|
|
9
9
|
* - native: Dispatched directly through Node.js handler functions (no Python)
|
|
10
|
-
* - passthrough:
|
|
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)
|
|
12
12
|
* - bridge: (Legacy) Dispatched via JSON-RPC through python-bridge.js — no commands
|
|
13
13
|
* use this mode as of M007/S03, retained for backward compatibility
|
package/lib/config.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Mirrors the Python gateway/config.py load_config() format and precedence:
|
|
9
9
|
* 1. Environment variables (LLM_BACKEND, ANTHROPIC_API_KEY) — highest
|
|
10
|
-
* 2. Project-root config.yaml (where
|
|
10
|
+
* 2. Project-root config.yaml (where cli/ directory lives)
|
|
11
11
|
* 3. ~/.config/cipher/config.yaml — lowest
|
|
12
12
|
*
|
|
13
13
|
* Produces YAML that Python's yaml.safe_load() can parse identically.
|
|
@@ -25,8 +25,8 @@ import { parse, stringify } from 'yaml';
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Walk up from `startDir` looking for
|
|
29
|
-
* _find_project_root(). Returns the directory containing
|
|
28
|
+
* Walk up from `startDir` looking for cli/ directory — same logic as Python's
|
|
29
|
+
* _find_project_root(). Returns the directory containing cli/ directory, or null.
|
|
30
30
|
*
|
|
31
31
|
* @param {string} [startDir=process.cwd()]
|
|
32
32
|
* @returns {string | null}
|
|
@@ -34,7 +34,7 @@ import { parse, stringify } from 'yaml';
|
|
|
34
34
|
function findProjectRoot(startDir = process.cwd()) {
|
|
35
35
|
let current = resolve(startDir);
|
|
36
36
|
for (let i = 0; i < 20; i++) {
|
|
37
|
-
if (existsSync(join(current, '
|
|
37
|
+
if (existsSync(join(current, 'cli'))) {
|
|
38
38
|
return current;
|
|
39
39
|
}
|
|
40
40
|
const parent = dirname(current);
|
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 30 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
|
|
@@ -35,6 +35,39 @@ function defaultMemoryDir() {
|
|
|
35
35
|
return join(homedir(), '.cipher', 'memory', 'default');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Extract a value from CLI args array or object.
|
|
40
|
+
* Handles both `cipher search lateral movement` (array) and `{query: "..."}` (object).
|
|
41
|
+
* @param {string[]|object} args
|
|
42
|
+
* @param {string} key - object key (e.g. 'query')
|
|
43
|
+
* @param {string} [flagName] - CLI flag (e.g. '--query'). If null, uses positional args.
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
function getArg(args, key, flagName) {
|
|
47
|
+
if (!Array.isArray(args)) return args[key] || '';
|
|
48
|
+
if (flagName) {
|
|
49
|
+
const idx = args.indexOf(flagName);
|
|
50
|
+
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
51
|
+
}
|
|
52
|
+
// Positional: join all non-flag args
|
|
53
|
+
return args.filter(a => !a.startsWith('-')).join(' ');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract a flag value from CLI args.
|
|
58
|
+
* @param {string[]|object} args
|
|
59
|
+
* @param {string} key - object key
|
|
60
|
+
* @param {string} flag - CLI flag (e.g. '--limit')
|
|
61
|
+
* @param {*} defaultVal
|
|
62
|
+
* @returns {*}
|
|
63
|
+
*/
|
|
64
|
+
function getFlagArg(args, key, flag, defaultVal) {
|
|
65
|
+
if (!Array.isArray(args)) return args[key] ?? defaultVal;
|
|
66
|
+
const idx = args.indexOf(flag);
|
|
67
|
+
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
68
|
+
return defaultVal;
|
|
69
|
+
}
|
|
70
|
+
|
|
38
71
|
/**
|
|
39
72
|
* Resolve the repository root by walking up from this file.
|
|
40
73
|
* @returns {string}
|
|
@@ -93,13 +126,13 @@ export async function handleSearch(args = {}) {
|
|
|
93
126
|
try {
|
|
94
127
|
const retriever = new AdaptiveRetriever(memory);
|
|
95
128
|
const results = retriever.retrieve(
|
|
96
|
-
args
|
|
97
|
-
args
|
|
98
|
-
args
|
|
129
|
+
getArg(args, "query"),
|
|
130
|
+
getFlagArg(args, "engagement", "--engagement", ""),
|
|
131
|
+
getFlagArg(args, "limit", "--limit", 10),
|
|
99
132
|
);
|
|
100
133
|
debug(`search: ${results.length} results for "${args.query}"`);
|
|
101
134
|
return {
|
|
102
|
-
query: args
|
|
135
|
+
query: getArg(args, "query"),
|
|
103
136
|
count: results.length,
|
|
104
137
|
results: results.map(r => ({
|
|
105
138
|
content: r.content,
|
|
@@ -126,18 +159,20 @@ export async function handleSearch(args = {}) {
|
|
|
126
159
|
* @returns {Promise<object>}
|
|
127
160
|
*/
|
|
128
161
|
export async function handleStore(args = {}) {
|
|
162
|
+
const content = getArg(args, 'content', '--content');
|
|
163
|
+
if (!content) {
|
|
164
|
+
return { error: true, message: 'Usage: cipher store --content "finding text" [--type finding|ioc|ttp|note]' };
|
|
165
|
+
}
|
|
129
166
|
const { CipherMemory, MemoryEntry, MemoryType } = await import('../memory/index.js');
|
|
130
167
|
const memory = new CipherMemory(defaultMemoryDir());
|
|
131
168
|
try {
|
|
132
|
-
const typeStr = args
|
|
169
|
+
const typeStr = getFlagArg(args, 'type', '--type', 'note');
|
|
133
170
|
const entry = new MemoryEntry({
|
|
134
|
-
content
|
|
171
|
+
content,
|
|
135
172
|
memoryType: Object.values(MemoryType).includes(typeStr) ? typeStr : MemoryType.NOTE,
|
|
136
|
-
severity: args
|
|
137
|
-
engagementId: args
|
|
138
|
-
tags:
|
|
139
|
-
? args.tags
|
|
140
|
-
: (typeof args.tags === 'string' && args.tags ? args.tags.split(',') : []),
|
|
173
|
+
severity: getFlagArg(args, 'severity', '--severity', ''),
|
|
174
|
+
engagementId: getFlagArg(args, 'engagement', '--engagement', ''),
|
|
175
|
+
tags: [],
|
|
141
176
|
});
|
|
142
177
|
const entryId = memory.store(entry);
|
|
143
178
|
debug(`store: id=${entryId} type=${typeStr}`);
|
|
@@ -173,7 +208,7 @@ export async function handleStats(args = {}) {
|
|
|
173
208
|
} catch { return false; }
|
|
174
209
|
}).length;
|
|
175
210
|
// Count script files recursively
|
|
176
|
-
scriptCount = countFiles(sDir, '*.
|
|
211
|
+
scriptCount = countFiles(sDir, '*.js', 'scripts');
|
|
177
212
|
} catch {
|
|
178
213
|
// Skills dir unreadable
|
|
179
214
|
}
|
|
@@ -199,13 +234,14 @@ export async function handleStats(args = {}) {
|
|
|
199
234
|
* @returns {Promise<object>}
|
|
200
235
|
*/
|
|
201
236
|
export async function handleScore(args = {}) {
|
|
237
|
+
const query = getFlagArg(args, 'query', '--query', '');
|
|
238
|
+
const response = getFlagArg(args, 'response', '--response', '');
|
|
239
|
+
if (!query || !response) {
|
|
240
|
+
return { error: true, message: 'Usage: cipher score --query "question" --response "answer"' };
|
|
241
|
+
}
|
|
202
242
|
const { ResponseScorer } = await import('../memory/index.js');
|
|
203
243
|
const scorer = new ResponseScorer();
|
|
204
|
-
const scored = scorer.score(
|
|
205
|
-
args.query || '',
|
|
206
|
-
args.response || '',
|
|
207
|
-
args.mode || '',
|
|
208
|
-
);
|
|
244
|
+
const scored = scorer.score(query, response, getFlagArg(args, 'mode', '--mode', ''));
|
|
209
245
|
debug(`score: ${scored.score}`);
|
|
210
246
|
return {
|
|
211
247
|
score: scored.score,
|
|
@@ -249,9 +285,12 @@ export async function handleMemoryExport(args = {}) {
|
|
|
249
285
|
* @returns {Promise<object>}
|
|
250
286
|
*/
|
|
251
287
|
export async function handleMemoryImport(args = {}) {
|
|
252
|
-
const file = args
|
|
253
|
-
if (!file
|
|
254
|
-
return { error: true, message:
|
|
288
|
+
const file = getArg(args, 'file');
|
|
289
|
+
if (!file) {
|
|
290
|
+
return { error: true, message: 'Usage: cipher memory-import <file.json>\n\nImport findings, IOCs, and notes from a previously exported JSON file.' };
|
|
291
|
+
}
|
|
292
|
+
if (!existsSync(file)) {
|
|
293
|
+
return { error: true, message: `File not found: ${file}` };
|
|
255
294
|
}
|
|
256
295
|
const { CipherMemory, MemoryEntry } = await import('../memory/index.js');
|
|
257
296
|
const memory = new CipherMemory(defaultMemoryDir());
|
|
@@ -283,9 +322,11 @@ export async function handleIngest(args = {}) {
|
|
|
283
322
|
const { CipherMemory } = await import('../memory/index.js');
|
|
284
323
|
const memory = new CipherMemory(defaultMemoryDir());
|
|
285
324
|
try {
|
|
325
|
+
// Rebuild FTS5 index to fix stale/corrupt indexes
|
|
326
|
+
memory.symbolic.db.exec("INSERT INTO entries_fts(entries_fts) VALUES('rebuild')");
|
|
286
327
|
const stats = memory.consolidate();
|
|
287
|
-
debug('ingest: consolidation complete');
|
|
288
|
-
return { ingested: true, ...stats };
|
|
328
|
+
debug('ingest: FTS rebuild + consolidation complete');
|
|
329
|
+
return { ingested: true, fts_rebuilt: true, ...stats };
|
|
289
330
|
} finally {
|
|
290
331
|
memory.close();
|
|
291
332
|
}
|
|
@@ -346,10 +387,13 @@ export async function handleStatus(args = {}) {
|
|
|
346
387
|
* @returns {Promise<object>}
|
|
347
388
|
*/
|
|
348
389
|
export async function handleDiff(args = {}) {
|
|
390
|
+
let diffText = getArg(args, 'file');
|
|
391
|
+
if (!diffText) {
|
|
392
|
+
return { error: true, message: 'Usage: cipher diff <file.patch>\n cat changes.diff | cipher diff -' };
|
|
393
|
+
}
|
|
349
394
|
const { SecurityDiffAnalyzer } = await import('../pipeline/index.js');
|
|
350
395
|
const analyzer = new SecurityDiffAnalyzer();
|
|
351
396
|
|
|
352
|
-
let diffText = args.file || '';
|
|
353
397
|
// If it looks like a file path (no newlines, exists on disk), read it
|
|
354
398
|
if (diffText && !diffText.includes('\n') && existsSync(diffText)) {
|
|
355
399
|
diffText = readFileSync(diffText, 'utf-8');
|
|
@@ -393,11 +437,14 @@ export async function handleWorkflow(args = {}) {
|
|
|
393
437
|
* @returns {Promise<object>}
|
|
394
438
|
*/
|
|
395
439
|
export async function handleSarif(args = {}) {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
440
|
+
const input = getArg(args, 'input');
|
|
441
|
+
if (!input) {
|
|
442
|
+
return { error: true, message: 'Usage: cipher sarif <scan-results.json> [--output results.sarif]\n\nConvert scan findings to SARIF v2.1.0 for GitHub Advanced Security / CI pipelines.' };
|
|
443
|
+
}
|
|
444
|
+
if (!existsSync(input)) {
|
|
445
|
+
return { error: true, message: `File not found: ${input}` };
|
|
400
446
|
}
|
|
447
|
+
const { SarifReport } = await import('../pipeline/index.js');
|
|
401
448
|
|
|
402
449
|
const data = JSON.parse(readFileSync(input, 'utf-8'));
|
|
403
450
|
const report = new SarifReport();
|
|
@@ -423,10 +470,13 @@ export async function handleSarif(args = {}) {
|
|
|
423
470
|
* @returns {Promise<object>}
|
|
424
471
|
*/
|
|
425
472
|
export async function handleOsint(args = {}) {
|
|
473
|
+
const target = getArg(args, 'target');
|
|
474
|
+
if (!target) {
|
|
475
|
+
return { error: true, message: 'Usage: cipher osint <domain|ip> [--type domain|ip]' };
|
|
476
|
+
}
|
|
426
477
|
const { OSINTPipeline } = await import('../pipeline/index.js');
|
|
427
478
|
const pipeline = new OSINTPipeline();
|
|
428
|
-
const
|
|
429
|
-
const type = args.type || 'domain';
|
|
479
|
+
const type = getFlagArg(args, 'type', '--type', 'domain');
|
|
430
480
|
|
|
431
481
|
const results = type === 'ip'
|
|
432
482
|
? pipeline.investigateIp(target)
|
|
@@ -493,12 +543,12 @@ export async function handleDomains(args = {}) {
|
|
|
493
543
|
*/
|
|
494
544
|
export async function handleSkills(args = {}) {
|
|
495
545
|
const sDir = skillsDir();
|
|
496
|
-
const query = (args
|
|
497
|
-
const limit = args
|
|
546
|
+
const query = getArg(args, 'query').toLowerCase();
|
|
547
|
+
const limit = parseInt(getFlagArg(args, 'limit', '--limit', 10), 10);
|
|
498
548
|
const results = [];
|
|
499
549
|
|
|
500
550
|
if (!existsSync(sDir)) {
|
|
501
|
-
return { query
|
|
551
|
+
return { query, count: 0, results: [] };
|
|
502
552
|
}
|
|
503
553
|
|
|
504
554
|
// Walk skills dir looking for SKILL.md files
|
|
@@ -526,7 +576,7 @@ export async function handleSkills(args = {}) {
|
|
|
526
576
|
|
|
527
577
|
walk(sDir, '');
|
|
528
578
|
debug(`skills: "${query}" → ${results.length} results`);
|
|
529
|
-
return { query
|
|
579
|
+
return { query, count: results.length, results };
|
|
530
580
|
}
|
|
531
581
|
|
|
532
582
|
// ---------------------------------------------------------------------------
|
|
@@ -684,40 +734,128 @@ export async function handleQuery(args = {}) {
|
|
|
684
734
|
* @returns {Promise<object>}
|
|
685
735
|
*/
|
|
686
736
|
export async function handleLeaderboard(args = {}) {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
737
|
+
try {
|
|
738
|
+
const { SkillLeaderboard } = await import('../autonomous/leaderboard.js');
|
|
739
|
+
const lb = new SkillLeaderboard();
|
|
740
|
+
const action = Array.isArray(args) ? args[0] : args.action;
|
|
741
|
+
|
|
742
|
+
if (action === 'dashboard') {
|
|
743
|
+
const dashboard = lb.getDashboard();
|
|
744
|
+
lb.close();
|
|
745
|
+
return dashboard;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const top = lb.getTopSkills(20);
|
|
749
|
+
lb.close();
|
|
750
|
+
return {
|
|
751
|
+
top_skills: top.map((e, i) => ({
|
|
752
|
+
rank: i + 1,
|
|
753
|
+
skill: e.skillPath,
|
|
754
|
+
score: e.avgScore,
|
|
755
|
+
invocations: e.invocationCount,
|
|
756
|
+
})),
|
|
757
|
+
total: top.length,
|
|
758
|
+
};
|
|
759
|
+
} catch (err) {
|
|
760
|
+
return { error: true, message: `Leaderboard error: ${err.message}` };
|
|
761
|
+
}
|
|
691
762
|
}
|
|
692
763
|
|
|
693
764
|
/**
|
|
694
765
|
* @returns {Promise<object>}
|
|
695
766
|
*/
|
|
696
767
|
export async function handleFeedback(args = {}) {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
768
|
+
try {
|
|
769
|
+
const { SkillQualityAnalyzer } = await import('../autonomous/feedback-loop.js');
|
|
770
|
+
const analyzer = new SkillQualityAnalyzer(skillsDir());
|
|
771
|
+
const dryRun = Array.isArray(args) ? args.includes('--dry-run') : args.dryRun;
|
|
772
|
+
const maxSkills = Array.isArray(args)
|
|
773
|
+
? parseInt(args.find((a, i) => args[i - 1] === '--max') || '10', 10)
|
|
774
|
+
: args.max || 10;
|
|
775
|
+
|
|
776
|
+
// Analyze skills quality
|
|
777
|
+
const results = [];
|
|
778
|
+
const { readdirSync } = await import('node:fs');
|
|
779
|
+
const sDir = skillsDir();
|
|
780
|
+
for (const domain of readdirSync(sDir).slice(0, maxSkills)) {
|
|
781
|
+
try {
|
|
782
|
+
const analysis = analyzer.analyzeSkill(domain);
|
|
783
|
+
if (analysis.needsImprovement) {
|
|
784
|
+
results.push({ skill: domain, quality: analysis.overallQuality, issues: analysis.issues.length });
|
|
785
|
+
}
|
|
786
|
+
} catch { /* skip */ }
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
analyzed: maxSkills,
|
|
791
|
+
needs_improvement: results.length,
|
|
792
|
+
dry_run: !!dryRun,
|
|
793
|
+
candidates: results,
|
|
794
|
+
};
|
|
795
|
+
} catch (err) {
|
|
796
|
+
return { error: true, message: `Feedback error: ${err.message}` };
|
|
797
|
+
}
|
|
701
798
|
}
|
|
702
799
|
|
|
703
800
|
/**
|
|
704
801
|
* @returns {Promise<object>}
|
|
705
802
|
*/
|
|
706
803
|
export async function handleMarketplace(args = {}) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
804
|
+
try {
|
|
805
|
+
const { SkillMarketplace } = await import('../api/marketplace.js');
|
|
806
|
+
const mp = new SkillMarketplace();
|
|
807
|
+
const action = Array.isArray(args) ? args[0] : args.action;
|
|
808
|
+
|
|
809
|
+
if (action === 'search') {
|
|
810
|
+
const query = Array.isArray(args) ? args.find((a, i) => args[i - 1] === '--query') || args[1] || '' : args.query || '';
|
|
811
|
+
const results = mp.search(query);
|
|
812
|
+
mp.close();
|
|
813
|
+
return { query, results: results.map(p => ({ name: p.name, domain: p.domain, rating: p.rating, downloads: p.downloads })), total: results.length };
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const index = mp.getIndex();
|
|
817
|
+
mp.close();
|
|
818
|
+
return index;
|
|
819
|
+
} catch (err) {
|
|
820
|
+
return { error: true, message: `Marketplace error: ${err.message}` };
|
|
821
|
+
}
|
|
711
822
|
}
|
|
712
823
|
|
|
713
824
|
/**
|
|
714
825
|
* @returns {Promise<object>}
|
|
715
826
|
*/
|
|
716
827
|
export async function handleCompliance(args = {}) {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
828
|
+
try {
|
|
829
|
+
const { ComplianceEngine, ComplianceFramework } = await import('../api/compliance.js');
|
|
830
|
+
const engine = new ComplianceEngine();
|
|
831
|
+
const framework = Array.isArray(args) ? args[0] : args.framework;
|
|
832
|
+
|
|
833
|
+
if (!framework) {
|
|
834
|
+
return {
|
|
835
|
+
available_frameworks: Object.keys(ComplianceFramework),
|
|
836
|
+
total: Object.keys(ComplianceFramework).length,
|
|
837
|
+
usage: 'cipher compliance <FRAMEWORK> [--format json|markdown|csv]',
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const fw = framework.toUpperCase();
|
|
842
|
+
if (!ComplianceFramework[fw]) {
|
|
843
|
+
return { error: true, message: `Unknown framework: ${framework}. Available: ${Object.keys(ComplianceFramework).join(', ')}` };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const format = Array.isArray(args) ? (args.find((a, i) => args[i - 1] === '--format') || 'json') : (args.format || 'json');
|
|
847
|
+
const report = engine.assessFromFindings([], fw);
|
|
848
|
+
|
|
849
|
+
if (format === 'markdown') {
|
|
850
|
+
return { output: report.toMarkdown() };
|
|
851
|
+
} else if (format === 'csv') {
|
|
852
|
+
return { output: engine.exportCsv(report) };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return report.toDict();
|
|
856
|
+
} catch (err) {
|
|
857
|
+
return { error: true, message: `Compliance error: ${err.message}` };
|
|
858
|
+
}
|
|
721
859
|
}
|
|
722
860
|
|
|
723
861
|
// ---------------------------------------------------------------------------
|
|
@@ -795,12 +933,15 @@ export async function handleScan(args = {}) {
|
|
|
795
933
|
// ---------------------------------------------------------------------------
|
|
796
934
|
|
|
797
935
|
export async function handleDashboard() {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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 {};
|
|
804
945
|
}
|
|
805
946
|
|
|
806
947
|
// ---------------------------------------------------------------------------
|
|
@@ -808,12 +949,13 @@ export async function handleDashboard() {
|
|
|
808
949
|
// ---------------------------------------------------------------------------
|
|
809
950
|
|
|
810
951
|
export async function handleWeb(args = {}) {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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 {};
|
|
817
959
|
}
|
|
818
960
|
|
|
819
961
|
// ---------------------------------------------------------------------------
|
package/lib/gateway/index.js
CHANGED
package/lib/gateway/prompt.js
CHANGED
|
@@ -25,7 +25,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
25
25
|
const __dirname = dirname(__filename);
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Walk up from startDir looking for
|
|
28
|
+
* Walk up from startDir looking for cli/ directory or package.json.
|
|
29
29
|
* Returns the directory or null.
|
|
30
30
|
*
|
|
31
31
|
* @param {string} startDir
|
|
@@ -35,15 +35,15 @@ function findRepoRoot(startDir) {
|
|
|
35
35
|
let current = resolve(startDir);
|
|
36
36
|
for (let i = 0; i < 20; i++) {
|
|
37
37
|
if (
|
|
38
|
-
existsSync(join(current, '
|
|
38
|
+
existsSync(join(current, 'cli')) ||
|
|
39
39
|
existsSync(join(current, 'package.json'))
|
|
40
40
|
) {
|
|
41
41
|
// Check for knowledge/ dir to distinguish from cli/package.json
|
|
42
42
|
if (existsSync(join(current, 'knowledge')) || existsSync(join(current, 'skills'))) {
|
|
43
43
|
return current;
|
|
44
44
|
}
|
|
45
|
-
// If it has
|
|
46
|
-
if (existsSync(join(current, '
|
|
45
|
+
// If it has cli/, it is the repo root
|
|
46
|
+
if (existsSync(join(current, 'cli'))) {
|
|
47
47
|
return current;
|
|
48
48
|
}
|
|
49
49
|
}
|
package/lib/mcp/server.js
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createInterface } from 'node:readline';
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const _version = (() => {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8')).version;
|
|
21
|
+
} catch { return 'unknown'; }
|
|
22
|
+
})();
|
|
13
23
|
|
|
14
24
|
/**
|
|
15
25
|
* MCP tool definitions — schema registry for all 14 tools.
|
|
@@ -205,7 +215,7 @@ export class CipherMCPServer {
|
|
|
205
215
|
result: {
|
|
206
216
|
protocolVersion: '2024-11-05',
|
|
207
217
|
capabilities: { tools: {} },
|
|
208
|
-
serverInfo: { name: 'cipher-mcp', version:
|
|
218
|
+
serverInfo: { name: 'cipher-mcp', version: _version },
|
|
209
219
|
},
|
|
210
220
|
};
|
|
211
221
|
}
|
package/lib/memory/compressor.js
CHANGED
|
@@ -305,7 +305,7 @@ class SemanticCompressor {
|
|
|
305
305
|
* @private
|
|
306
306
|
*/
|
|
307
307
|
async _llmCompress(window, entities) {
|
|
308
|
-
//
|
|
308
|
+
// LLM-based extraction not implemented — returns heuristic results
|
|
309
309
|
// For now, fall back to heuristic compression
|
|
310
310
|
return this._heuristicCompress(window, entities);
|
|
311
311
|
}
|
package/lib/memory/engine.js
CHANGED
|
@@ -481,22 +481,26 @@ class CipherMemory {
|
|
|
481
481
|
search(query, filters = {}, limit = DEFAULT_TOP_K) {
|
|
482
482
|
const resultLists = [];
|
|
483
483
|
|
|
484
|
-
// Layer 1: Lexical search (BM25)
|
|
484
|
+
// Layer 1: Lexical search (BM25) — always run when query is provided
|
|
485
485
|
const lexicalResults = this.symbolic.searchLexical(query, limit * 2);
|
|
486
486
|
if (lexicalResults.length > 0) {
|
|
487
487
|
resultLists.push(lexicalResults.map((r) => r.entry_id));
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
-
// Layer 2: Symbolic search (structured)
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
490
|
+
// Layer 2: Symbolic search (structured) — only when filters are provided
|
|
491
|
+
// Without filters, symbolic returns ALL entries which drowns out lexical ranking
|
|
492
|
+
const hasFilters = !!(filters.engagementId || filters.memoryType || filters.severity || filters.mitreTechnique);
|
|
493
|
+
if (hasFilters) {
|
|
494
|
+
const symbolicResults = this.symbolic.searchSymbolic({
|
|
495
|
+
engagementId: filters.engagementId ?? '',
|
|
496
|
+
memoryType: filters.memoryType ?? '',
|
|
497
|
+
severity: filters.severity ?? '',
|
|
498
|
+
mitreTechnique: filters.mitreTechnique ?? '',
|
|
499
|
+
limit: limit * 2,
|
|
500
|
+
});
|
|
501
|
+
if (symbolicResults.length > 0) {
|
|
502
|
+
resultLists.push(symbolicResults.map((r) => r.entry_id));
|
|
503
|
+
}
|
|
500
504
|
}
|
|
501
505
|
|
|
502
506
|
// Fuse with RRF
|
|
@@ -678,9 +678,9 @@ jobs:
|
|
|
678
678
|
steps:
|
|
679
679
|
- uses: actions/checkout@v4
|
|
680
680
|
with: { fetch-depth: 0 }
|
|
681
|
-
- uses: actions/setup-
|
|
682
|
-
with: {
|
|
683
|
-
- run:
|
|
681
|
+
- uses: actions/setup-node@v5
|
|
682
|
+
with: { node-version: "22" }
|
|
683
|
+
- run: npm install -g cipher-security
|
|
684
684
|
- name: Run PR Security Review
|
|
685
685
|
env:
|
|
686
686
|
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
@@ -722,9 +722,9 @@ outputs:
|
|
|
722
722
|
runs:
|
|
723
723
|
using: "composite"
|
|
724
724
|
steps:
|
|
725
|
-
- uses: actions/setup-
|
|
726
|
-
with: {
|
|
727
|
-
- { shell: bash, run: "
|
|
725
|
+
- uses: actions/setup-node@v5
|
|
726
|
+
with: { node-version: "22" }
|
|
727
|
+
- { shell: bash, run: "npm install -g cipher-security" }
|
|
728
728
|
- name: Run Security Review
|
|
729
729
|
shell: bash
|
|
730
730
|
env:
|
package/lib/setup-wizard.js
CHANGED
|
@@ -251,16 +251,7 @@ export async function runSetupWizard() {
|
|
|
251
251
|
// ── Write config ──────────────────────────────────────────────────
|
|
252
252
|
const configPath = writeConfig(config);
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
const { projectConfig } = getConfigPaths();
|
|
256
|
-
const projectRoot = join(projectConfig, '..');
|
|
257
|
-
if (existsSync(join(projectRoot, 'pyproject.toml')) && projectConfig !== configPath) {
|
|
258
|
-
try {
|
|
259
|
-
writeConfig(config, projectConfig);
|
|
260
|
-
} catch {
|
|
261
|
-
// Non-fatal — user config was already written
|
|
262
|
-
}
|
|
263
|
-
}
|
|
254
|
+
|
|
264
255
|
|
|
265
256
|
// ── Completion summary ────────────────────────────────────────────
|
|
266
257
|
const summaryLines = [
|
package/package.json
CHANGED
package/lib/complexity.js
DELETED
|
@@ -1,377 +0,0 @@
|
|
|
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
|
-
* Smart routing complexity classifier for CIPHER CLI.
|
|
7
|
-
*
|
|
8
|
-
* Evaluates multiple heuristic signals to determine query complexity and
|
|
9
|
-
* route to the appropriate backend (Ollama for simple/moderate, Claude for
|
|
10
|
-
* complex/expert).
|
|
11
|
-
*
|
|
12
|
-
* Ported from src/gateway/complexity.py — all keyword dictionaries, regex
|
|
13
|
-
* patterns, score thresholds, and scoring logic are identical.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* import { classify, routeBackend, classifyAndRoute, COMPLEXITY } from './complexity.js';
|
|
17
|
-
*
|
|
18
|
-
* const level = classify("design a zero trust architecture for AWS");
|
|
19
|
-
* const backend = routeBackend(level); // "claude"
|
|
20
|
-
*
|
|
21
|
-
* const [lvl, be] = classifyAndRoute("what port does SSH use");
|
|
22
|
-
* // lvl === 1 (SIMPLE), be === "ollama"
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Complexity enum
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
/** Query complexity levels, ordered by increasing difficulty. */
|
|
30
|
-
export const COMPLEXITY = Object.freeze({
|
|
31
|
-
SIMPLE: 1, // Factual lookups, single-concept questions → Ollama
|
|
32
|
-
MODERATE: 2, // Multi-part but bounded questions → Ollama
|
|
33
|
-
COMPLEX: 3, // Deep reasoning, threat models, architecture → Claude
|
|
34
|
-
EXPERT: 4, // Cross-domain, multi-framework, advanced analysis → Claude
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const _NAMES = Object.freeze({
|
|
38
|
-
[COMPLEXITY.SIMPLE]: 'SIMPLE',
|
|
39
|
-
[COMPLEXITY.MODERATE]: 'MODERATE',
|
|
40
|
-
[COMPLEXITY.COMPLEX]: 'COMPLEX',
|
|
41
|
-
[COMPLEXITY.EXPERT]: 'EXPERT',
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Reverse lookup: complexity level integer → human-readable name.
|
|
46
|
-
* @param {number} level
|
|
47
|
-
* @returns {string}
|
|
48
|
-
*/
|
|
49
|
-
export function nameOf(level) {
|
|
50
|
-
return _NAMES[level] ?? 'UNKNOWN';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Tunable weights — adjust these to change routing sensitivity
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
/** Points thresholds for each complexity level. */
|
|
58
|
-
export const THRESHOLDS = Object.freeze({
|
|
59
|
-
moderate: 3, // >= 3 points → MODERATE
|
|
60
|
-
complex: 6, // >= 6 points → COMPLEX
|
|
61
|
-
expert: 10, // >= 10 points → EXPERT
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
/** Message length breakpoints (characters) and their point values. */
|
|
65
|
-
export const LENGTH_SCORES = Object.freeze([
|
|
66
|
-
[500, 4], // Very long queries are likely complex
|
|
67
|
-
[300, 3], // Long queries
|
|
68
|
-
[150, 2], // Medium queries
|
|
69
|
-
[80, 1], // Short but not trivial
|
|
70
|
-
]);
|
|
71
|
-
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
// Keyword sets — each match adds the specified points
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
|
|
76
|
-
/** Simple indicators (negative points — pull toward Ollama). */
|
|
77
|
-
export const SIMPLE_KEYWORDS = Object.freeze({
|
|
78
|
-
'what is': -2,
|
|
79
|
-
'what are': -1,
|
|
80
|
-
'what does': -2,
|
|
81
|
-
'what port': -3,
|
|
82
|
-
'which port': -3,
|
|
83
|
-
'default port': -3,
|
|
84
|
-
'how to install': -2,
|
|
85
|
-
'how to start': -2,
|
|
86
|
-
'how to run': -2,
|
|
87
|
-
'syntax for': -2,
|
|
88
|
-
'command for': -2,
|
|
89
|
-
'define ': -2,
|
|
90
|
-
'definition of': -2,
|
|
91
|
-
'meaning of': -2,
|
|
92
|
-
'example of': -1,
|
|
93
|
-
'list of': -1,
|
|
94
|
-
'difference between': -1,
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
/** Domain complexity indicators (positive points — push toward Claude). */
|
|
98
|
-
export const COMPLEX_KEYWORDS = Object.freeze({
|
|
99
|
-
// Architecture & design
|
|
100
|
-
'threat model': 4,
|
|
101
|
-
'architecture': 3,
|
|
102
|
-
'design': 2,
|
|
103
|
-
'zero trust': 3,
|
|
104
|
-
'blast radius': 3,
|
|
105
|
-
'compensating control': 3,
|
|
106
|
-
'defense in depth': 2,
|
|
107
|
-
'security architecture': 4,
|
|
108
|
-
'data flow diagram': 3,
|
|
109
|
-
'trust boundary': 3,
|
|
110
|
-
// Risk & compliance
|
|
111
|
-
'dpia': 4,
|
|
112
|
-
'risk assessment': 4,
|
|
113
|
-
'risk analysis': 3,
|
|
114
|
-
'compliance mapping': 4,
|
|
115
|
-
'gap analysis': 3,
|
|
116
|
-
'maturity assessment': 3,
|
|
117
|
-
'audit finding': 3,
|
|
118
|
-
'control mapping': 3,
|
|
119
|
-
// Analysis & reasoning
|
|
120
|
-
'analyze': 2,
|
|
121
|
-
'analyse': 2,
|
|
122
|
-
'evaluate': 2,
|
|
123
|
-
'compare and contrast': 3,
|
|
124
|
-
'trade-off': 2,
|
|
125
|
-
'tradeoff': 2,
|
|
126
|
-
'pros and cons': 2,
|
|
127
|
-
'impact analysis': 3,
|
|
128
|
-
'root cause': 3,
|
|
129
|
-
// Offensive / red team
|
|
130
|
-
'attack chain': 4,
|
|
131
|
-
'attack path': 3,
|
|
132
|
-
'kill chain': 3,
|
|
133
|
-
'exploitation chain': 4,
|
|
134
|
-
'lateral movement': 2,
|
|
135
|
-
'privilege escalation': 2,
|
|
136
|
-
'privesc': 2,
|
|
137
|
-
'post-exploitation': 3,
|
|
138
|
-
'evasion technique': 3,
|
|
139
|
-
'bypass': 1,
|
|
140
|
-
// Incident response
|
|
141
|
-
'incident analysis': 4,
|
|
142
|
-
'incident': 2,
|
|
143
|
-
'forensic analysis': 4,
|
|
144
|
-
'forensic': 2,
|
|
145
|
-
'timeline reconstruction': 4,
|
|
146
|
-
'indicator of compromise': 2,
|
|
147
|
-
'ioc': 1,
|
|
148
|
-
'beacon': 2,
|
|
149
|
-
'cobalt strike': 3,
|
|
150
|
-
'mimikatz': 2,
|
|
151
|
-
'dcsync': 3,
|
|
152
|
-
'triage': 2,
|
|
153
|
-
'containment strategy': 3,
|
|
154
|
-
'eradication plan': 3,
|
|
155
|
-
'breach': 2,
|
|
156
|
-
'malware analysis': 3,
|
|
157
|
-
'reverse engineer': 3,
|
|
158
|
-
// Detection engineering
|
|
159
|
-
'detection logic': 3,
|
|
160
|
-
'sigma rule': 2,
|
|
161
|
-
'detection coverage': 3,
|
|
162
|
-
'false positive': 2,
|
|
163
|
-
'detection gap': 3,
|
|
164
|
-
'correlation rule': 3,
|
|
165
|
-
'hunting hypothesis': 3,
|
|
166
|
-
// Multi-step / planning
|
|
167
|
-
'strategy': 2,
|
|
168
|
-
'roadmap': 3,
|
|
169
|
-
'implementation plan': 3,
|
|
170
|
-
'step by step': 1,
|
|
171
|
-
'runbook': 2,
|
|
172
|
-
'playbook': 2,
|
|
173
|
-
'workflow': 1,
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
/** Framework references — signal expert-level queries. */
|
|
177
|
-
export const FRAMEWORK_KEYWORDS = Object.freeze({
|
|
178
|
-
'nist': 2,
|
|
179
|
-
'nist 800-53': 3,
|
|
180
|
-
'nist 800-171': 3,
|
|
181
|
-
'nist csf': 3,
|
|
182
|
-
'mitre att&ck': 3,
|
|
183
|
-
'mitre attack': 3,
|
|
184
|
-
'stride': 3,
|
|
185
|
-
'pasta': 3,
|
|
186
|
-
'dread': 3,
|
|
187
|
-
'owasp': 2,
|
|
188
|
-
'owasp top 10': 2,
|
|
189
|
-
'cis controls': 2,
|
|
190
|
-
'cis benchmark': 2,
|
|
191
|
-
'iso 27001': 3,
|
|
192
|
-
'iso 27002': 3,
|
|
193
|
-
'soc 2': 2,
|
|
194
|
-
'pci dss': 3,
|
|
195
|
-
'hipaa': 2,
|
|
196
|
-
'gdpr': 2,
|
|
197
|
-
'ccpa': 2,
|
|
198
|
-
'fedramp': 3,
|
|
199
|
-
'cmmc': 3,
|
|
200
|
-
'cve-': 1,
|
|
201
|
-
'cwe-': 1,
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// ---------------------------------------------------------------------------
|
|
205
|
-
// Structural patterns — regex-based signals
|
|
206
|
-
// ---------------------------------------------------------------------------
|
|
207
|
-
|
|
208
|
-
/** Multi-part query indicators (each match adds points). */
|
|
209
|
-
export const STRUCTURE_PATTERNS = Object.freeze([
|
|
210
|
-
// Conjunctions joining distinct requests
|
|
211
|
-
[/\b(?:and also|and then|additionally|furthermore|moreover)\b/, 2],
|
|
212
|
-
// Numbered lists in the query
|
|
213
|
-
[/(?:^|\n)\s*\d+[.)]\s/, 2],
|
|
214
|
-
// Conditional logic
|
|
215
|
-
[/\bif\s+.+\bthen\b/, 2],
|
|
216
|
-
[/\bwhat if\b/, 2],
|
|
217
|
-
[/\bassuming\b/, 1],
|
|
218
|
-
[/\bgiven that\b/, 1],
|
|
219
|
-
// Comparison requests
|
|
220
|
-
[/\bvs\.?\b/, 1],
|
|
221
|
-
[/\bversus\b/, 1],
|
|
222
|
-
[/\bcompare\b/, 2],
|
|
223
|
-
// Scope amplifiers
|
|
224
|
-
[/\b(?:comprehensive|exhaustive|thorough|detailed|in-depth|end-to-end)\b/, 2],
|
|
225
|
-
[/\b(?:enterprise|organization-wide|company-wide|across all)\b/, 2],
|
|
226
|
-
// Multiple question marks (multiple questions)
|
|
227
|
-
[/\?.*\?/, 2],
|
|
228
|
-
]);
|
|
229
|
-
|
|
230
|
-
/** Count of "and" conjunctions — many ands suggest multi-part queries. */
|
|
231
|
-
export const AND_CONJUNCTION_THRESHOLD = 3;
|
|
232
|
-
export const AND_CONJUNCTION_POINTS = 2;
|
|
233
|
-
|
|
234
|
-
// ---------------------------------------------------------------------------
|
|
235
|
-
// Scoring functions
|
|
236
|
-
// ---------------------------------------------------------------------------
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Score based on message length.
|
|
240
|
-
* @param {string} message
|
|
241
|
-
* @returns {number}
|
|
242
|
-
*/
|
|
243
|
-
export function scoreLength(message) {
|
|
244
|
-
const length = message.length;
|
|
245
|
-
for (const [threshold, points] of LENGTH_SCORES) {
|
|
246
|
-
if (length >= threshold) {
|
|
247
|
-
return points;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return 0;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Score based on keyword presence. Returns sum of matched keyword points.
|
|
255
|
-
* @param {string} lower — lowercased message
|
|
256
|
-
* @param {Record<string, number>} keywordSet
|
|
257
|
-
* @returns {number}
|
|
258
|
-
*/
|
|
259
|
-
export function scoreKeywords(lower, keywordSet) {
|
|
260
|
-
let total = 0;
|
|
261
|
-
for (const [keyword, points] of Object.entries(keywordSet)) {
|
|
262
|
-
if (lower.includes(keyword)) {
|
|
263
|
-
total += points;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return total;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Score based on query structure patterns.
|
|
271
|
-
* @param {string} lower — lowercased message
|
|
272
|
-
* @returns {number}
|
|
273
|
-
*/
|
|
274
|
-
export function scoreStructure(lower) {
|
|
275
|
-
let total = 0;
|
|
276
|
-
for (const [pattern, points] of STRUCTURE_PATTERNS) {
|
|
277
|
-
if (pattern.test(lower)) {
|
|
278
|
-
total += points;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Count "and" conjunctions
|
|
283
|
-
const andMatches = lower.match(/\band\b/g);
|
|
284
|
-
const andCount = andMatches ? andMatches.length : 0;
|
|
285
|
-
if (andCount > AND_CONJUNCTION_THRESHOLD) {
|
|
286
|
-
total += AND_CONJUNCTION_POINTS;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return total;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Bonus points when multiple frameworks are referenced.
|
|
294
|
-
* @param {string} lower — lowercased message
|
|
295
|
-
* @returns {number}
|
|
296
|
-
*/
|
|
297
|
-
export function scoreFrameworkDensity(lower) {
|
|
298
|
-
let matches = 0;
|
|
299
|
-
let totalPoints = 0;
|
|
300
|
-
for (const [keyword, points] of Object.entries(FRAMEWORK_KEYWORDS)) {
|
|
301
|
-
if (lower.includes(keyword)) {
|
|
302
|
-
matches += 1;
|
|
303
|
-
totalPoints += points;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Bonus for cross-framework queries (referencing 3+ frameworks)
|
|
308
|
-
if (matches >= 3) {
|
|
309
|
-
totalPoints += 3;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return totalPoints;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// ---------------------------------------------------------------------------
|
|
316
|
-
// Public API
|
|
317
|
-
// ---------------------------------------------------------------------------
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Classify a query's complexity based on multiple heuristic signals.
|
|
321
|
-
* @param {string} message — The user's query string.
|
|
322
|
-
* @returns {number} Complexity level (1=SIMPLE, 2=MODERATE, 3=COMPLEX, 4=EXPERT).
|
|
323
|
-
*/
|
|
324
|
-
export function classify(message) {
|
|
325
|
-
const lower = message.toLowerCase();
|
|
326
|
-
|
|
327
|
-
let score = 0;
|
|
328
|
-
score += scoreLength(message);
|
|
329
|
-
score += scoreKeywords(lower, SIMPLE_KEYWORDS);
|
|
330
|
-
score += scoreKeywords(lower, COMPLEX_KEYWORDS);
|
|
331
|
-
score += scoreStructure(lower);
|
|
332
|
-
score += scoreFrameworkDensity(lower);
|
|
333
|
-
|
|
334
|
-
// Floor at 0 — negative scores are still SIMPLE
|
|
335
|
-
score = Math.max(score, 0);
|
|
336
|
-
|
|
337
|
-
if (score >= THRESHOLDS.expert) {
|
|
338
|
-
return COMPLEXITY.EXPERT;
|
|
339
|
-
}
|
|
340
|
-
if (score >= THRESHOLDS.complex) {
|
|
341
|
-
return COMPLEXITY.COMPLEX;
|
|
342
|
-
}
|
|
343
|
-
if (score >= THRESHOLDS.moderate) {
|
|
344
|
-
return COMPLEXITY.MODERATE;
|
|
345
|
-
}
|
|
346
|
-
return COMPLEXITY.SIMPLE;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Map a complexity level to a backend name.
|
|
351
|
-
*
|
|
352
|
-
* COMPLEX and EXPERT → "claude", SIMPLE and MODERATE → "ollama".
|
|
353
|
-
* The "litellm" backend is user-configured, not auto-routed.
|
|
354
|
-
*
|
|
355
|
-
* @param {number} level — The classified complexity level.
|
|
356
|
-
* @returns {string} Backend string: "ollama" or "claude".
|
|
357
|
-
*/
|
|
358
|
-
export function routeBackend(level) {
|
|
359
|
-
if (level >= COMPLEXITY.COMPLEX) {
|
|
360
|
-
return 'claude';
|
|
361
|
-
}
|
|
362
|
-
return 'ollama';
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Classify a message and return both the complexity level and backend.
|
|
367
|
-
*
|
|
368
|
-
* Convenience function combining classify() and routeBackend().
|
|
369
|
-
*
|
|
370
|
-
* @param {string} message — The user's query string.
|
|
371
|
-
* @returns {[number, string]} Tuple of [complexity level, backend name].
|
|
372
|
-
*/
|
|
373
|
-
export function classifyAndRoute(message) {
|
|
374
|
-
const level = classify(message);
|
|
375
|
-
const backend = routeBackend(level);
|
|
376
|
-
return [level, backend];
|
|
377
|
-
}
|