cipher-security 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cipher.js CHANGED
@@ -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 python-bridge.js, spawns the Python
13
- * gateway, routes JSON-RPC commands, prints results, and exits.
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
- // Node.js-native commands — handled before the Python bridge is loaded
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
- const flags = [];
218
- const positional = [];
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
- if (positional.length === 0) {
229
- // No positional args — show help
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: [...positional.slice(1), ...flags],
231
+ commandArgs: argv.slice(1), // Pass all args as-is, preserve flag-value order
240
232
  };
241
233
  }
242
234
 
243
- // Bare query — join all positional words as query text
235
+ // Bare query — join everything as query text
244
236
  return {
245
237
  command: 'query',
246
- commandArgs: [...positional, ...flags],
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 bridge.py:
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 — Node.js handler functions (no Python) ────────────
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 (no Python) ──────────
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';
@@ -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
  *
@@ -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.py
176
- const agentPy = join(fullPath, 'scripts', 'agent.py');
177
- if (existsSync(agentPy)) {
178
- const agentContent = readFileSync(agentPy, 'utf-8');
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.py too short (${agentLines.length} lines)`);
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('argparse')) issues.push('agent.py missing argparse');
185
- if (!agentContent.includes('json')) issues.push('agent.py missing JSON output');
186
- if (!agentContent.includes('__main__')) issues.push('agent.py missing __main__ guard');
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.py missing');
188
+ issues.push('scripts/agent.js missing');
189
189
  scores.agent_quality = 0;
190
190
  }
191
191
 
@@ -2,6 +2,8 @@
2
2
  // Licensed under AGPL-3.0 — see LICENSE file for details.
3
3
  // CIPHER is a trademark of defconxt.
4
4
 
5
+ import { createRequire } from "node:module";
6
+ const require = createRequire(import.meta.url);
5
7
  /**
6
8
  * Skill effectiveness tracking and metrics system.
7
9
  *
@@ -306,8 +306,8 @@ ${description}
306
306
  ## Verification
307
307
 
308
308
  \`\`\`bash
309
- python3 scripts/agent.py analyze --target example
310
- python3 scripts/agent.py report --format json
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
- | \`python3 agent.py analyze --target <t>\` | Analyze a target for ${techniqueName} indicators |
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.py, and api-reference.md.
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.py');
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/commands.js CHANGED
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Maps all 29 CLI commands to one of three dispatch modes:
9
9
  * - native: Dispatched directly through Node.js handler functions (no Python)
10
- * - passthrough: Spawned directly as `python -m gateway.app <cmd>` with stdio: 'inherit'
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 pyproject.toml lives)
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 pyproject.toml — same logic as Python's
29
- * _find_project_root(). Returns the directory containing pyproject.toml, or null.
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, 'pyproject.toml'))) {
37
+ if (existsSync(join(current, 'cli'))) {
38
38
  return current;
39
39
  }
40
40
  const parent = dirname(current);
@@ -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.query || '',
97
- args.engagement || '',
98
- args.limit || 10,
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.query || '',
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.type || 'note';
169
+ const typeStr = getFlagArg(args, 'type', '--type', 'note');
133
170
  const entry = new MemoryEntry({
134
- content: args.content || '',
171
+ content,
135
172
  memoryType: Object.values(MemoryType).includes(typeStr) ? typeStr : MemoryType.NOTE,
136
- severity: args.severity || '',
137
- engagementId: args.engagement || '',
138
- tags: Array.isArray(args.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, '*.py', 'scripts');
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.file;
253
- if (!file || !existsSync(file)) {
254
- return { error: true, message: `File not found: ${file || '(none)'}` };
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 { SarifReport } = await import('../pipeline/index.js');
397
- const input = args.input;
398
- if (!input || !existsSync(input)) {
399
- return { error: true, message: `Input file not found: ${input || '(none)'}` };
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 target = args.target || '';
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.query || '').toLowerCase();
497
- const limit = args.limit || 10;
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: args.query || '', count: 0, results: [] };
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: args.query || '', count: results.length, results };
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
- return {
688
- error: true,
689
- message: 'Leaderboard requires autonomous framework (available after S04). Use Python bridge: cipher leaderboard --bridge',
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
- return {
698
- error: true,
699
- message: 'Feedback loop requires autonomous framework (available after S04). Use Python bridge: cipher feedback --bridge',
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
- return {
708
- error: true,
709
- message: 'Marketplace requires api/marketplace module (available after S05). Use Python bridge: cipher marketplace --bridge',
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
- return {
718
- error: true,
719
- message: 'Compliance requires api/compliance module (available after S05). Use Python bridge: cipher compliance --bridge',
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
- return {
799
- output: JSON.stringify({
800
- status: 'info',
801
- message: 'Dashboard TUI is not yet available in Node.js mode. Use `cipher status` for system status or `cipher api` for the REST API.',
802
- }, null, 2),
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
- return {
812
- output: JSON.stringify({
813
- status: 'info',
814
- message: 'The web interface has been consolidated into `cipher api`. Use `cipher api --no-auth --port 8443` to start the REST API server.',
815
- }, null, 2),
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
  // ---------------------------------------------------------------------------
@@ -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 pyproject.toml or package.json.
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, 'pyproject.toml')) ||
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 pyproject.toml, it's the repo root
46
- if (existsSync(join(current, 'pyproject.toml'))) {
45
+ // If it has cli/, it is the repo root
46
+ if (existsSync(join(current, 'cli'))) {
47
47
  return current;
48
48
  }
49
49
  }
@@ -305,7 +305,7 @@ class SemanticCompressor {
305
305
  * @private
306
306
  */
307
307
  async _llmCompress(window, entities) {
308
- // TODO: S03 LLM integrationcall this.llmClient with extraction prompt
308
+ // LLM-based extraction not implementedreturns heuristic results
309
309
  // For now, fall back to heuristic compression
310
310
  return this._heuristicCompress(window, entities);
311
311
  }
@@ -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
- const symbolicResults = this.symbolic.searchSymbolic({
492
- engagementId: filters.engagementId ?? '',
493
- memoryType: filters.memoryType ?? '',
494
- severity: filters.severity ?? '',
495
- mitreTechnique: filters.mitreTechnique ?? '',
496
- limit: limit * 2,
497
- });
498
- if (symbolicResults.length > 0) {
499
- resultLists.push(symbolicResults.map((r) => r.entry_id));
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-python@v5
682
- with: { python-version: "3.12" }
683
- - run: pip install cipher-security
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-python@v5
726
- with: { python-version: "3.12" }
727
- - { shell: bash, run: "pip install cipher-security" }
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:
@@ -251,16 +251,7 @@ export async function runSetupWizard() {
251
251
  // ── Write config ──────────────────────────────────────────────────
252
252
  const configPath = writeConfig(config);
253
253
 
254
- // Also write to project root if pyproject.toml exists (dual-write like Python)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cipher-security",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "CIPHER — AI Security Engineering Platform CLI",
5
5
  "type": "module",
6
6
  "engines": {