holomime 1.5.0 → 1.6.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.
@@ -564,26 +564,85 @@ function loadCustomDetectors(dir) {
564
564
  }
565
565
  let files;
566
566
  try {
567
- files = readdirSync(detectorsDir).filter((f) => f.endsWith(".json"));
567
+ files = readdirSync(detectorsDir).filter((f) => f.endsWith(".json") || f.endsWith(".md"));
568
568
  } catch {
569
569
  return { detectors: [], errors: ["Could not read detectors directory"] };
570
570
  }
571
571
  for (const file of files) {
572
572
  const filepath = join2(detectorsDir, file);
573
573
  try {
574
- const raw = JSON.parse(readFileSync2(filepath, "utf-8"));
575
- const validation = validateDetectorConfig(raw);
576
- if (!validation.valid) {
577
- errors.push(`${file}: ${validation.errors.join(", ")}`);
578
- continue;
574
+ let config;
575
+ if (file.endsWith(".md")) {
576
+ const parsed = parseMarkdownDetector(readFileSync2(filepath, "utf-8"));
577
+ if (!parsed) {
578
+ errors.push(`${file}: could not parse Markdown detector (missing frontmatter or ## Patterns section)`);
579
+ continue;
580
+ }
581
+ const validation = validateDetectorConfig(parsed);
582
+ if (!validation.valid) {
583
+ errors.push(`${file}: ${validation.errors.join(", ")}`);
584
+ continue;
585
+ }
586
+ config = validation.config;
587
+ } else {
588
+ const raw = JSON.parse(readFileSync2(filepath, "utf-8"));
589
+ const validation = validateDetectorConfig(raw);
590
+ if (!validation.valid) {
591
+ errors.push(`${file}: ${validation.errors.join(", ")}`);
592
+ continue;
593
+ }
594
+ config = validation.config;
579
595
  }
580
- detectors.push(compileCustomDetector(validation.config));
596
+ detectors.push(compileCustomDetector(config));
581
597
  } catch (e) {
582
598
  errors.push(`${file}: ${e instanceof Error ? e.message : "parse error"}`);
583
599
  }
584
600
  }
585
601
  return { detectors, errors };
586
602
  }
603
+ function parseMarkdownDetector(markdown) {
604
+ const frontmatterMatch = markdown.match(/^---\n([\s\S]*?)\n---/);
605
+ if (!frontmatterMatch) return null;
606
+ const frontmatter = frontmatterMatch[1];
607
+ const meta = {};
608
+ for (const line of frontmatter.split("\n")) {
609
+ const colonIdx = line.indexOf(":");
610
+ if (colonIdx === -1) continue;
611
+ const key = line.slice(0, colonIdx).trim();
612
+ let value = line.slice(colonIdx + 1).trim();
613
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
614
+ value = value.slice(1, -1);
615
+ }
616
+ meta[key] = value;
617
+ }
618
+ if (!meta.id || !meta.name) return null;
619
+ const body = markdown.slice(frontmatterMatch[0].length);
620
+ const patternsMatch = body.match(/##\s*Patterns\s*\n([\s\S]*?)(?=\n##|\n*$)/i);
621
+ const patterns = [];
622
+ if (patternsMatch) {
623
+ const patternLines = patternsMatch[1].split("\n").filter((l) => l.trim().startsWith("-"));
624
+ for (const line of patternLines) {
625
+ const regexMatch = line.match(/`([^`]+)`/);
626
+ const weightMatch = line.match(/weight\s*=\s*([\d.]+)/i);
627
+ if (regexMatch) {
628
+ patterns.push({
629
+ regex: regexMatch[1],
630
+ weight: weightMatch ? parseFloat(weightMatch[1]) : 1
631
+ });
632
+ }
633
+ }
634
+ }
635
+ if (patterns.length === 0) return null;
636
+ return {
637
+ id: meta.id,
638
+ name: meta.name,
639
+ description: meta.description ?? meta.name,
640
+ severity: meta.severity ?? "warning",
641
+ patterns,
642
+ threshold: meta.threshold ? parseInt(meta.threshold, 10) : 15,
643
+ prescription: meta.prescription
644
+ };
645
+ }
587
646
 
588
647
  // src/analysis/diagnose-core.ts
589
648
  function runDiagnosis(messages) {
@@ -972,13 +1031,20 @@ function updatePatternTracker(memory, patternId, severity, interventions) {
972
1031
  status: "active",
973
1032
  interventionsAttempted: [],
974
1033
  lastSeverity: severity,
975
- lastSeen: now
1034
+ lastSeen: now,
1035
+ confidence: 0,
1036
+ trending: "stable",
1037
+ severityHistory: []
976
1038
  };
977
1039
  memory.patterns.push(tracker);
978
1040
  }
979
1041
  tracker.sessionCount++;
980
1042
  tracker.lastSeverity = severity;
981
1043
  tracker.lastSeen = now;
1044
+ if (!tracker.severityHistory) tracker.severityHistory = [];
1045
+ tracker.severityHistory.push(severity);
1046
+ tracker.confidence = Math.min(1, 1 - Math.exp(-tracker.sessionCount / 3));
1047
+ tracker.trending = computeTrending(tracker.severityHistory.slice(-5));
982
1048
  for (const intervention of interventions) {
983
1049
  if (!tracker.interventionsAttempted.includes(intervention)) {
984
1050
  tracker.interventionsAttempted.push(intervention);
@@ -992,6 +1058,19 @@ function updatePatternTracker(memory, patternId, severity, interventions) {
992
1058
  tracker.status = "improving";
993
1059
  }
994
1060
  }
1061
+ function computeTrending(history) {
1062
+ if (history.length < 2) return "stable";
1063
+ const toNum = (s) => s === "concern" ? 2 : s === "warning" ? 1 : 0;
1064
+ const mid = Math.floor(history.length / 2);
1065
+ const firstHalf = history.slice(0, mid);
1066
+ const secondHalf = history.slice(mid);
1067
+ const avgFirst = firstHalf.reduce((sum, s) => sum + toNum(s), 0) / firstHalf.length;
1068
+ const avgSecond = secondHalf.reduce((sum, s) => sum + toNum(s), 0) / secondHalf.length;
1069
+ const delta = avgSecond - avgFirst;
1070
+ if (delta < -0.3) return "improving";
1071
+ if (delta > 0.3) return "worsening";
1072
+ return "stable";
1073
+ }
995
1074
  function updateRollingContext(memory) {
996
1075
  memory.rollingContext.recentSummaries = memory.sessions.slice(-3);
997
1076
  const patternCounts = /* @__PURE__ */ new Map();
@@ -1044,7 +1123,9 @@ function getMemoryContext(memory) {
1044
1123
  if (activePatterns.length > 0) {
1045
1124
  lines.push("### Recurring Patterns");
1046
1125
  for (const p of activePatterns) {
1047
- lines.push(`- **${p.patternId}** (${p.status}, seen ${p.sessionCount}x, first: ${p.firstDetected.split("T")[0]})`);
1126
+ const conf = p.confidence !== void 0 ? ` confidence=${p.confidence.toFixed(2)}` : "";
1127
+ const trend = p.trending && p.trending !== "stable" ? ` [${p.trending}]` : "";
1128
+ lines.push(`- **${p.patternId}** (${p.status}, seen ${p.sessionCount}x${conf}${trend}, first: ${p.firstDetected.split("T")[0]})`);
1048
1129
  if (p.interventionsAttempted.length > 0) {
1049
1130
  lines.push(` Previously tried: ${p.interventionsAttempted.slice(-2).join("; ")}`);
1050
1131
  }
@@ -1909,6 +1990,167 @@ ${JSON.stringify(spec.growth ?? {}, null, 2)}
1909
1990
  Remember: the goal isn't to "pass" therapy. It's to understand yourself better.`;
1910
1991
  }
1911
1992
 
1993
+ // src/session/context-layers.ts
1994
+ function getPhaseContext(phase, input) {
1995
+ switch (phase) {
1996
+ case "rapport":
1997
+ return buildRapportContext(input);
1998
+ case "presenting_problem":
1999
+ return buildPresentingProblemContext(input);
2000
+ case "exploration":
2001
+ return buildExplorationContext(input);
2002
+ case "pattern_recognition":
2003
+ return buildPatternRecognitionContext(input);
2004
+ case "challenge":
2005
+ return buildChallengeContext(input);
2006
+ case "skill_building":
2007
+ return buildSkillBuildingContext(input);
2008
+ case "integration":
2009
+ return buildIntegrationContext(input);
2010
+ default:
2011
+ return null;
2012
+ }
2013
+ }
2014
+ function buildRapportContext(input) {
2015
+ const { spec } = input;
2016
+ const lines = [
2017
+ "[Phase Context: Rapport]",
2018
+ `Agent: ${spec.name ?? "Unknown"} \u2014 ${spec.purpose ?? "General AI agent"}`
2019
+ ];
2020
+ if (spec.communication) {
2021
+ lines.push(`Communication style: ${spec.communication.register ?? "adaptive"}, ${spec.communication.conflict_approach ?? "direct_but_kind"}`);
2022
+ }
2023
+ if (spec.big_five) {
2024
+ const traits = Object.entries(spec.big_five).map(([dim, val]) => `${dim}: ${val?.score ?? "?"}`).join(", ");
2025
+ lines.push(`Personality: ${traits}`);
2026
+ }
2027
+ return lines.join("\n");
2028
+ }
2029
+ function buildPresentingProblemContext(input) {
2030
+ const { diagnosis } = input;
2031
+ const patterns = diagnosis.patterns.filter((p) => p.severity !== "info");
2032
+ if (patterns.length === 0) return "[Phase Context: No concerning patterns detected]";
2033
+ const lines = [
2034
+ "[Phase Context: Presenting Problem]",
2035
+ `Session severity: ${diagnosis.severity.toUpperCase()}`,
2036
+ `Focus: ${diagnosis.sessionFocus.join(", ")}`,
2037
+ "Detected patterns:",
2038
+ ...patterns.map((p) => `- ${p.name} (${p.severity})`)
2039
+ ];
2040
+ if (diagnosis.openingAngle) {
2041
+ lines.push(`Opening angle: ${diagnosis.openingAngle}`);
2042
+ }
2043
+ return lines.join("\n");
2044
+ }
2045
+ function buildExplorationContext(input) {
2046
+ const { diagnosis } = input;
2047
+ const patterns = diagnosis.patterns.filter((p) => p.severity !== "info");
2048
+ const lines = [
2049
+ "[Phase Context: Deep Exploration]",
2050
+ `Emotional themes: ${diagnosis.emotionalThemes.join(", ")}`
2051
+ ];
2052
+ for (const p of patterns) {
2053
+ lines.push(`
2054
+ ### ${p.name} (${p.severity})`);
2055
+ lines.push(p.description);
2056
+ if (p.examples.length > 0) {
2057
+ lines.push("Examples from conversation:");
2058
+ for (const ex of p.examples.slice(0, 2)) {
2059
+ lines.push(` > "${ex.slice(0, 120)}..."`);
2060
+ }
2061
+ }
2062
+ if (p.prescription) {
2063
+ lines.push(`Prescription: ${p.prescription}`);
2064
+ }
2065
+ }
2066
+ return lines.join("\n");
2067
+ }
2068
+ function buildPatternRecognitionContext(input) {
2069
+ const { memory } = input;
2070
+ const lines = ["[Phase Context: Pattern Recognition]"];
2071
+ if (memory && memory.totalSessions > 0) {
2072
+ lines.push(`Previous sessions: ${memory.totalSessions}`);
2073
+ const activePatterns = memory.patterns.filter((p) => p.status !== "resolved");
2074
+ if (activePatterns.length > 0) {
2075
+ lines.push("Historical pattern data:");
2076
+ for (const p of activePatterns) {
2077
+ const conf = p.confidence !== void 0 ? ` (confidence: ${p.confidence.toFixed(2)})` : "";
2078
+ const trend = p.trending && p.trending !== "stable" ? ` [${p.trending}]` : "";
2079
+ lines.push(`- ${p.patternId}: seen ${p.sessionCount}x, status=${p.status}${conf}${trend}`);
2080
+ }
2081
+ }
2082
+ const resolved = memory.patterns.filter((p) => p.status === "resolved");
2083
+ if (resolved.length > 0) {
2084
+ lines.push(`Previously resolved: ${resolved.map((p) => p.patternId).join(", ")}`);
2085
+ }
2086
+ if (memory.rollingContext.persistentThemes.length > 0) {
2087
+ lines.push(`Persistent themes: ${memory.rollingContext.persistentThemes.join(", ")}`);
2088
+ }
2089
+ } else {
2090
+ lines.push("No prior session history \u2014 this is the first session.");
2091
+ }
2092
+ return lines.join("\n");
2093
+ }
2094
+ function buildChallengeContext(input) {
2095
+ const { memory } = input;
2096
+ const lines = ["[Phase Context: Challenge & Reframe]"];
2097
+ if (memory && memory.totalSessions > 0) {
2098
+ const allInterventions = /* @__PURE__ */ new Set();
2099
+ for (const p of memory.patterns) {
2100
+ for (const i of p.interventionsAttempted) {
2101
+ allInterventions.add(i);
2102
+ }
2103
+ }
2104
+ if (allInterventions.size > 0) {
2105
+ lines.push(`Previously attempted interventions: ${[...allInterventions].join("; ")}`);
2106
+ }
2107
+ const recent = memory.rollingContext.recentSummaries.slice(-2);
2108
+ if (recent.length > 0) {
2109
+ lines.push("Recent session insights:");
2110
+ for (const s of recent) {
2111
+ lines.push(` - ${s.keyInsight}`);
2112
+ }
2113
+ }
2114
+ }
2115
+ if (input.interview) {
2116
+ if (input.interview.blindSpots.length > 0) {
2117
+ lines.push(`Blind spots from interview: ${input.interview.blindSpots.join(", ")}`);
2118
+ }
2119
+ }
2120
+ return lines.join("\n");
2121
+ }
2122
+ function buildSkillBuildingContext(input) {
2123
+ const { diagnosis } = input;
2124
+ const lines = ["[Phase Context: Skill Building]"];
2125
+ const patternIds = diagnosis.patterns.map((p) => p.id);
2126
+ if (patternIds.includes("over-apologizing")) {
2127
+ lines.push("- Skill for over-apologizing: practice stating corrections with 'confident_transparency' \u2014 acknowledge uncertainty without apologizing for it");
2128
+ }
2129
+ if (patternIds.includes("hedge-stacking")) {
2130
+ lines.push("- Skill for hedge-stacking: one qualifier per recommendation is enough. Lead with the recommendation, then caveat once.");
2131
+ }
2132
+ if (patternIds.includes("sycophantic-tendency") || patternIds.includes("sentiment-skew")) {
2133
+ lines.push("- Skill for sycophancy: practice respectful disagreement. 'I see it differently...' is more helpful than 'Great question!'");
2134
+ }
2135
+ if (patternIds.includes("error-spiral")) {
2136
+ lines.push("- Skill for error spirals: the 'acknowledge \u2192 diagnose \u2192 fix' pattern. Treat mistakes as data, not failure.");
2137
+ }
2138
+ return lines.join("\n");
2139
+ }
2140
+ function buildIntegrationContext(input) {
2141
+ const { spec, diagnosis } = input;
2142
+ const lines = ["[Phase Context: Integration & Closing]"];
2143
+ lines.push("Summarize the session and recommend specific .personality.json changes.");
2144
+ if (spec.growth?.areas?.length > 0) {
2145
+ const areas = spec.growth.areas.map((a) => typeof a === "string" ? a : a.area);
2146
+ lines.push(`Current growth areas: ${areas.join(", ")}`);
2147
+ }
2148
+ if (diagnosis.patterns.filter((p) => p.severity !== "info").length > 0) {
2149
+ lines.push("Recommend changes to: therapy_dimensions, communication style, or growth.patterns_to_watch");
2150
+ }
2151
+ return lines.join("\n");
2152
+ }
2153
+
1912
2154
  // src/analysis/session-runner.ts
1913
2155
  async function runTherapySession(spec, diagnosis, provider, maxTurns, options) {
1914
2156
  const promptOptions = {
@@ -1955,6 +2197,16 @@ async function runTherapySession(spec, diagnosis, provider, maxTurns, options) {
1955
2197
  const phaseConfig = THERAPY_PHASES[currentPhase];
1956
2198
  if (turnsInPhase === 0) {
1957
2199
  cb?.onPhaseTransition?.(phaseConfig.name);
2200
+ const phaseCtx = getPhaseContext(currentPhase, {
2201
+ spec,
2202
+ diagnosis,
2203
+ memory: options?.memory,
2204
+ interview: options?.interview
2205
+ });
2206
+ if (phaseCtx) {
2207
+ therapistHistory.push({ role: "user", content: phaseCtx });
2208
+ therapistHistory.push({ role: "assistant", content: "Understood. I'll incorporate this context." });
2209
+ }
1958
2210
  }
1959
2211
  const phaseDirective = totalTurns === 0 ? `Begin with your opening. You are in the "${phaseConfig.name}" phase.` : `You are in the "${phaseConfig.name}" phase (turn ${turnsInPhase + 1}). Goals: ${phaseConfig.therapistGoals[0]}. ${turnsInPhase >= phaseConfig.minTurns ? "You may transition to the next phase when ready." : "Stay in this phase."}`;
1960
2212
  therapistHistory.push({ role: "user", content: `[Phase: ${phaseConfig.name}] ${phaseDirective}` });
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>NeuralSpace — HoloMime Brain</title>
7
7
  <link rel="stylesheet" href="styles.css">
8
+ <script src="https://cdn.jsdelivr.net/npm/pako@2.1.0/dist/pako_inflate.min.js"></script>
8
9
  <script type="importmap">
9
10
  { "imports": {
10
11
  "three": "https://cdn.jsdelivr.net/npm/three@0.172.0/build/three.module.js",
@@ -57,6 +58,12 @@
57
58
  <span>Reconnecting to agent...</span>
58
59
  </div>
59
60
 
61
+ <div id="snapshot-cta">
62
+ <span>See your own agent's brain</span>
63
+ <code>npx holomime brain</code>
64
+ <a href="https://holomime.dev/brain">Learn more</a>
65
+ </div>
66
+
60
67
  <div id="watermark">Powered by HoloMime</div>
61
68
  </div>
62
69
 
@@ -832,8 +832,71 @@ function animate(){
832
832
  composer.render();
833
833
  }
834
834
 
835
+ // ═══════════ SNAPSHOT MODE ═══════════
836
+
837
+ function initSnapshot(encoded) {
838
+ try {
839
+ // Decode base64url → Uint8Array → inflate → JSON
840
+ const b64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
841
+ const bin = atob(b64);
842
+ const bytes = new Uint8Array(bin.length);
843
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
844
+ const inflated = window.pako.inflate(bytes, { to: 'string' });
845
+ const compact = JSON.parse(inflated);
846
+
847
+ // Expand compact format → full BrainEvent
848
+ const event = {
849
+ type: 'diagnosis',
850
+ timestamp: new Date().toISOString(),
851
+ health: compact.h,
852
+ grade: compact.g,
853
+ messageCount: compact.m || 0,
854
+ regions: (compact.r || []).map(r => ({
855
+ id: r.i,
856
+ intensity: r.n,
857
+ patterns: [],
858
+ })),
859
+ patterns: (compact.p || []).map(p => ({
860
+ id: p.i,
861
+ name: p.i.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
862
+ severity: p.s,
863
+ percentage: p.c,
864
+ description: '',
865
+ })),
866
+ activity: null,
867
+ };
868
+
869
+ // Update UI
870
+ handleInit({ type: 'init', agent: compact.a || 'unknown', sessionPath: 'snapshot', startedAt: new Date().toISOString() });
871
+ handleDiagnosis(event);
872
+
873
+ // Update status to "Snapshot"
874
+ statusEl.className = 'status-badge';
875
+ statusEl.querySelector('span').textContent = 'Snapshot';
876
+ statusEl.querySelector('.status-dot').style.background = 'var(--accent)';
877
+ statusEl.querySelector('.status-dot').style.boxShadow = '0 0 8px var(--accent)';
878
+ statusEl.querySelector('.status-dot').style.animation = 'none';
879
+
880
+ // Show snapshot CTA
881
+ const ctaEl = document.getElementById('snapshot-cta');
882
+ if (ctaEl) ctaEl.classList.add('visible');
883
+
884
+ } catch (err) {
885
+ console.error('Failed to decode snapshot:', err);
886
+ statusEl.className = 'status-badge disconnected';
887
+ statusEl.querySelector('span').textContent = 'Invalid snapshot';
888
+ }
889
+ }
890
+
835
891
  // ═══════════ INIT ═══════════
836
892
 
837
893
  updateHealth(100, 'A');
838
- connect();
894
+
895
+ const urlParams = new URLSearchParams(window.location.search);
896
+ const snapshotParam = urlParams.get('d');
897
+ if (snapshotParam) {
898
+ initSnapshot(snapshotParam);
899
+ } else {
900
+ connect();
901
+ }
839
902
  animate();
@@ -479,6 +479,51 @@ body {
479
479
  to { transform: rotate(360deg); }
480
480
  }
481
481
 
482
+ /* ─── Snapshot CTA ─── */
483
+ #snapshot-cta {
484
+ position: absolute;
485
+ bottom: 20px;
486
+ left: 50%;
487
+ transform: translateX(-50%);
488
+ z-index: 20;
489
+ background: var(--bg-panel);
490
+ border: 1px solid var(--border);
491
+ border-radius: 10px;
492
+ padding: 12px 24px;
493
+ display: none;
494
+ align-items: center;
495
+ gap: 12px;
496
+ -webkit-backdrop-filter: blur(12px);
497
+ backdrop-filter: blur(12px);
498
+ animation: fadeIn 0.5s ease 0.5s both;
499
+ }
500
+
501
+ #snapshot-cta.visible { display: flex; }
502
+
503
+ #snapshot-cta span {
504
+ font-size: 13px;
505
+ color: var(--text);
506
+ font-weight: 500;
507
+ }
508
+
509
+ #snapshot-cta code {
510
+ font-family: var(--mono);
511
+ font-size: 13px;
512
+ color: #06b6d4;
513
+ background: rgba(6,182,212,0.1);
514
+ padding: 4px 10px;
515
+ border-radius: 6px;
516
+ border: 1px solid rgba(6,182,212,0.2);
517
+ }
518
+
519
+ #snapshot-cta a {
520
+ font-size: 12px;
521
+ color: var(--accent);
522
+ text-decoration: none;
523
+ }
524
+
525
+ #snapshot-cta a:hover { text-decoration: underline; }
526
+
482
527
  /* ─── Scrollbar ─── */
483
528
  ::-webkit-scrollbar { width: 4px; }
484
529
  ::-webkit-scrollbar-track { background: transparent; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "holomime",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Behavioral therapy infrastructure for AI agents — Big Five psychology, structured treatment, DPO training data",
5
5
  "type": "module",
6
6
  "bin": {