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.
- package/install.js +688 -68
- package/package.json +15 -3
- package/src/dashboard/build.js +1503 -0
- package/src/dashboard/dashboard.js +276 -0
- package/src/dashboard/index.js +319 -0
- package/src/dashboard/lib/activation-log.js +297 -0
- package/src/dashboard/lib/active-context-parser.js +177 -0
- package/src/dashboard/lib/agent-command.js +93 -0
- package/src/dashboard/lib/agent-detect.js +255 -0
- package/src/dashboard/lib/agent-detector.js +92 -0
- package/src/dashboard/lib/agent-session.js +462 -0
- package/src/dashboard/lib/anti-identity-detector.js +116 -0
- package/src/dashboard/lib/auto-learnings-parser.js +183 -0
- package/src/dashboard/lib/cli-usage-parser.js +92 -0
- package/src/dashboard/lib/config-parser.js +109 -0
- package/src/dashboard/lib/connect-recommender.js +131 -0
- package/src/dashboard/lib/correlations-parser.js +231 -0
- package/src/dashboard/lib/daily-note-resolver.js +211 -0
- package/src/dashboard/lib/date-utils.js +35 -0
- package/src/dashboard/lib/distribution-parser.js +78 -0
- package/src/dashboard/lib/dossier-parser.js +64 -0
- package/src/dashboard/lib/drift-history.js +83 -0
- package/src/dashboard/lib/drift-score.js +119 -0
- package/src/dashboard/lib/engine-health.js +170 -0
- package/src/dashboard/lib/engine-writer.js +1685 -0
- package/src/dashboard/lib/execution-plan.js +125 -0
- package/src/dashboard/lib/experiments-parser.js +429 -0
- package/src/dashboard/lib/feed-parser.js +294 -0
- package/src/dashboard/lib/forked-future.js +60 -0
- package/src/dashboard/lib/goal-forecast.js +304 -0
- package/src/dashboard/lib/goals-parser.js +67 -0
- package/src/dashboard/lib/health-protocols-parser.js +133 -0
- package/src/dashboard/lib/hook-activity.js +48 -0
- package/src/dashboard/lib/hook-indexer.js +169 -0
- package/src/dashboard/lib/hourly-activity-parser.js +143 -0
- package/src/dashboard/lib/identity-parser.js +85 -0
- package/src/dashboard/lib/ingestion.js +418 -0
- package/src/dashboard/lib/library-indexer-v2.js +212 -0
- package/src/dashboard/lib/library-indexer.js +105 -0
- package/src/dashboard/lib/log-activation.sh +61 -0
- package/src/dashboard/lib/memory-curator.js +97 -0
- package/src/dashboard/lib/memory-gardener.js +177 -0
- package/src/dashboard/lib/memory-gepa.js +102 -0
- package/src/dashboard/lib/memory-index.js +470 -0
- package/src/dashboard/lib/memory-rerank.js +72 -0
- package/src/dashboard/lib/memory-text.js +136 -0
- package/src/dashboard/lib/metrics-log-parser.js +76 -0
- package/src/dashboard/lib/moves-usage-parser.js +184 -0
- package/src/dashboard/lib/onboarding-forge.js +70 -0
- package/src/dashboard/lib/override-outcome-parser.js +68 -0
- package/src/dashboard/lib/override-summary.js +73 -0
- package/src/dashboard/lib/paths.js +192 -0
- package/src/dashboard/lib/pattern-manifest-loader.js +29 -0
- package/src/dashboard/lib/phantom-strategy.js +129 -0
- package/src/dashboard/lib/project-scanner.js +121 -0
- package/src/dashboard/lib/promise-wall.js +88 -0
- package/src/dashboard/lib/record-score.js +173 -0
- package/src/dashboard/lib/redaction.js +140 -0
- package/src/dashboard/lib/refusal-parser.js +44 -0
- package/src/dashboard/lib/regenerate-manifest.js +206 -0
- package/src/dashboard/lib/rewind-snapshots.js +689 -0
- package/src/dashboard/lib/roast-wall-parser.js +200 -0
- package/src/dashboard/lib/run-registry.js +226 -0
- package/src/dashboard/lib/safe-write.js +63 -0
- package/src/dashboard/lib/session-log-parser.js +145 -0
- package/src/dashboard/lib/session-time-parser.js +158 -0
- package/src/dashboard/lib/skill-index.js +171 -0
- package/src/dashboard/lib/skill-indexer.js +118 -0
- package/src/dashboard/lib/skill-recommender.js +689 -0
- package/src/dashboard/lib/strategy-parser.js +245 -0
- package/src/dashboard/lib/strategy-protocol-parser.js +135 -0
- package/src/dashboard/lib/streak-parser.js +95 -0
- package/src/dashboard/lib/suggested-now.js +254 -0
- package/src/dashboard/lib/tool-awareness.js +125 -0
- package/src/dashboard/lib/transcript-parser.js +331 -0
- package/src/dashboard/lib/vault-graph-parser.js +205 -0
- package/src/dashboard/lib/view-generator.js +163 -0
- package/src/dashboard/lib/voice-dna-parser.js +57 -0
- package/src/dashboard/lib/walkthrough-script.js +140 -0
- package/src/dashboard/lib/workflow-graph-parser.js +170 -0
- package/src/dashboard/lib/workflow-library-parser.js +146 -0
- package/src/dashboard/server.js +2024 -0
- package/src/dashboard/shortcut.js +0 -0
- package/src/dashboard/static/setup-poc.html +306 -0
- package/src/dashboard/static/walkthrough-poc.html +580 -0
- package/src/dashboard/styles.css +1201 -0
- package/src/dashboard/templates/index.html +278 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hero.png +0 -0
- package/src/dashboard/ui/dist/assets/index-598CSGOZ.js +157 -0
- package/src/dashboard/ui/dist/assets/index-BP--M69H.css +1 -0
- package/src/dashboard/ui/dist/assets/index-CidzmwSW.js +34 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/mover-system.css +62 -0
- package/src/dashboard/ui/dist/assets/xterm-CqkleIqs.js +1 -0
- package/src/dashboard/ui/dist/icon.svg +4 -0
- package/src/dashboard/ui/dist/index.html +18 -0
- package/src/dashboard/ui/dist/manifest.webmanifest +1 -0
- package/src/dashboard/ui/dist/registerSW.js +1 -0
- package/src/dashboard/ui/dist/sw.js +1 -0
- 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 };
|