mover-os 4.7.7 → 4.7.8

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 (107) hide show
  1. package/install.js +688 -68
  2. package/package.json +15 -3
  3. package/src/dashboard/build.js +1503 -0
  4. package/src/dashboard/dashboard.js +276 -0
  5. package/src/dashboard/index.js +319 -0
  6. package/src/dashboard/lib/activation-log.js +297 -0
  7. package/src/dashboard/lib/active-context-parser.js +177 -0
  8. package/src/dashboard/lib/agent-command.js +93 -0
  9. package/src/dashboard/lib/agent-detect.js +255 -0
  10. package/src/dashboard/lib/agent-detector.js +92 -0
  11. package/src/dashboard/lib/agent-session.js +462 -0
  12. package/src/dashboard/lib/anti-identity-detector.js +116 -0
  13. package/src/dashboard/lib/auto-learnings-parser.js +183 -0
  14. package/src/dashboard/lib/cli-usage-parser.js +92 -0
  15. package/src/dashboard/lib/config-parser.js +109 -0
  16. package/src/dashboard/lib/connect-recommender.js +131 -0
  17. package/src/dashboard/lib/correlations-parser.js +231 -0
  18. package/src/dashboard/lib/daily-note-resolver.js +211 -0
  19. package/src/dashboard/lib/date-utils.js +35 -0
  20. package/src/dashboard/lib/distribution-parser.js +78 -0
  21. package/src/dashboard/lib/dossier-parser.js +64 -0
  22. package/src/dashboard/lib/drift-history.js +83 -0
  23. package/src/dashboard/lib/drift-score.js +119 -0
  24. package/src/dashboard/lib/engine-health.js +170 -0
  25. package/src/dashboard/lib/engine-writer.js +1685 -0
  26. package/src/dashboard/lib/execution-plan.js +125 -0
  27. package/src/dashboard/lib/experiments-parser.js +429 -0
  28. package/src/dashboard/lib/feed-parser.js +294 -0
  29. package/src/dashboard/lib/forked-future.js +60 -0
  30. package/src/dashboard/lib/goal-forecast.js +304 -0
  31. package/src/dashboard/lib/goals-parser.js +67 -0
  32. package/src/dashboard/lib/health-protocols-parser.js +133 -0
  33. package/src/dashboard/lib/hook-activity.js +48 -0
  34. package/src/dashboard/lib/hook-indexer.js +169 -0
  35. package/src/dashboard/lib/hourly-activity-parser.js +143 -0
  36. package/src/dashboard/lib/identity-parser.js +85 -0
  37. package/src/dashboard/lib/ingestion.js +418 -0
  38. package/src/dashboard/lib/library-indexer-v2.js +212 -0
  39. package/src/dashboard/lib/library-indexer.js +105 -0
  40. package/src/dashboard/lib/log-activation.sh +61 -0
  41. package/src/dashboard/lib/memory-curator.js +97 -0
  42. package/src/dashboard/lib/memory-gardener.js +177 -0
  43. package/src/dashboard/lib/memory-gepa.js +102 -0
  44. package/src/dashboard/lib/memory-index.js +470 -0
  45. package/src/dashboard/lib/memory-rerank.js +72 -0
  46. package/src/dashboard/lib/memory-text.js +136 -0
  47. package/src/dashboard/lib/metrics-log-parser.js +76 -0
  48. package/src/dashboard/lib/moves-usage-parser.js +184 -0
  49. package/src/dashboard/lib/onboarding-forge.js +70 -0
  50. package/src/dashboard/lib/override-outcome-parser.js +68 -0
  51. package/src/dashboard/lib/override-summary.js +73 -0
  52. package/src/dashboard/lib/paths.js +192 -0
  53. package/src/dashboard/lib/pattern-manifest-loader.js +29 -0
  54. package/src/dashboard/lib/phantom-strategy.js +129 -0
  55. package/src/dashboard/lib/project-scanner.js +121 -0
  56. package/src/dashboard/lib/promise-wall.js +88 -0
  57. package/src/dashboard/lib/record-score.js +173 -0
  58. package/src/dashboard/lib/redaction.js +140 -0
  59. package/src/dashboard/lib/refusal-parser.js +44 -0
  60. package/src/dashboard/lib/regenerate-manifest.js +206 -0
  61. package/src/dashboard/lib/rewind-snapshots.js +689 -0
  62. package/src/dashboard/lib/roast-wall-parser.js +200 -0
  63. package/src/dashboard/lib/run-registry.js +226 -0
  64. package/src/dashboard/lib/safe-write.js +63 -0
  65. package/src/dashboard/lib/session-log-parser.js +145 -0
  66. package/src/dashboard/lib/session-time-parser.js +158 -0
  67. package/src/dashboard/lib/skill-index.js +171 -0
  68. package/src/dashboard/lib/skill-indexer.js +118 -0
  69. package/src/dashboard/lib/skill-recommender.js +689 -0
  70. package/src/dashboard/lib/strategy-parser.js +245 -0
  71. package/src/dashboard/lib/strategy-protocol-parser.js +135 -0
  72. package/src/dashboard/lib/streak-parser.js +95 -0
  73. package/src/dashboard/lib/suggested-now.js +254 -0
  74. package/src/dashboard/lib/tool-awareness.js +125 -0
  75. package/src/dashboard/lib/transcript-parser.js +331 -0
  76. package/src/dashboard/lib/vault-graph-parser.js +205 -0
  77. package/src/dashboard/lib/view-generator.js +163 -0
  78. package/src/dashboard/lib/voice-dna-parser.js +57 -0
  79. package/src/dashboard/lib/walkthrough-script.js +140 -0
  80. package/src/dashboard/lib/workflow-graph-parser.js +170 -0
  81. package/src/dashboard/lib/workflow-library-parser.js +146 -0
  82. package/src/dashboard/server.js +2024 -0
  83. package/src/dashboard/shortcut.js +0 -0
  84. package/src/dashboard/static/setup-poc.html +306 -0
  85. package/src/dashboard/static/walkthrough-poc.html +580 -0
  86. package/src/dashboard/styles.css +1201 -0
  87. package/src/dashboard/templates/index.html +278 -0
  88. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
  89. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
  90. package/src/dashboard/ui/dist/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
  91. package/src/dashboard/ui/dist/assets/hero.png +0 -0
  92. package/src/dashboard/ui/dist/assets/index-598CSGOZ.js +157 -0
  93. package/src/dashboard/ui/dist/assets/index-BP--M69H.css +1 -0
  94. package/src/dashboard/ui/dist/assets/index-CidzmwSW.js +34 -0
  95. package/src/dashboard/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  96. package/src/dashboard/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  97. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  98. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  99. package/src/dashboard/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  100. package/src/dashboard/ui/dist/assets/mover-system.css +62 -0
  101. package/src/dashboard/ui/dist/assets/xterm-CqkleIqs.js +1 -0
  102. package/src/dashboard/ui/dist/icon.svg +4 -0
  103. package/src/dashboard/ui/dist/index.html +18 -0
  104. package/src/dashboard/ui/dist/manifest.webmanifest +1 -0
  105. package/src/dashboard/ui/dist/registerSW.js +1 -0
  106. package/src/dashboard/ui/dist/sw.js +1 -0
  107. package/src/dashboard/ui/dist/workbox-39fa566e.js +1 -0
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+
3
+ const { goalsPath, safeRead, safeStat } = require("./paths");
4
+
5
+ // Parse Goals.md. Extracts 3-month, 1-year, 10-year goals + expiration dates.
6
+
7
+ function parseGoals(vault) {
8
+ const filePath = goalsPath(vault);
9
+ const text = safeRead(filePath);
10
+ const stat = safeStat(filePath);
11
+ if (!text) return { available: false, filePath };
12
+
13
+ const out = {
14
+ available: true,
15
+ filePath,
16
+ lastUpdated: stat ? stat.mtime : null,
17
+ raw: text,
18
+ horizons: []
19
+ };
20
+
21
+ // Split on ## headers
22
+ const lines = text.split("\n");
23
+ let current = null;
24
+ let buf = [];
25
+ for (const line of lines) {
26
+ const h = line.match(/^##\s+(.+?)\s*$/);
27
+ if (h) {
28
+ if (current) {
29
+ out.horizons.push({ title: current, body: buf.join("\n"), ...analyzeHorizon(buf.join("\n")) });
30
+ }
31
+ current = h[1];
32
+ buf = [];
33
+ } else if (current) {
34
+ buf.push(line);
35
+ }
36
+ }
37
+ if (current) {
38
+ out.horizons.push({ title: current, body: buf.join("\n"), ...analyzeHorizon(buf.join("\n")) });
39
+ }
40
+
41
+ // Identify 3-month, 1-year, 10-year via title heuristic
42
+ const now = Date.now();
43
+ out.expired = [];
44
+ for (const h of out.horizons) {
45
+ if (h.targetDate) {
46
+ const dt = new Date(h.targetDate);
47
+ if (!isNaN(dt.getTime()) && dt.getTime() < now) {
48
+ out.expired.push({ title: h.title, targetDate: h.targetDate, expiredDays: Math.floor((now - dt.getTime()) / 86400000) });
49
+ }
50
+ }
51
+ }
52
+
53
+ return out;
54
+ }
55
+
56
+ function analyzeHorizon(body) {
57
+ const dateMatch = body.match(/\*\*(?:Target Date|Date|By):?\*\*\s*([\d\-/]{8,10})/i);
58
+ const goalsList = body.split("\n")
59
+ .map(l => l.match(/^\s*[-*\d.]+\s+(.+)$/))
60
+ .filter(Boolean).map(m => m[1].trim());
61
+ return {
62
+ targetDate: dateMatch ? dateMatch[1] : null,
63
+ goals: goalsList.slice(0, 12)
64
+ };
65
+ }
66
+
67
+ module.exports = { parseGoals };
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+ const fs = require("fs");
5
+ const { safeListDir, safeRead, safeStat } = require("./paths");
6
+
7
+ // Parse 02_Areas/Health/*.md as actionable recovery/maintenance protocols.
8
+ // A "protocol" file looks like:
9
+ // # <Title>
10
+ // ## PHASE 1 — <name> (Days 1-7: <date range>)
11
+ // ### Every day (10 min total)
12
+ // - [ ] action one
13
+ // - [ ] action two
14
+ // ## PHASE 2 — ...
15
+ //
16
+ // Output for the dashboard:
17
+ // - title
18
+ // - currentPhase (detected by date range vs today, or first uncompleted)
19
+ // - today's checklist (unchecked items in current phase)
20
+ // - completedToday (checked items in current phase — for progress)
21
+ // - daysIntoPhase / daysRemaining (if dates present)
22
+ //
23
+ // Forgiving: never throws. Empty/missing → { available: false }.
24
+
25
+ function healthDir(vault) {
26
+ return path.join(vault, "02_Areas", "Health");
27
+ }
28
+
29
+ function parseDateRange(line) {
30
+ // Match "(May 12-19)" or "(Days 1-7: May 12-19)" or "(May 20 → Jun 30)"
31
+ // Returns { startISO, endISO } or null
32
+ const m = line.match(/\((?:[^:)]*?:)?\s*([A-Z][a-z]{2})\s+(\d{1,2})(?:\s*[-–→]\s*(?:([A-Z][a-z]{2})\s+)?(\d{1,2}))?\)/);
33
+ if (!m) return null;
34
+ const monthMap = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };
35
+ const year = new Date().getFullYear();
36
+ const sm = monthMap[m[1]];
37
+ const sd = parseInt(m[2], 10);
38
+ const em = m[3] ? monthMap[m[3]] : sm;
39
+ const ed = m[4] ? parseInt(m[4], 10) : sd;
40
+ if (sm === undefined || em === undefined) return null;
41
+ return {
42
+ start: new Date(year, sm, sd),
43
+ end: new Date(year, em, ed)
44
+ };
45
+ }
46
+
47
+ function parseProtocolFile(filePath) {
48
+ const text = safeRead(filePath);
49
+ if (!text) return null;
50
+ const stat = safeStat(filePath);
51
+
52
+ // Title from first H1, fallback to filename
53
+ const titleMatch = text.match(/^#\s+(.+)$/m);
54
+ const title = titleMatch ? titleMatch[1].trim() : path.basename(filePath, ".md");
55
+
56
+ // Find all PHASE blocks. Phases are H2 headings that contain "PHASE" or similar.
57
+ const lines = text.split("\n");
58
+ const phases = [];
59
+ let currentPhase = null;
60
+
61
+ for (const raw of lines) {
62
+ const line = raw.trimEnd();
63
+ const phaseMatch = line.match(/^##\s+(.*PHASE.*)$/i);
64
+ if (phaseMatch) {
65
+ if (currentPhase) phases.push(currentPhase);
66
+ currentPhase = {
67
+ heading: phaseMatch[1].trim(),
68
+ dates: parseDateRange(phaseMatch[1]),
69
+ items: [],
70
+ done: 0,
71
+ total: 0
72
+ };
73
+ continue;
74
+ }
75
+ if (!currentPhase) continue;
76
+ // Skip subheadings — items only
77
+ if (line.startsWith("##")) continue;
78
+ // Checkbox lines
79
+ const cb = line.match(/^[-*]\s+\[([ xX])\]\s+(.+)$/);
80
+ if (cb) {
81
+ const checked = cb[1].toLowerCase() === "x";
82
+ currentPhase.items.push({ text: cb[2].trim(), checked });
83
+ currentPhase.total++;
84
+ if (checked) currentPhase.done++;
85
+ }
86
+ }
87
+ if (currentPhase) phases.push(currentPhase);
88
+
89
+ // Determine the current phase: one whose date range contains today,
90
+ // OR the first phase with any unchecked items if no date hits.
91
+ const today = new Date();
92
+ today.setHours(0, 0, 0, 0);
93
+ let current = phases.find(p => p.dates && p.dates.start <= today && today <= p.dates.end);
94
+ if (!current) current = phases.find(p => p.done < p.total);
95
+ if (!current && phases.length) current = phases[phases.length - 1];
96
+
97
+ // Days remaining in current phase
98
+ let daysRemaining = null;
99
+ let daysIntoPhase = null;
100
+ if (current && current.dates) {
101
+ daysRemaining = Math.max(0, Math.ceil((current.dates.end - today) / (1000 * 60 * 60 * 24)));
102
+ daysIntoPhase = Math.max(0, Math.ceil((today - current.dates.start) / (1000 * 60 * 60 * 24)));
103
+ }
104
+
105
+ return {
106
+ available: true,
107
+ title,
108
+ filePath,
109
+ lastUpdated: stat ? stat.mtime : null,
110
+ phases,
111
+ current,
112
+ daysRemaining,
113
+ daysIntoPhase
114
+ };
115
+ }
116
+
117
+ function parseHealthProtocols(vault) {
118
+ const dir = healthDir(vault);
119
+ const entries = safeListDir(dir, { filesOnly: true, extension: ".md", skipUnderscore: true });
120
+ const protocols = [];
121
+ for (const entry of entries) {
122
+ const full = path.join(dir, entry.name);
123
+ const p = parseProtocolFile(full);
124
+ if (p && p.available) protocols.push(p);
125
+ }
126
+ return {
127
+ available: protocols.length > 0,
128
+ dir,
129
+ protocols
130
+ };
131
+ }
132
+
133
+ module.exports = { parseHealthProtocols };
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+ const os = require("os");
5
+ const fs = require("fs");
6
+ const { exists, safeListDir, safeStat } = require("./paths");
7
+
8
+ // Read recent hook activity from ~/.claude/tmp/mover_* markers + session-history if available.
9
+
10
+ function readHookActivity() {
11
+ const home = os.homedir();
12
+ const claudeTmp = path.join(home, ".claude", "tmp");
13
+ const codexTmp = path.join(home, ".codex", "tmp");
14
+ const geminiTmp = path.join(home, ".gemini", "tmp");
15
+
16
+ const out = { available: true, recentMarkers: [], hookStats: { sessionStart: 0, log: 0, engineHealth: 0, planSync: 0 }, lastFire: null };
17
+
18
+ for (const dir of [claudeTmp, codexTmp, geminiTmp]) {
19
+ if (!exists(dir)) continue;
20
+ const entries = safeListDir(dir, { filesOnly: true });
21
+ for (const e of entries) {
22
+ if (!e.name.startsWith("mover_")) continue;
23
+ const stat = safeStat(e.path);
24
+ if (!stat) continue;
25
+ const ageDays = (Date.now() - stat.mtime.getTime()) / 86400000;
26
+ if (ageDays > 7) continue;
27
+ out.recentMarkers.push({
28
+ name: e.name,
29
+ agent: dir.includes(".claude") ? "Claude" : dir.includes(".codex") ? "Codex" : "Gemini",
30
+ mtime: stat.mtime,
31
+ ageDays: Math.floor(ageDays * 24 * 60) / (24 * 60),
32
+ ageHours: Math.floor(ageDays * 24 * 10) / 10
33
+ });
34
+ // Categorize
35
+ if (e.name.includes("logprompt") || e.name.includes("logdone")) out.hookStats.log++;
36
+ else if (e.name.includes("session-start") || e.name.includes("sessionstart")) out.hookStats.sessionStart++;
37
+ else if (e.name.includes("engine") || e.name.includes("health")) out.hookStats.engineHealth++;
38
+ else if (e.name.includes("plan-sync")) out.hookStats.planSync++;
39
+ if (!out.lastFire || stat.mtime > out.lastFire) out.lastFire = stat.mtime;
40
+ }
41
+ }
42
+ out.recentMarkers.sort((a, b) => b.mtime - a.mtime);
43
+ out.recentMarkers = out.recentMarkers.slice(0, 10);
44
+
45
+ return out;
46
+ }
47
+
48
+ module.exports = { readHookActivity };
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+ const { safeListDir, safeRead, safeStat, safeReadJson, exists } = require("./paths");
7
+
8
+ // Index Mover OS hook scripts and their registration state.
9
+ //
10
+ // Scripts live in <bundle>/src/hooks/. We try to locate the bundle:
11
+ // 1. Walking up from this file (lib/ → dashboard/ → src/ → bundle root)
12
+ // 2. Default vault layout <vault>/01_Projects/Mover OS Bundle/src/hooks/
13
+ //
14
+ // Registration state is read from ~/.claude/settings.json hooks block.
15
+ //
16
+ // Last-fired timestamps come from ~/.claude/tmp/mover_<hook>_<id>.* markers.
17
+
18
+ const HOOK_EVENTS = [
19
+ "SessionStart",
20
+ "Stop",
21
+ "PreToolUse",
22
+ "PostToolUse",
23
+ "PreCompact",
24
+ "UserPromptSubmit",
25
+ "SubagentStop",
26
+ ];
27
+
28
+ function bundleHooksDir() {
29
+ // From src/dashboard/lib/hook-indexer.js, walk up to bundle root then into src/hooks
30
+ // lib/hook-indexer.js → lib → dashboard → src → bundle root
31
+ const candidate = path.resolve(__dirname, "..", "..", "hooks");
32
+ if (exists(candidate)) return candidate;
33
+ // Fallback: ~/.claude/hooks (if installed there)
34
+ const alt = path.join(os.homedir(), ".claude", "hooks");
35
+ if (exists(alt)) return alt;
36
+ return null;
37
+ }
38
+
39
+ function claudeSettingsPath() {
40
+ return path.join(os.homedir(), ".claude", "settings.json");
41
+ }
42
+
43
+ function claudeTmpDir() {
44
+ return path.join(os.homedir(), ".claude", "tmp");
45
+ }
46
+
47
+ // Extract a one-line description from the first commented lines of a script.
48
+ function extractDescription(content) {
49
+ if (!content) return "";
50
+ const lines = content.split(/\r?\n/);
51
+ // For .sh: lines start with #
52
+ // For .js: lines start with // or /* */
53
+ for (let i = 0; i < Math.min(20, lines.length); i++) {
54
+ const line = lines[i].trim();
55
+ if (!line) continue;
56
+ if (line.startsWith("#!")) continue;
57
+ if (line.startsWith("'use strict")) continue;
58
+ // Shell comment
59
+ const sh = line.match(/^#\s*(.+)$/);
60
+ if (sh && sh[1].length > 5) return sh[1].trim().slice(0, 140);
61
+ // JS line comment
62
+ const js = line.match(/^\/\/\s*(.+)$/);
63
+ if (js && js[1].length > 5) return js[1].trim().slice(0, 140);
64
+ // JS block comment start
65
+ const block = line.match(/^\/\*\s*(.+?)\s*\*?\/?$/);
66
+ if (block && block[1].length > 5) return block[1].trim().slice(0, 140);
67
+ }
68
+ return "";
69
+ }
70
+
71
+ function readRegisteredHooks() {
72
+ const settings = safeReadJson(claudeSettingsPath());
73
+ if (!settings || !settings.hooks) return { byEvent: {}, allPaths: new Set() };
74
+
75
+ const byEvent = {};
76
+ const allPaths = new Set();
77
+
78
+ for (const event of Object.keys(settings.hooks)) {
79
+ const entries = settings.hooks[event];
80
+ if (!Array.isArray(entries)) continue;
81
+ byEvent[event] = [];
82
+ for (const entry of entries) {
83
+ // shape: { matcher?: string, hooks: [{ type, command }] }
84
+ const hooks = Array.isArray(entry.hooks) ? entry.hooks : [];
85
+ for (const h of hooks) {
86
+ if (typeof h.command === "string") {
87
+ // Extract script filename from command
88
+ const scriptMatch = h.command.match(/([^\s"'\/]+\.(sh|js))/i);
89
+ if (scriptMatch) {
90
+ const filename = scriptMatch[1];
91
+ byEvent[event].push({ filename, command: h.command, matcher: entry.matcher || null });
92
+ allPaths.add(filename);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ return { byEvent, allPaths };
100
+ }
101
+
102
+ function readLastFired() {
103
+ const tmp = claudeTmpDir();
104
+ if (!safeStat(tmp)) return {};
105
+ const markers = safeListDir(tmp, { filesOnly: true });
106
+ const lastFired = {};
107
+ for (const m of markers) {
108
+ if (!m.name.startsWith("mover_")) continue;
109
+ const stat = safeStat(m.path);
110
+ if (!stat) continue;
111
+ // Marker filename heuristic: mover_<hookname>_<id>.<ext>
112
+ const m2 = m.name.match(/^mover_([a-z0-9-]+)_/i);
113
+ if (m2) {
114
+ const hookKey = m2[1];
115
+ const t = stat.mtime.getTime();
116
+ if (!lastFired[hookKey] || lastFired[hookKey] < t) {
117
+ lastFired[hookKey] = t;
118
+ }
119
+ }
120
+ }
121
+ return lastFired;
122
+ }
123
+
124
+ function indexHooks() {
125
+ const hooksDir = bundleHooksDir();
126
+ if (!hooksDir) {
127
+ return { available: false, count: 0, byEvent: {}, registered: [], unregistered: [] };
128
+ }
129
+
130
+ const entries = safeListDir(hooksDir, { filesOnly: true });
131
+ const scripts = entries.filter(e => e.name.endsWith(".sh") || e.name.endsWith(".js"));
132
+
133
+ const { byEvent: regByEvent, allPaths: registeredPaths } = readRegisteredHooks();
134
+ const lastFired = readLastFired();
135
+
136
+ const all = scripts.map(s => {
137
+ const content = safeRead(s.path);
138
+ const description = extractDescription(content);
139
+ const stat = safeStat(s.path);
140
+ const isRegistered = registeredPaths.has(s.name);
141
+ // Derive a "key" matching the marker convention (script stem)
142
+ const stem = s.name.replace(/\.(sh|js)$/, "");
143
+ const lastFiredAt = lastFired[stem] || null;
144
+
145
+ return {
146
+ filename: s.name,
147
+ path: s.path,
148
+ type: s.name.endsWith(".js") ? "js" : "sh",
149
+ description,
150
+ registered: isRegistered,
151
+ lastFiredAt,
152
+ lastModified: stat ? stat.mtime.getTime() : null,
153
+ };
154
+ });
155
+
156
+ all.sort((a, b) => a.filename.localeCompare(b.filename));
157
+
158
+ return {
159
+ available: true,
160
+ count: all.length,
161
+ byEvent: regByEvent,
162
+ registered: all.filter(h => h.registered),
163
+ unregistered: all.filter(h => !h.registered),
164
+ all,
165
+ events: HOOK_EVENTS,
166
+ };
167
+ }
168
+
169
+ module.exports = { indexHooks };
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { dailiesDir } = require("./paths");
6
+ const { localDateKey } = require("./date-utils");
7
+
8
+ // hourly-activity-parser.js — a 24-hour readiness-curve UI pattern applied
9
+ // to behavioral data.
10
+ //
11
+ // The reference pattern plots biometric readiness on a 24-hour curve. We
12
+ // don't have biometrics in the vault — we have BEHAVIORAL ACTIVITY: how
13
+ // often the user actually
14
+ // works (sessions, log entries) at each hour. That density curve answers the
15
+ // same question ("when am I sharpest?") from a different angle: when do you
16
+ // actually get things done historically.
17
+ //
18
+ // Returns: { available, hours: [{ hour: 0..23, count, isPeak }], peakHour,
19
+ // todayHours: [{ hour, count }], events: [{ hour, text, date }] }
20
+ //
21
+ // Sources:
22
+ // - Daily Notes ### HH:MM entries inside ## Session Log sections
23
+ // - Last 14 days (2 full weeks for a stable curve)
24
+
25
+ function compute({ vault, daysLookback = 14 } = {}) {
26
+ if (!vault) return { available: false, hours: [], peakHour: null, todayHours: [], events: [] };
27
+
28
+ const dDir = dailiesDir(vault);
29
+ if (!fs.existsSync(dDir)) return { available: false, hours: [], peakHour: null, todayHours: [], events: [] };
30
+
31
+ // Per-hour aggregated counts across the lookback window
32
+ const hourCounts = new Array(24).fill(0);
33
+ // Today's specific hour counts (for the bright overlay line)
34
+ const todayCounts = new Array(24).fill(0);
35
+ // Notable session-log events to plot as markers
36
+ const events = [];
37
+
38
+ const cutoff = Date.now() - daysLookback * 24 * 3600 * 1000;
39
+ const todayKey = localDateKey();
40
+
41
+ let monthDirs;
42
+ try {
43
+ monthDirs = fs.readdirSync(dDir).filter(d => /^\d{4}-\d{2}$/.test(d)).sort().reverse();
44
+ } catch (_) {
45
+ return { available: false, hours: [], peakHour: null, todayHours: [], events: [] };
46
+ }
47
+
48
+ let scanned = 0;
49
+ outer: for (const mDir of monthDirs) {
50
+ if (scanned >= daysLookback) break;
51
+ const monthPath = path.join(dDir, mDir);
52
+ let files;
53
+ try {
54
+ files = fs.readdirSync(monthPath).filter(f => /^Daily - \d{4}-\d{2}-\d{2}\.md$/.test(f)).sort().reverse();
55
+ } catch (_) { continue; }
56
+ for (const f of files) {
57
+ if (scanned >= daysLookback) break outer;
58
+ const m = f.match(/(\d{4})-(\d{2})-(\d{2})/);
59
+ if (!m) continue;
60
+ const dayKey = `${m[1]}-${m[2]}-${m[3]}`;
61
+ const dayTs = Date.parse(dayKey);
62
+ if (!Number.isFinite(dayTs) || dayTs < cutoff) continue;
63
+ const isToday = dayKey === todayKey;
64
+ scanned++;
65
+
66
+ let text;
67
+ try { text = fs.readFileSync(path.join(monthPath, f), "utf8"); }
68
+ catch (_) { continue; }
69
+
70
+ // Find the Session Log section. Mover OS uses 3 formats:
71
+ // ### Session Log [ATTENDED] — ~02:03-14:37 [May 13] [Claude Code] [Mover OS Bundle] [COMPACTED]
72
+ // ### Session Log [AUTO-STUB] — ~22:21 [Claude Code] [Mover OS Bundle] [hash]
73
+ // ### ~14:32 - Event Title (legacy / nested events)
74
+ // T242 bug hunt fix (B#11): `text.indexOf("## Session Log")` was a
75
+ // substring search — `### Session Log` (level 3, also valid) contains
76
+ // the literal `## Session Log` as a substring. Conversely, level-2
77
+ // `## Session Log` headers with no nested `###` sub-entries were
78
+ // matched by the section locator but the entries regex below
79
+ // returned nothing. Anchor to level-2 explicitly. Fallback: if no
80
+ // level-2 found, try level-3 (some daily notes use only ### form).
81
+ let slMatch = text.match(/^##\s+Session Log/m);
82
+ if (!slMatch) slMatch = text.match(/^###\s+Session Log/m);
83
+ if (!slMatch || typeof slMatch.index !== "number") continue;
84
+ const section = text.slice(slMatch.index);
85
+
86
+ // Format A: Session Log header with time range — count the START hour
87
+ const reSession = /^###\s+Session Log\s+\[[^\]]+\]\s+—\s+~?(\d{1,2}):(\d{2})(?:[-–]\s*(\d{1,2}):(\d{2}))?\s*(?:\[[^\]]+\])?\s*(?:\[([^\]]+)\])?/gm;
88
+ let mm;
89
+ while ((mm = reSession.exec(section)) !== null) {
90
+ const startHour = parseInt(mm[1], 10);
91
+ if (!Number.isFinite(startHour) || startHour < 0 || startHour > 23) continue;
92
+ hourCounts[startHour]++;
93
+ if (isToday) todayCounts[startHour]++;
94
+ const tag = mm[5] ? `[${mm[5]}]` : "";
95
+ const range = mm[3] ? ` ${mm[1]}:${mm[2]}-${mm[3]}:${mm[4]}` : ` ${mm[1]}:${mm[2]}`;
96
+ events.push({ hour: startHour, date: dayKey, text: `Session${range}${tag ? " " + tag : ""}`.trim(), isToday });
97
+ }
98
+
99
+ // Format B: Legacy nested event headers — `### HH:MM - Title`
100
+ const reLegacy = /^###\s+~?(\d{1,2}):(\d{2})\s+-\s+(.+)$/gm;
101
+ while ((mm = reLegacy.exec(section)) !== null) {
102
+ const hour = parseInt(mm[1], 10);
103
+ if (!Number.isFinite(hour) || hour < 0 || hour > 23) continue;
104
+ const title = (mm[3] || "").trim();
105
+ // Skip Session Log headers re-matched (they contain " — ")
106
+ if (title.startsWith("Session Log")) continue;
107
+ hourCounts[hour]++;
108
+ if (isToday) todayCounts[hour]++;
109
+ if (title.length < 80) {
110
+ events.push({ hour, date: dayKey, text: title, isToday });
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ // Compute peak hour
117
+ let peakHour = null;
118
+ let peakCount = 0;
119
+ for (let h = 0; h < 24; h++) {
120
+ if (hourCounts[h] > peakCount) {
121
+ peakCount = hourCounts[h];
122
+ peakHour = h;
123
+ }
124
+ }
125
+
126
+ const hours = hourCounts.map((c, h) => ({ hour: h, count: c, isPeak: h === peakHour && peakCount > 0 }));
127
+ const todayHours = todayCounts.map((c, h) => ({ hour: h, count: c }));
128
+
129
+ // Keep last 12 today-events for marker overlay
130
+ const todayEvents = events.filter(e => e.isToday).slice(0, 12);
131
+
132
+ return {
133
+ available: scanned > 0,
134
+ daysScanned: scanned,
135
+ hours,
136
+ peakHour,
137
+ peakCount,
138
+ todayHours,
139
+ events: todayEvents
140
+ };
141
+ }
142
+
143
+ module.exports = { compute };
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+
3
+ const { identityPrimePath, safeRead, safeStat } = require("./paths");
4
+
5
+ // Parse Identity_Prime.md. Extracts archetype, core drive, fatal flaw, the law, anti-identity, code lines.
6
+
7
+ function parseIdentity(vault) {
8
+ const filePath = identityPrimePath(vault);
9
+ const text = safeRead(filePath);
10
+ const stat = safeStat(filePath);
11
+ if (!text) return { available: false, filePath };
12
+
13
+ const out = { available: true, filePath, lastUpdated: stat ? stat.mtime : null, raw: text, fields: {} };
14
+
15
+ // Bold field extraction
16
+ const re = /\*\*([^*]+):\*\*\s*([^\n]+)/g;
17
+ let m;
18
+ while ((m = re.exec(text)) !== null) {
19
+ const k = m[1].trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_|_$/g, "");
20
+ out.fields[k] = m[2].trim();
21
+ }
22
+
23
+ // Personal name from the H1 ("# IDENTITY PRIME: <name>") — the onboarding
24
+ // engine-writer writes the user's own answer there. Persona titles
25
+ // ("THE MOVER") and the unanswered placeholder ("You") are not names.
26
+ const h1 = text.match(/^#[^\n]*IDENTITY PRIME:\s*(.+?)\s*$/mi);
27
+ const h1name = h1 ? h1[1].trim() : "";
28
+ out.name = h1name && !/^the\s/i.test(h1name) && h1name.toLowerCase() !== "you" ? h1name : null;
29
+
30
+ // Common keys
31
+ out.archetype = out.fields.archetype || null;
32
+ out.coreDrive = out.fields.core_drive || null;
33
+ out.fatalFlaw = out.fields.fatal_flaw || null;
34
+ out.theLaw = out.fields.the_law || null;
35
+ out.antiIdentity = out.fields.anti_identity || null;
36
+ out.psychFirewall = out.fields.psychological_firewall || null;
37
+
38
+ // Sections
39
+ const sections = splitSections(text);
40
+ out.sections = sections;
41
+ out.code = extractListUnder(sections, "the code");
42
+ out.evolvedSelf = sectionBody(sections, "evolved self");
43
+ out.antiIdentityBody = sectionBody(sections, "anti-identity") || sectionBody(sections, "the mediocre");
44
+ out.primaryResistanceForms = extractListUnder(sections, "primary resistance forms");
45
+
46
+ return out;
47
+ }
48
+
49
+ function splitSections(text) {
50
+ const sections = {};
51
+ const lines = text.split("\n");
52
+ let currentTitle = null;
53
+ let buf = [];
54
+ for (const line of lines) {
55
+ const h = line.match(/^##+\s+(.+?)\s*$/);
56
+ if (h) {
57
+ if (currentTitle) sections[currentTitle.toLowerCase()] = buf.join("\n");
58
+ currentTitle = h[1];
59
+ buf = [];
60
+ } else if (currentTitle) {
61
+ buf.push(line);
62
+ }
63
+ }
64
+ if (currentTitle) sections[currentTitle.toLowerCase()] = buf.join("\n");
65
+ return sections;
66
+ }
67
+
68
+ function sectionBody(sections, needle) {
69
+ for (const [title, body] of Object.entries(sections)) {
70
+ if (title.includes(needle.toLowerCase())) return body;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ function extractListUnder(sections, needle) {
76
+ const body = sectionBody(sections, needle);
77
+ if (!body) return [];
78
+ return body.split("\n")
79
+ .map(l => l.match(/^\s*[-*\d.]+\s+(.+)$/))
80
+ .filter(Boolean)
81
+ .map(m => m[1].trim())
82
+ .slice(0, 12);
83
+ }
84
+
85
+ module.exports = { parseIdentity };