mover-os 4.7.6 → 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 +763 -70
  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,254 @@
1
+ "use strict";
2
+
3
+ // Pure scoring engine. Ports the workflow-router proactive triggers to JS.
4
+ // Input: { now, ac, manifest, daily, engineHealth, strategy, projects }
5
+ // Output: Array<{ workflow, reason, score, urgency, signal, time }> sorted by score desc, top N.
6
+
7
+ const SIGNAL_TABLE = [
8
+ // Each entry returns null OR { workflow, reason, score, urgency }
9
+ function staleACRed({ now, ac }) {
10
+ if (!ac || !ac.lastUpdated) return null;
11
+ const days = ageDays(now, ac.lastUpdated);
12
+ if (days >= 7) return { workflow: "/reboot", reason: `Active_Context.md hasn't been updated in ${days} days. Recovery needed.`, score: 100, urgency: "RED" };
13
+ return null;
14
+ },
15
+ function staleACYellow({ now, ac }) {
16
+ if (!ac || !ac.lastUpdated) return null;
17
+ const days = ageDays(now, ac.lastUpdated);
18
+ if (days >= 3 && days < 7) return { workflow: "/log", reason: `Active_Context.md is ${days} days stale. Capture session state.`, score: 70, urgency: "YELLOW" };
19
+ return null;
20
+ },
21
+ function noDailyNoteEarly({ now, daily }) {
22
+ if (!daily) return null;
23
+ const hour = now.getHours();
24
+ if (hour < 12 && daily.today && !daily.today.exists) {
25
+ return { workflow: "/plan-tomorrow", reason: `It's ${formatTime(now)} and you don't have a Daily Note yet. Set today's plan.`, score: 95, urgency: "AMBER" };
26
+ }
27
+ return null;
28
+ },
29
+ function lateAfternoonNoLog({ now, ac }) {
30
+ if (!ac) return null;
31
+ const hour = now.getHours();
32
+ if (hour < 17 || hour > 21) return null;
33
+ const lastLog = ac.workflowState && ac.workflowState.log_last_run;
34
+ if (!lastLog) return { workflow: "/log", reason: `It's ${formatTime(now)} and no /log run today. Capture session before context drifts.`, score: 85, urgency: "AMBER" };
35
+ if (!isSameDay(parseDate(lastLog), now)) {
36
+ return { workflow: "/log", reason: `It's ${formatTime(now)} and /log hasn't run today. Capture before context drifts.`, score: 85, urgency: "AMBER" };
37
+ }
38
+ return null;
39
+ },
40
+ function eveningNoAnalyseDay({ now, ac }) {
41
+ if (!ac) return null;
42
+ const hour = now.getHours();
43
+ if (hour < 21) return null;
44
+ const lastAd = ac.workflowState && ac.workflowState.analyse_day_last_run;
45
+ if (!lastAd || !isSameDay(parseDate(lastAd), now)) {
46
+ return { workflow: "/analyse-day", reason: `It's past ${formatTime(now)}. Brutal day audit before sleep.`, score: 90, urgency: "AMBER" };
47
+ }
48
+ return null;
49
+ },
50
+ function strategyStaleYellow({ now, engineHealth }) {
51
+ if (!engineHealth) return null;
52
+ const item = engineHealth.items && engineHealth.items.find(i => i.file === "Strategy");
53
+ if (!item || !item.ageDays) return null;
54
+ if (item.ageDays >= 14 && item.ageDays < 60) {
55
+ return { workflow: "/pivot-strategy", reason: `Strategy.md is ${item.ageDays} days old. Hypothesis may be drifting from reality.`, score: 60, urgency: "YELLOW" };
56
+ }
57
+ return null;
58
+ },
59
+ function strategyStaleRed({ now, engineHealth }) {
60
+ if (!engineHealth) return null;
61
+ const item = engineHealth.items && engineHealth.items.find(i => i.file === "Strategy");
62
+ if (!item || !item.ageDays) return null;
63
+ if (item.ageDays >= 60) {
64
+ return { workflow: "/pivot-strategy", reason: `Strategy.md is ${item.ageDays} days stale. Workflows will block, pivot or confirm now.`, score: 100, urgency: "RED" };
65
+ }
66
+ return null;
67
+ },
68
+ function reviewDay({ now, ac, engineHealth }) {
69
+ const reviewDay = (ac && ac.workflowState && ac.workflowState.review_week_day) || "Sunday";
70
+ const dayName = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][now.getDay()];
71
+ if (dayName !== reviewDay) return null;
72
+ const lastWr = ac.workflowState && ac.workflowState.weekly_review_last_run;
73
+ if (!lastWr) return { workflow: "/review-week", reason: `It's ${dayName}. Weekly review hasn't run yet.`, score: 80, urgency: "AMBER" };
74
+ const lastDt = parseDate(lastWr);
75
+ if (lastDt && (now.getTime() - lastDt.getTime()) > 3 * 86400000) {
76
+ return { workflow: "/review-week", reason: `It's ${dayName} and the last weekly review was ${ageDays(now, lastDt)}d ago.`, score: 80, urgency: "AMBER" };
77
+ }
78
+ return null;
79
+ },
80
+ function zombieTask({ manifest }) {
81
+ if (!manifest || !manifest.zombieCount) return null;
82
+ if (manifest.zombieCount > 0) {
83
+ const taskName = manifest.zombieTasks && manifest.zombieTasks[0] ? manifest.zombieTasks[0] : "a task";
84
+ return { workflow: "/debug-resistance", reason: `${manifest.zombieCount} zombie task${manifest.zombieCount > 1 ? "s" : ""}: "${taskName}" stuck 3+ days.`, score: 75, urgency: "AMBER" };
85
+ }
86
+ return null;
87
+ },
88
+ function frictionEscalated({ manifest, engineHealth }) {
89
+ if (!manifest) return null;
90
+ const level = manifest.frictionLevel || "L0";
91
+ const num = parseInt(String(level).replace(/[^\d]/g, ""), 10) || 0;
92
+ if (num < 2) return null;
93
+ const stratItem = engineHealth && engineHealth.items && engineHealth.items.find(i => i.file === "Strategy");
94
+ const stratStale = stratItem && stratItem.ageDays && stratItem.ageDays > 10;
95
+ if (stratStale) {
96
+ return { workflow: "/pivot-strategy", reason: `Friction at ${level} AND Strategy ${stratItem.ageDays}d stale. Confirm strategy before enforcing.`, score: 95, urgency: "RED" };
97
+ }
98
+ return null;
99
+ },
100
+ function firstLoadNoMorning({ now, ac }) {
101
+ const hour = now.getHours();
102
+ if (hour >= 11) return null;
103
+ const today = formatDate(now);
104
+ const lastLog = ac && ac.workflowState && ac.workflowState.log_last_run;
105
+ const lastLogDate = lastLog ? parseDate(lastLog) : null;
106
+ if (lastLogDate && isSameDay(lastLogDate, now)) return null;
107
+ return { workflow: "/morning", reason: `Morning primer, set energy, focus, single test for today.`, score: 70, urgency: "INFO" };
108
+ },
109
+ function noDailyStreakBroken({ now, daily }) {
110
+ if (!daily) return null;
111
+ if (daily.streak === 0 && daily.recentNotes) {
112
+ const lastNote = daily.recentNotes.find(n => n.exists);
113
+ if (lastNote) {
114
+ const lastDt = parseDate(lastNote.date);
115
+ const days = ageDays(now, lastDt);
116
+ if (days >= 3) {
117
+ return { workflow: "/reboot", reason: `${days} days since last Daily Note. Recovery + minimum viable action.`, score: 100, urgency: "RED" };
118
+ }
119
+ }
120
+ }
121
+ return null;
122
+ },
123
+ function expiredGoal({ engineHealth }) {
124
+ if (!engineHealth || !engineHealth.expiredGoals || engineHealth.expiredGoals.length === 0) return null;
125
+ const exp = engineHealth.expiredGoals[0];
126
+ return { workflow: "/review-week", reason: `Goal expired ${exp.expiredDays} days ago (target: ${exp.targetDate}). Revise or kill.`, score: 65, urgency: "YELLOW" };
127
+ }
128
+ ];
129
+
130
+ function computeSuggestions(input, options = {}) {
131
+ const ctx = {
132
+ now: input.now || new Date(),
133
+ ac: input.ac || null,
134
+ manifest: input.manifest || null,
135
+ daily: input.daily || null,
136
+ engineHealth: input.engineHealth || null,
137
+ strategy: input.strategy || null,
138
+ projects: input.projects || null
139
+ };
140
+
141
+ // Fresh-vault gate (sandbox catch, 2026-07-07): a day-zero vault has no
142
+ // sessions to capture and no day to audit — every time-of-day nag ("/log
143
+ // hasn't run today") would be a fabricated errand for a user who just
144
+ // arrived. Until any workflow has ever run or any note exists, the only
145
+ // honest suggestion is starting: /setup (no Engine yet) or /morning
146
+ // (Engine written, first day not started).
147
+ // The shipped Active_Context template writes literal "never" values — a
148
+ // truthy string is NOT evidence a workflow ever ran. Only a parseable
149
+ // date counts.
150
+ const ws = (ctx.ac && ctx.ac.workflowState) || {};
151
+ const anyRun = ["log_last_run", "analyse_day_last_run", "weekly_review_last_run", "pivot_strategy_last_run"]
152
+ .some(k => ws[k] && parseDate(String(ws[k])));
153
+ const anyNote = !!(ctx.daily && ((ctx.daily.streak || 0) > 0 || (ctx.daily.recentNotes || []).some(n => n && n.exists)));
154
+ if (!anyRun && !anyNote) {
155
+ const strategyWritten = !!(ctx.strategy && (ctx.strategy.available || ctx.strategy.primeObjective));
156
+ return [{
157
+ workflow: strategyWritten ? "/morning" : "/setup",
158
+ reason: strategyWritten
159
+ ? "Your Engine is built but no day is on the record yet. /morning starts the first one."
160
+ : "Nothing is on the record yet. /setup builds your Identity, Strategy, and Goals, the Engine everything here runs on.",
161
+ score: 100,
162
+ urgency: "INFO",
163
+ signal: "freshVault",
164
+ time: formatTime(ctx.now)
165
+ }];
166
+ }
167
+
168
+ const candidates = [];
169
+ for (const fn of SIGNAL_TABLE) {
170
+ try {
171
+ const sig = fn(ctx);
172
+ if (sig) {
173
+ sig.signal = fn.name;
174
+ sig.time = formatTime(ctx.now);
175
+ candidates.push(sig);
176
+ }
177
+ } catch (e) {
178
+ // Per-signal try/catch: bad parser data shouldn't kill the engine
179
+ }
180
+ }
181
+
182
+ // Deduplicate by workflow — keep highest-score per workflow
183
+ const byWorkflow = {};
184
+ for (const c of candidates) {
185
+ if (!byWorkflow[c.workflow] || byWorkflow[c.workflow].score < c.score) {
186
+ byWorkflow[c.workflow] = c;
187
+ }
188
+ }
189
+
190
+ let merged = Object.values(byWorkflow);
191
+
192
+ // Audit pass 2 (find-bugs #4): drop any workflow whose snooze.until is
193
+ // still in the future. Snooze button on the Action Panel writes
194
+ // ~/.mover/dashboard/snoozes.json then triggers a rebuild — without
195
+ // this filter, the snoozed workflow re-surfaces at the top until the
196
+ // snooze expires, defeating the click.
197
+ const snoozes = Array.isArray(input.snoozes) ? input.snoozes : [];
198
+ if (snoozes.length) {
199
+ const nowMs = ctx.now.getTime();
200
+ // Pass 3 #9 regression: corrupt `until` (typo'd ISO, empty string)
201
+ // returns NaN from Date.parse — falls through the filter silently,
202
+ // un-snoozing the workflow without telling the user their click
203
+ // didn't stick. Warn once per build so the server log surfaces the
204
+ // bad entry without spamming.
205
+ const active = new Set();
206
+ for (const s of snoozes) {
207
+ if (!s || !s.workflow) continue;
208
+ const t = s.until ? Date.parse(s.until) : NaN;
209
+ if (Number.isNaN(t)) {
210
+ console.warn(`[suggested-now] snooze entry has invalid until: ${JSON.stringify(s).slice(0, 120)}`);
211
+ continue;
212
+ }
213
+ if (t > nowMs) active.add(s.workflow);
214
+ }
215
+ if (active.size) {
216
+ merged = merged.filter(c => !active.has(c.workflow));
217
+ }
218
+ }
219
+
220
+ merged.sort((a, b) => b.score - a.score);
221
+
222
+ const top = merged.slice(0, options.limit || 3);
223
+ return top;
224
+ }
225
+
226
+ function ageDays(now, then) {
227
+ if (!then) return null;
228
+ const t = then instanceof Date ? then : new Date(then);
229
+ if (isNaN(t.getTime())) return null;
230
+ return Math.floor((now.getTime() - t.getTime()) / (24 * 60 * 60 * 1000));
231
+ }
232
+
233
+ function isSameDay(a, b) {
234
+ if (!a || !b) return false;
235
+ return a.getFullYear() === b.getFullYear() &&
236
+ a.getMonth() === b.getMonth() &&
237
+ a.getDate() === b.getDate();
238
+ }
239
+
240
+ function parseDate(s) {
241
+ if (!s) return null;
242
+ const d = new Date(s);
243
+ return isNaN(d.getTime()) ? null : d;
244
+ }
245
+
246
+ function formatTime(d) {
247
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
248
+ }
249
+
250
+ function formatDate(d) {
251
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
252
+ }
253
+
254
+ module.exports = { computeSuggestions };
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+ const { execFileSync } = require("child_process");
7
+
8
+ // tool-awareness.js — the awareness half of the recommend layer (owner ruling
9
+ // 2026-07-07: bundled skills install ALL; recommendations are for EXTERNAL
10
+ // things only — MCP connectors + CLI tools). This module answers two honest
11
+ // questions from the machine itself, never from assumption:
12
+ // 1. Which useful CLI tools are already installed? (command -v sweep)
13
+ // 2. Which MCP connectors are already attached? (the agent's own config)
14
+ // Catalogues live in src/system/{cli,connector}-catalogue.json and ship with
15
+ // the bundle; the funnel matches them against the setup profile and marks
16
+ // what the user already has instead of recommending it.
17
+
18
+ const SYSTEM_DIR = path.join(__dirname, "..", "..", "system");
19
+
20
+ function readCatalogue(file, key) {
21
+ try {
22
+ const raw = JSON.parse(fs.readFileSync(path.join(SYSTEM_DIR, file), "utf8"));
23
+ return Array.isArray(raw[key]) ? raw[key] : [];
24
+ } catch (_) {
25
+ return [];
26
+ }
27
+ }
28
+
29
+ function cliCatalogue() { return readCatalogue("cli-catalogue.json", "tools"); }
30
+ function connectorCatalogue() { return readCatalogue("connector-catalogue.json", "connectors"); }
31
+
32
+ // which(bin) — true when the binary resolves on PATH. execFileSync (no shell)
33
+ // so a hostile catalogue entry can't smuggle commands.
34
+ function which(bin) {
35
+ if (!/^[A-Za-z0-9._-]+$/.test(String(bin || ""))) return false;
36
+ try {
37
+ execFileSync(process.platform === "win32" ? "where" : "which", [bin], { stdio: "pipe", timeout: 3000 });
38
+ return true;
39
+ } catch (_) {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ // detectCli() — the catalogue with an installed flag per entry.
45
+ function detectCli() {
46
+ return cliCatalogue().map((t) => ({ ...t, installed: which(t.bin) }));
47
+ }
48
+
49
+ // attachedMcpServers() — names of MCP servers the user's Claude config already
50
+ // carries. Reads config files directly (fast, no agent spawn): ~/.claude.json
51
+ // top-level mcpServers + per-project entries, plus .mcp.json in the vault if
52
+ // one exists. Missing/unparseable files are honest empties, never errors.
53
+ function attachedMcpServers({ vault } = {}) {
54
+ const names = new Set();
55
+ const home = os.homedir();
56
+ const eat = (obj) => {
57
+ if (obj && typeof obj.mcpServers === "object" && obj.mcpServers) {
58
+ Object.keys(obj.mcpServers).forEach((k) => names.add(k.toLowerCase()));
59
+ }
60
+ };
61
+ try {
62
+ const cfg = JSON.parse(fs.readFileSync(path.join(home, ".claude.json"), "utf8"));
63
+ eat(cfg);
64
+ if (cfg && typeof cfg.projects === "object" && cfg.projects) {
65
+ Object.values(cfg.projects).forEach(eat);
66
+ }
67
+ } catch (_) { /* no global claude config — fine */ }
68
+ if (vault) {
69
+ try { eat(JSON.parse(fs.readFileSync(path.join(vault, ".mcp.json"), "utf8"))); } catch (_) { /* none */ }
70
+ }
71
+ return Array.from(names);
72
+ }
73
+
74
+ // detectConnectors() — the catalogue with an attached flag per entry (matched
75
+ // by id against the user's configured MCP server names, substring-tolerant:
76
+ // "slack" matches "slack-mcp" etc.).
77
+ function detectConnectors({ vault } = {}) {
78
+ const attached = attachedMcpServers({ vault });
79
+ return connectorCatalogue().map((c) => ({
80
+ ...c,
81
+ attached: attached.some((n) => n.includes(c.id) || c.id.includes(n)),
82
+ }));
83
+ }
84
+
85
+ // matchByProfile(entries, profile) — same keyword engine shape as
86
+ // skill-recommender: every profile string is tokenized and matched against
87
+ // each entry's keywords; score = distinct keyword hits. Returns entries with
88
+ // score > 0, ranked, un-matched entries dropped (the funnel shows a "show
89
+ // everything" affordance backed by the raw catalogue instead).
90
+ function matchByProfile(entries, profile) {
91
+ const text = []
92
+ .concat(profile && profile.work ? [profile.work] : [])
93
+ .concat(Array.isArray(profile && profile.domain) ? profile.domain : [])
94
+ .concat(Array.isArray(profile && profile.tools) ? profile.tools : [])
95
+ .concat(Array.isArray(profile && profile.goals) ? profile.goals : [])
96
+ .join(" ")
97
+ .toLowerCase();
98
+ if (!text.trim()) return [];
99
+ return entries
100
+ .map((e) => {
101
+ const hits = (e.keywords || []).filter((k) => text.includes(k)).length;
102
+ return { entry: e, score: hits };
103
+ })
104
+ .filter((x) => x.score > 0)
105
+ .sort((a, b) => b.score - a.score)
106
+ .map((x) => ({ ...x.entry, confidence: Math.min(1, x.score / 3) }));
107
+ }
108
+
109
+ // awareness(profile, {vault}) — the full recommend-layer payload:
110
+ // cliTools: profile-matched catalogue entries, installed flagged
111
+ // connectors: profile-matched catalogue entries, attached flagged
112
+ // installedCli / attachedConnectors: the full awareness sweep (for the
113
+ // Configure surface and the co-founder's context)
114
+ function awareness(profile, { vault } = {}) {
115
+ const cli = detectCli();
116
+ const conn = detectConnectors({ vault });
117
+ return {
118
+ cliTools: matchByProfile(cli, profile),
119
+ connectors: matchByProfile(conn, profile),
120
+ installedCli: cli.filter((t) => t.installed).map((t) => t.id),
121
+ attachedConnectors: conn.filter((c) => c.attached).map((c) => c.id),
122
+ };
123
+ }
124
+
125
+ module.exports = { awareness, detectCli, detectConnectors, attachedMcpServers, matchByProfile, cliCatalogue, connectorCatalogue };