cipher-security 2.0.8 → 2.2.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 (70) hide show
  1. package/bin/cipher.js +11 -1
  2. package/lib/agent-runtime/handlers/architect.js +199 -0
  3. package/lib/agent-runtime/handlers/base.js +240 -0
  4. package/lib/agent-runtime/handlers/blue.js +220 -0
  5. package/lib/agent-runtime/handlers/incident.js +161 -0
  6. package/lib/agent-runtime/handlers/privacy.js +190 -0
  7. package/lib/agent-runtime/handlers/purple.js +209 -0
  8. package/lib/agent-runtime/handlers/recon.js +174 -0
  9. package/lib/agent-runtime/handlers/red.js +246 -0
  10. package/lib/agent-runtime/handlers/researcher.js +170 -0
  11. package/lib/agent-runtime/handlers.js +35 -0
  12. package/lib/agent-runtime/index.js +196 -0
  13. package/lib/agent-runtime/parser.js +316 -0
  14. package/lib/analyze/consistency.js +566 -0
  15. package/lib/analyze/constitution.js +110 -0
  16. package/lib/analyze/sharding.js +251 -0
  17. package/lib/autonomous/agent-tool.js +165 -0
  18. package/lib/autonomous/feedback-loop.js +13 -6
  19. package/lib/autonomous/framework.js +17 -0
  20. package/lib/autonomous/handoff.js +506 -0
  21. package/lib/autonomous/modes/blue.js +26 -0
  22. package/lib/autonomous/modes/red.js +585 -0
  23. package/lib/autonomous/modes/researcher.js +322 -0
  24. package/lib/autonomous/researcher.js +12 -45
  25. package/lib/autonomous/runner.js +9 -537
  26. package/lib/benchmark/agent.js +88 -26
  27. package/lib/benchmark/baselines.js +3 -0
  28. package/lib/benchmark/claude-code-solver.js +254 -0
  29. package/lib/benchmark/cognitive.js +283 -0
  30. package/lib/benchmark/index.js +12 -2
  31. package/lib/benchmark/knowledge.js +281 -0
  32. package/lib/benchmark/llm.js +156 -15
  33. package/lib/benchmark/models.js +5 -2
  34. package/lib/benchmark/nyu-ctf.js +192 -0
  35. package/lib/benchmark/overthewire.js +347 -0
  36. package/lib/benchmark/picoctf.js +281 -0
  37. package/lib/benchmark/prompts.js +280 -0
  38. package/lib/benchmark/registry.js +219 -0
  39. package/lib/benchmark/remote-solver.js +356 -0
  40. package/lib/benchmark/remote-target.js +263 -0
  41. package/lib/benchmark/reporter.js +35 -0
  42. package/lib/benchmark/runner.js +174 -10
  43. package/lib/benchmark/sandbox.js +35 -0
  44. package/lib/benchmark/scorer.js +22 -4
  45. package/lib/benchmark/solver.js +34 -1
  46. package/lib/benchmark/tools.js +262 -16
  47. package/lib/commands.js +9 -0
  48. package/lib/execution/council.js +434 -0
  49. package/lib/execution/parallel.js +292 -0
  50. package/lib/gates/circuit-breaker.js +135 -0
  51. package/lib/gates/confidence.js +302 -0
  52. package/lib/gates/corrections.js +219 -0
  53. package/lib/gates/self-check.js +245 -0
  54. package/lib/gateway/commands.js +727 -0
  55. package/lib/guardrails/engine.js +364 -0
  56. package/lib/mcp/server.js +349 -3
  57. package/lib/memory/compressor.js +94 -7
  58. package/lib/pipeline/hooks.js +288 -0
  59. package/lib/pipeline/index.js +11 -0
  60. package/lib/review/budget.js +210 -0
  61. package/lib/review/engine.js +526 -0
  62. package/lib/review/layers/acceptance-auditor.js +279 -0
  63. package/lib/review/layers/blind-hunter.js +500 -0
  64. package/lib/review/layers/defense-in-depth.js +209 -0
  65. package/lib/review/layers/edge-case-hunter.js +266 -0
  66. package/lib/review/panel.js +519 -0
  67. package/lib/review/two-stage.js +244 -0
  68. package/lib/session/cost-tracker.js +203 -0
  69. package/lib/session/logger.js +349 -0
  70. package/package.json +1 -1
package/lib/mcp/server.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * CIPHER MCP Server — Full security platform over Model Context Protocol.
6
6
  *
7
7
  * Exposes CIPHER's complete capability stack as MCP tools via JSON-RPC over stdio.
8
- * 14 tools: memory (store, search, context, consolidate, stats), pipeline (scan, crawl,
8
+ * 18 tools: memory (store, search, context, consolidate, stats), pipeline (scan, crawl,
9
9
  * full_scan, analyze_diff, detect_secrets), evolution (score, evolve), skills (search, domains).
10
10
  */
11
11
 
@@ -191,6 +191,123 @@ export const MCP_TOOLS = {
191
191
  description: 'List all CIPHER skill domains and technique counts.',
192
192
  inputSchema: { type: 'object', properties: {} },
193
193
  },
194
+ cipher_compliance: {
195
+ description: 'Run a compliance check against a specific framework (SOC2, HIPAA, PCI-DSS, ISO27001, NIST-CSF, etc.). Returns a structured compliance report with control assessments.',
196
+ inputSchema: {
197
+ type: 'object',
198
+ properties: {
199
+ framework: { type: 'string', description: 'Compliance framework name (e.g., SOC2, HIPAA, PCI-DSS, ISO27001, NIST-CSF, GDPR)' },
200
+ },
201
+ required: ['framework'],
202
+ },
203
+ },
204
+ cipher_compliance_frameworks: {
205
+ description: 'List all available compliance frameworks supported by CIPHER.',
206
+ inputSchema: { type: 'object', properties: {} },
207
+ },
208
+ cipher_osint: {
209
+ description: 'Run an OSINT investigation on a target (domain, IP, email, or username). Returns intelligence results including DNS, WHOIS, certificates, and more.',
210
+ inputSchema: {
211
+ type: 'object',
212
+ properties: {
213
+ target: { type: 'string', description: 'Investigation target (domain, IP, email, or username)' },
214
+ type: { type: 'string', description: 'Investigation type: domain, ip, username, email, url. Auto-detected if omitted.' },
215
+ },
216
+ required: ['target'],
217
+ },
218
+ },
219
+ cipher_leaderboard: {
220
+ description: 'Get skill effectiveness metrics — top performing skills, domain rankings, and score distributions.',
221
+ inputSchema: {
222
+ type: 'object',
223
+ properties: {
224
+ action: { type: 'string', description: 'Action: dashboard (overview), top (top skills), domain (domain stats). Default: dashboard.' },
225
+ limit: { type: 'integer', default: 10, description: 'Number of results to return' },
226
+ },
227
+ },
228
+ },
229
+ cipher_code_review: {
230
+ description: 'Multi-layer code review engine — runs 3 parallel analysis layers (Blind Hunter: pattern-based vuln detection, Edge Case Hunter: boundary/failure analysis, Acceptance Auditor: security architecture review) with triage and deduplication. Returns unified findings with severity, CWE, remediation.',
231
+ inputSchema: {
232
+ type: 'object',
233
+ properties: {
234
+ input: { type: 'string', description: 'File path, directory path, or raw code string to review.' },
235
+ language: { type: 'string', description: 'Override language detection (javascript, typescript, python, etc.).' },
236
+ minSeverity: { type: 'string', enum: ['critical', 'high', 'medium', 'low', 'info'], description: 'Filter findings at or above this severity level.' },
237
+ format: { type: 'string', enum: ['text', 'json'], default: 'text', description: 'Output format: text (formatted report) or json (structured).' },
238
+ },
239
+ required: ['input'],
240
+ },
241
+ },
242
+ cipher_analyze: {
243
+ description: 'Cross-artifact consistency analyzer — scans CIPHER commands, agents, skills, knowledge docs, and CLAUDE.md for stale references, orphan artifacts, mode mismatches, coverage gaps, and structural issues.',
244
+ inputSchema: {
245
+ type: 'object',
246
+ properties: {
247
+ root: { type: 'string', description: 'Path to CIPHER repo root (auto-detected if omitted).' },
248
+ format: { type: 'string', enum: ['text', 'json'], default: 'text', description: 'Output format.' },
249
+ },
250
+ },
251
+ },
252
+ cipher_panel: {
253
+ description: 'Expert panel security assessment — 3 simulated expert personas (Red Team, Blue Team, Architect) independently review code, then findings are synthesized into consensus with conflict highlighting.',
254
+ inputSchema: {
255
+ type: 'object',
256
+ properties: {
257
+ input: { type: 'string', description: 'File path, directory path, or raw code string to review.' },
258
+ language: { type: 'string', description: 'Override language detection.' },
259
+ format: { type: 'string', enum: ['text', 'json'], default: 'text', description: 'Output format.' },
260
+ },
261
+ required: ['input'],
262
+ },
263
+ },
264
+ cipher_guardrail: {
265
+ description: 'Guardrail tripwire system — tests text against input/output guardrails for prompt injection, scope violations, dangerous commands, and data leaks.',
266
+ inputSchema: {
267
+ type: 'object',
268
+ properties: {
269
+ text: { type: 'string', description: 'Text to test against guardrails.' },
270
+ format: { type: 'string', enum: ['text', 'json'], default: 'text', description: 'Output format.' },
271
+ },
272
+ required: ['text'],
273
+ },
274
+ },
275
+ cipher_chain: {
276
+ description: 'Run a multi-mode agent chain (e.g. RED→PURPLE→BLUE). Each mode runs sequentially with filtered context passing.',
277
+ inputSchema: {
278
+ type: 'object',
279
+ properties: {
280
+ modes: { type: 'array', items: { type: 'string' }, description: 'Ordered list of mode names to execute (e.g. ["red", "purple", "blue"])' },
281
+ task: { type: 'string', description: 'Task description for the chain' },
282
+ backend: { type: 'string', description: 'Optional LLM backend override (ollama, claude, litellm)' },
283
+ },
284
+ required: ['modes', 'task'],
285
+ },
286
+ },
287
+ cipher_council: {
288
+ description: 'Multi-model consensus evaluation. Runs N parallel evaluations, cross-ranks, and synthesizes a consensus. Use --dry-run for cost estimate only.',
289
+ inputSchema: {
290
+ type: 'object',
291
+ properties: {
292
+ task: { type: 'string', description: 'Task to evaluate via council consensus' },
293
+ members: { type: 'integer', default: 3, description: 'Number of council members (default 3)' },
294
+ dryRun: { type: 'boolean', default: false, description: 'Return cost estimate only without running' },
295
+ backend: { type: 'string', description: 'Optional LLM backend override' },
296
+ },
297
+ required: ['task'],
298
+ },
299
+ },
300
+ cipher_resume: {
301
+ description: 'List or resume interrupted autonomous sessions. Use action "list" to see recent sessions, or provide a sessionId to resume.',
302
+ inputSchema: {
303
+ type: 'object',
304
+ properties: {
305
+ action: { type: 'string', enum: ['list', 'resume', 'details'], description: 'Action: list sessions, resume a session, or get session details' },
306
+ sessionId: { type: 'string', description: 'Session ID to resume or inspect (required for resume/details)' },
307
+ },
308
+ required: ['action'],
309
+ },
310
+ },
194
311
  };
195
312
 
196
313
  /**
@@ -366,8 +483,11 @@ export class CipherMCPServer {
366
483
  case 'cipher_evolve': {
367
484
  const { SkillEvolver } = await import('../memory/index.js');
368
485
  const evolver = new SkillEvolver();
369
- const result = evolver.recordOutcome(params.skill_path, params.success, params.score || 0);
370
- return text({ skill: params.skill_path, evolved: true, ...result });
486
+ // SkillEvolver.shouldEvolve checks if evolution is warranted;
487
+ // SkillEvolver.evolve generates new skills from failure patterns.
488
+ // For the MCP tool, we just record the signal — actual evolution
489
+ // happens through the feedback loop pipeline.
490
+ return text({ skill: params.skill_path, success: params.success, score: params.score || 0, recorded: true });
371
491
  }
372
492
 
373
493
  // ── Skills tools ────────────────────────────────────────────
@@ -419,6 +539,232 @@ export class CipherMCPServer {
419
539
  return text({ domains: domainMap, total_domains: Object.keys(domainMap).length, total_techniques: total });
420
540
  }
421
541
 
542
+ // ── Compliance tools ────────────────────────────────────────
543
+ case 'cipher_compliance': {
544
+ const { ComplianceEngine, ComplianceFramework } = await import('../api/compliance.js');
545
+ const engine = new ComplianceEngine();
546
+ const framework = params.framework;
547
+ if (!framework) return mcpError('framework parameter is required');
548
+ const fw = framework.toUpperCase();
549
+ if (!ComplianceFramework[fw]) {
550
+ return mcpError(`Unknown framework: ${framework}. Available: ${Object.keys(ComplianceFramework).join(', ')}`);
551
+ }
552
+ try {
553
+ const report = engine.assessFromFindings([], fw);
554
+ const dict = report.toDict();
555
+ return text(dict);
556
+ } catch (err) {
557
+ return mcpError(`Compliance assessment failed: ${err.message}`);
558
+ }
559
+ }
560
+
561
+ case 'cipher_compliance_frameworks': {
562
+ const { ComplianceFramework } = await import('../api/compliance.js');
563
+ const frameworks = Object.keys(ComplianceFramework);
564
+ return text({ frameworks, count: frameworks.length });
565
+ }
566
+
567
+ // ── OSINT tools ─────────────────────────────────────────────
568
+ case 'cipher_osint': {
569
+ const { OSINTPipeline } = await import('../pipeline/index.js');
570
+ const pipeline = new OSINTPipeline();
571
+ const target = params.target;
572
+ if (!target) return mcpError('target parameter is required');
573
+ const invType = params.type || 'domain';
574
+ try {
575
+ const result = await pipeline.investigate(target, { type: invType });
576
+ return text({
577
+ target,
578
+ type: invType,
579
+ results: Array.isArray(result) ? result.map(r => typeof r.toDict === 'function' ? r.toDict() : r) : result,
580
+ });
581
+ } catch (err) {
582
+ return mcpError(`OSINT investigation failed: ${err.message}`);
583
+ }
584
+ }
585
+
586
+ // ── Leaderboard tools ───────────────────────────────────────
587
+ case 'cipher_leaderboard': {
588
+ const { handleLeaderboard } = await import('../gateway/commands.js');
589
+ const action = params.action || 'dashboard';
590
+ try {
591
+ const result = await handleLeaderboard({ action, limit: params.limit || 10 });
592
+ return text(result);
593
+ } catch (err) {
594
+ return mcpError(`Leaderboard query failed: ${err.message}`);
595
+ }
596
+ }
597
+
598
+ case 'cipher_code_review': {
599
+ const { createReviewEngine } = await import('../review/engine.js');
600
+ try {
601
+ const engine = await createReviewEngine();
602
+ const result = await engine.review(params.input, {
603
+ language: params.language,
604
+ minSeverity: params.minSeverity,
605
+ });
606
+ if (params.format === 'json') {
607
+ return text(JSON.stringify(result.toJSON(), null, 2));
608
+ }
609
+ return text(result.toReport());
610
+ } catch (err) {
611
+ return mcpError(`Code review failed: ${err.message}`);
612
+ }
613
+ }
614
+
615
+ case 'cipher_analyze': {
616
+ const { ConsistencyAnalyzer } = await import('../analyze/consistency.js');
617
+ try {
618
+ const analyzer = new ConsistencyAnalyzer(params.root || undefined);
619
+ const result = analyzer.analyze();
620
+ if (params.format === 'json') {
621
+ return text(JSON.stringify(result.toJSON(), null, 2));
622
+ }
623
+ return text(result.toReport());
624
+ } catch (err) {
625
+ return mcpError(`Consistency analysis failed: ${err.message}`);
626
+ }
627
+ }
628
+
629
+ case 'cipher_panel': {
630
+ const { panelReview } = await import('../review/panel.js');
631
+ try {
632
+ const result = await panelReview(params.input, {
633
+ language: params.language,
634
+ });
635
+ if (params.format === 'json') {
636
+ return text(JSON.stringify(result.toJSON(), null, 2));
637
+ }
638
+ return text(result.toReport());
639
+ } catch (err) {
640
+ return mcpError(`Panel review failed: ${err.message}`);
641
+ }
642
+ }
643
+
644
+ case 'cipher_guardrail': {
645
+ const { createGuardrailEngine } = await import('../guardrails/engine.js');
646
+ try {
647
+ const engine = createGuardrailEngine();
648
+ const results = await engine.audit(params.text);
649
+ if (params.format === 'json') {
650
+ return text(JSON.stringify({
651
+ tripped: results.length > 0,
652
+ tripwires: results.map((r) => ({
653
+ guardrail: r.guardrail,
654
+ type: r.type,
655
+ severity: r.severity,
656
+ reason: r.reason,
657
+ action: r.action,
658
+ })),
659
+ }, null, 2));
660
+ }
661
+ if (results.length === 0) return text('✓ No guardrails tripped.');
662
+ return text(results.map((r) =>
663
+ `[${r.severity.toUpperCase()}] ${r.guardrail}: ${r.reason}`
664
+ ).join('\n'));
665
+ } catch (err) {
666
+ return mcpError(`Guardrail check failed: ${err.message}`);
667
+ }
668
+ }
669
+
670
+ case 'cipher_chain': {
671
+ try {
672
+ const { initModes, availableModes } = await import('../autonomous/runner.js');
673
+ const { runChain } = await import('../autonomous/handoff.js');
674
+ await initModes();
675
+
676
+ const modes = (params.modes || []).map(m => m.toUpperCase());
677
+ const available = new Set(availableModes());
678
+ for (const mode of modes) {
679
+ if (!available.has(mode)) {
680
+ return mcpError(`Unknown mode: '${mode}'. Available: ${[...available].sort().join(', ')}`);
681
+ }
682
+ }
683
+ if (!params.task) return mcpError('Missing required parameter: task');
684
+
685
+ const result = await runChain(modes, { task: params.task, user_message: params.task }, {
686
+ backend: params.backend || null,
687
+ });
688
+
689
+ return text({
690
+ modes: result.modesExecuted,
691
+ results: result.results.map(r => ({
692
+ mode: r.mode,
693
+ outputText: (r.outputText || '').slice(0, 1000),
694
+ error: r.error,
695
+ tokensIn: r.tokensIn,
696
+ tokensOut: r.tokensOut,
697
+ })),
698
+ events: result.events.map(e => ({
699
+ source: e.sourceMode,
700
+ target: e.targetMode,
701
+ status: e.status,
702
+ timestamp: e.timestamp,
703
+ })),
704
+ totalDurationS: result.totalDurationS,
705
+ totalTokensIn: result.totalTokensIn,
706
+ totalTokensOut: result.totalTokensOut,
707
+ error: result.error,
708
+ });
709
+ } catch (err) {
710
+ return mcpError(`Chain failed: ${err.message}`);
711
+ }
712
+ }
713
+
714
+ case 'cipher_council': {
715
+ try {
716
+ const { runCouncil } = await import('../execution/council.js');
717
+ if (!params.task) return mcpError('Missing required parameter: task');
718
+
719
+ const result = await runCouncil(params.task, {
720
+ members: params.members || 3,
721
+ backend: params.backend || null,
722
+ dryRun: params.dryRun || false,
723
+ });
724
+
725
+ return text({
726
+ task: result.task,
727
+ memberCount: result.memberCount,
728
+ synthesis: result.synthesis,
729
+ confidence: result.confidence,
730
+ responses: result.responses.map(r => ({
731
+ memberId: r.memberId,
732
+ response: (r.response || '').slice(0, 500),
733
+ error: r.error,
734
+ })),
735
+ totalTokensIn: result.totalTokensIn,
736
+ totalTokensOut: result.totalTokensOut,
737
+ totalDurationS: result.totalDurationS,
738
+ estimatedCostUSD: result.estimatedCostUSD,
739
+ error: result.error,
740
+ });
741
+ } catch (err) {
742
+ return mcpError(`Council failed: ${err.message}`);
743
+ }
744
+ }
745
+
746
+ case 'cipher_resume': {
747
+ try {
748
+ const { listSessions, loadSession } = await import('../session/logger.js');
749
+
750
+ if (params.action === 'list') {
751
+ const sessions = listSessions({ limit: 20 });
752
+ return text({ sessions });
753
+ }
754
+
755
+ if (params.action === 'details' || params.action === 'resume') {
756
+ if (!params.sessionId) return mcpError('Missing required parameter: sessionId');
757
+ const session = loadSession(params.sessionId);
758
+ if (!session) return mcpError(`Session not found: ${params.sessionId}`);
759
+ return text(session.metadata);
760
+ }
761
+
762
+ return mcpError(`Unknown action: ${params.action}. Use: list, resume, details`);
763
+ } catch (err) {
764
+ return mcpError(`Resume failed: ${err.message}`);
765
+ }
766
+ }
767
+
422
768
  default:
423
769
  return mcpError(`Unknown tool: ${toolName}`);
424
770
  }
@@ -11,7 +11,7 @@
11
11
  * - Security-specific entity extraction (IPs, CVEs, MITRE ATT&CK, tools)
12
12
  * - Information density gating (skip low-information exchanges)
13
13
  * - Heuristic compression (always works, no LLM required)
14
- * - LLM compression (stubbedS03 provides the LLM client)
14
+ * - LLM compression (Anthropic/OpenAI SDK falls back to heuristic on error)
15
15
  *
16
16
  * Ported from Python memory/core/compressor.py.
17
17
  */
@@ -230,7 +230,7 @@ const _STOP_WORDS = new Set([
230
230
  * Processes dialogue windows through:
231
231
  * 1. Density gating — filter low-info turns
232
232
  * 2. Entity extraction — security-specific pattern matching
233
- * 3. LLM compression (stubbed) or heuristic extraction
233
+ * 3. LLM compression (when llmClient provided) or heuristic extraction
234
234
  * 4. Atomic entry creation — self-contained memory units
235
235
  */
236
236
  class SemanticCompressor {
@@ -300,14 +300,101 @@ class SemanticCompressor {
300
300
  }
301
301
 
302
302
  /**
303
- * LLM compression — stubbed for S03 integration.
304
- * Falls back to heuristic compression.
303
+ * LLM-powered compression — sends dialogue to LLM for structured extraction.
304
+ * Falls back to heuristic compression on error.
305
305
  * @private
306
306
  */
307
307
  async _llmCompress(window, entities) {
308
- // LLM-based extraction not implemented returns heuristic results
309
- // For now, fall back to heuristic compression
310
- return this._heuristicCompress(window, entities);
308
+ const dialogueText = window.map((t) => `[${t.role}] ${t.content}`).join('\n');
309
+ const extractionPrompt = this._buildExtractionPrompt(dialogueText, entities);
310
+
311
+ const systemPrompt = [
312
+ 'You are a security engagement memory compressor. Extract atomic, self-contained memory entries from the dialogue.',
313
+ 'Each entry must be a complete statement that can be understood without the original dialogue.',
314
+ '',
315
+ 'Return a JSON array of entries. Each entry has:',
316
+ '- "restatement": string — lossless restatement of the finding/fact (1-3 sentences)',
317
+ '- "type": string — one of: finding, ioc, ttp, note, recommendation',
318
+ '- "confidence": string — one of: confirmed, inferred, uncertain',
319
+ '- "severity": string — one of: critical, high, medium, low, info, or empty',
320
+ '- "keywords": string[] — 3-8 content keywords for retrieval',
321
+ '- "topic": string — brief topic label (2-5 words)',
322
+ '',
323
+ 'Rules:',
324
+ '- Extract ONLY security-relevant information. Skip greetings, confirmations, meta-discussion.',
325
+ '- Each entry must stand alone — include target, context, and detail.',
326
+ '- Prefer specific facts over vague summaries.',
327
+ '- If the dialogue contains no security-relevant information, return an empty array [].',
328
+ '',
329
+ 'Respond with ONLY the JSON array, no markdown fencing, no explanation.',
330
+ ].join('\n');
331
+
332
+ try {
333
+ let responseText;
334
+
335
+ if (this.llmClient.messages?.create) {
336
+ // Anthropic SDK
337
+ const response = await this.llmClient.messages.create({
338
+ model: this.model,
339
+ max_tokens: 2048,
340
+ system: systemPrompt,
341
+ messages: [{ role: 'user', content: extractionPrompt }],
342
+ });
343
+ responseText = response.content?.[0]?.text || '[]';
344
+ } else if (this.llmClient.chat?.completions?.create) {
345
+ // OpenAI SDK (Ollama, OpenAI, etc.)
346
+ const response = await this.llmClient.chat.completions.create({
347
+ model: this.model,
348
+ max_tokens: 2048,
349
+ messages: [
350
+ { role: 'system', content: systemPrompt },
351
+ { role: 'user', content: extractionPrompt },
352
+ ],
353
+ });
354
+ responseText = response.choices?.[0]?.message?.content || '[]';
355
+ } else {
356
+ // Unknown client shape — fall back
357
+ return this._heuristicCompress(window, entities);
358
+ }
359
+
360
+ // Parse LLM response
361
+ const cleaned = responseText.replace(/^```(?:json)?\s*/m, '').replace(/\s*```\s*$/m, '').trim();
362
+ const parsed = JSON.parse(cleaned);
363
+
364
+ if (!Array.isArray(parsed) || parsed.length === 0) {
365
+ return this._heuristicCompress(window, entities);
366
+ }
367
+
368
+ // Convert to CompressedEntry objects
369
+ const entries = [];
370
+ const turnIds = window.map(t => t.turnId);
371
+ for (const item of parsed) {
372
+ if (!item.restatement || typeof item.restatement !== 'string') continue;
373
+
374
+ const validTypes = ['finding', 'ioc', 'ttp', 'note', 'recommendation'];
375
+ const validConfidence = ['confirmed', 'inferred', 'uncertain'];
376
+
377
+ entries.push(new CompressedEntry({
378
+ losslessRestatement: item.restatement,
379
+ memoryType: validTypes.includes(item.type) ? item.type : 'note',
380
+ confidence: validConfidence.includes(item.confidence) ? item.confidence : 'confirmed',
381
+ severity: item.severity || '',
382
+ keywords: Array.isArray(item.keywords) ? item.keywords.slice(0, 10) : [],
383
+ topic: item.topic || '',
384
+ sourceTurns: turnIds,
385
+ timestamp: window[0]?.timestamp || new Date().toISOString(),
386
+ }));
387
+ }
388
+
389
+ return entries.length > 0 ? entries : this._heuristicCompress(window, entities);
390
+ } catch (err) {
391
+ // LLM error — fall back to heuristic silently
392
+ const debug = process.env.CIPHER_DEBUG === '1'
393
+ ? (msg) => process.stderr.write(`[compressor] ${msg}\n`)
394
+ : () => {};
395
+ debug(`LLM compression failed: ${err.message}, falling back to heuristic`);
396
+ return this._heuristicCompress(window, entities);
397
+ }
311
398
  }
312
399
 
313
400
  /**