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,183 @@
1
+ "use strict";
2
+
3
+ const { autoLearningsPath, safeRead, safeStat } = require("./paths");
4
+
5
+ // Parse Auto_Learnings.md — splits on `## (PATTERN|CORRECTION|BELIEF SHIFT|REFLEXION): Name` headers.
6
+ // REFLEXION (C3) is the AI's own error→recovery post-mortem, distinct from a user CORRECTION.
7
+ // Returns {available, entries: [...], stats: {patterns, corrections, beliefShifts, reflexions, byConfidence}}
8
+
9
+ function parseAutoLearnings(vault) {
10
+ const filePath = autoLearningsPath(vault);
11
+ const text = safeRead(filePath);
12
+ const stat = safeStat(filePath);
13
+ if (!text) return { available: false, error: "Auto_Learnings.md not found", entries: [], stats: emptyStats() };
14
+
15
+ const lines = text.split("\n");
16
+ const entries = [];
17
+ let current = null;
18
+ let buf = [];
19
+ // Track current section context so we can tag entries with polarity
20
+ // when authored under `## Anti-Patterns` / `## Positive Patterns`
21
+ // sectional headers. Defaults to null (use heuristic).
22
+ let sectionPolarity = null;
23
+
24
+ for (const line of lines) {
25
+ // Section-context header (Anti-Patterns / Positive Patterns)
26
+ const sec = line.match(/^##\s+(Anti-Patterns?|Positive Patterns?|Strengths?|Rituals?)\s*$/i);
27
+ if (sec) {
28
+ if (current) {
29
+ current.body = buf.join("\n");
30
+ entries.push(finalizeEntry(current, sectionPolarity));
31
+ current = null;
32
+ buf = [];
33
+ }
34
+ const tok = sec[1].toLowerCase();
35
+ sectionPolarity = /anti|leak/.test(tok) ? "anti"
36
+ : /positive|strength/.test(tok) ? "lift"
37
+ : /ritual/.test(tok) ? "ritual"
38
+ : null;
39
+ continue;
40
+ }
41
+
42
+ const m = line.match(/^##\s+(PATTERN|CORRECTION|BELIEF SHIFT|REFLEXION):\s*(.+?)\s*$/i);
43
+ if (m) {
44
+ if (current) {
45
+ current.body = buf.join("\n");
46
+ entries.push(finalizeEntry(current, sectionPolarity));
47
+ }
48
+ current = { type: m[1].toUpperCase(), name: m[2].trim() };
49
+ buf = [];
50
+ } else if (current) {
51
+ buf.push(line);
52
+ }
53
+ }
54
+ if (current) {
55
+ current.body = buf.join("\n");
56
+ entries.push(finalizeEntry(current, sectionPolarity));
57
+ }
58
+
59
+ const stats = computeStats(entries);
60
+
61
+ return {
62
+ available: true,
63
+ filePath,
64
+ lastUpdated: stat ? stat.mtime : null,
65
+ entries,
66
+ stats,
67
+ activePatternsConf3Plus: entries.filter(e =>
68
+ e.type === "PATTERN" &&
69
+ typeof e.confidence === "number" &&
70
+ e.confidence >= 3 &&
71
+ !["RESOLVED", "AUTOMATIC"].includes((e.status || "").toUpperCase())
72
+ ),
73
+ growingPatterns: entries.filter(e =>
74
+ e.type === "PATTERN" && (e.status || "").toUpperCase() === "GROWING"
75
+ ),
76
+ escalatedPatterns: entries.filter(e =>
77
+ e.type === "PATTERN" && (e.status || "").toUpperCase() === "ESCALATED"
78
+ ),
79
+ recentBeliefShifts: entries.filter(e => e.type === "BELIEF SHIFT").slice(0, 5),
80
+ recentCorrections: entries.filter(e => e.type === "CORRECTION").slice(-10).reverse(),
81
+ recentReflexions: entries.filter(e => e.type === "REFLEXION").slice(-10).reverse()
82
+ };
83
+ }
84
+
85
+ function finalizeEntry(entry, sectionPolarity) {
86
+ const body = entry.body || "";
87
+ const fields = parseBoldFields(body);
88
+ const evidence = parseEvidence(body);
89
+
90
+ const severity = (fields.severity || "").toUpperCase() || null;
91
+ const status = (fields.status || "").toUpperCase().replace(/\s+/g, "_") || null;
92
+ // Polarity: explicit section context > severity heuristic
93
+ let polarity = sectionPolarity;
94
+ if (!polarity) {
95
+ if (severity && /POSITIVE/.test(severity)) polarity = "lift";
96
+ else if (severity === "HIGH" || severity === "CRIT" || severity === "CRITICAL") polarity = "anti";
97
+ else if (status === "GROWING" || status === "BAKED_INTO_STRATEGY_MD") polarity = "watching";
98
+ else polarity = "neutral";
99
+ }
100
+ // 5-type taxonomy mapping (LEAK / DRIFT / STRENGTH / RECOVERY / RITUAL)
101
+ let category = "watching";
102
+ if (polarity === "anti") {
103
+ if (/distribution|gap|spree|avoidance|procrastinat|phantom/i.test(entry.name)) category = "drift";
104
+ else category = "leak";
105
+ } else if (polarity === "lift" || polarity === "strength") category = "strength";
106
+ else if (polarity === "ritual") category = "ritual";
107
+ else if (status === "GROWING") category = "recovery";
108
+
109
+ return {
110
+ type: entry.type,
111
+ name: entry.name,
112
+ body,
113
+ fields,
114
+ evidence,
115
+ confidence: parseInt(fields.confidence || "0", 10) || null,
116
+ status,
117
+ severity,
118
+ polarity,
119
+ category,
120
+ domain: fields.domain || null,
121
+ type_subtype: fields.type || null,
122
+ firstDetected: fields.first_detected || null,
123
+ lastUpdated: fields.last_updated || null,
124
+ lastValidated: fields.last_validated || null,
125
+ confrontationCount: parseInt(fields.confrontation_count || "0", 10) || null,
126
+ trigger: fields.trigger || null,
127
+ counterAction: fields.counter_action || null,
128
+ suspectedVariable: fields.suspected_variable || null,
129
+ hypothesis: fields.hypothesis || null,
130
+ resistanceForm: fields.resistance_form || null,
131
+ beliefTier: fields.belief_tier || null,
132
+ previousBelief: fields.previous_belief || null,
133
+ newBelief: fields.new_belief || null,
134
+ error: fields.error || null,
135
+ fix: fields.fix || null
136
+ };
137
+ }
138
+
139
+ function parseBoldFields(body) {
140
+ const fields = {};
141
+ const re = /\*\*([^*]+):\*\*\s*([^\n]+)/g;
142
+ let m;
143
+ while ((m = re.exec(body)) !== null) {
144
+ const k = m[1].trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_|_$/g, "");
145
+ fields[k] = m[2].trim();
146
+ }
147
+ return fields;
148
+ }
149
+
150
+ function parseEvidence(body) {
151
+ const evidence = [];
152
+ const lines = body.split("\n");
153
+ let inEvidence = false;
154
+ for (const line of lines) {
155
+ if (/^###\s+Evidence/i.test(line)) { inEvidence = true; continue; }
156
+ if (inEvidence && /^###\s+/.test(line)) inEvidence = false;
157
+ if (inEvidence) {
158
+ const m = line.match(/^[-*]\s+(.+)$/);
159
+ if (m) evidence.push(m[1].trim());
160
+ }
161
+ }
162
+ return evidence;
163
+ }
164
+
165
+ function emptyStats() {
166
+ return { total: 0, patterns: 0, corrections: 0, beliefShifts: 0, reflexions: 0, byConfidence: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }, byStatus: {} };
167
+ }
168
+
169
+ function computeStats(entries) {
170
+ const stats = emptyStats();
171
+ stats.total = entries.length;
172
+ for (const e of entries) {
173
+ if (e.type === "PATTERN") stats.patterns++;
174
+ else if (e.type === "CORRECTION") stats.corrections++;
175
+ else if (e.type === "BELIEF SHIFT") stats.beliefShifts++;
176
+ else if (e.type === "REFLEXION") stats.reflexions++;
177
+ if (e.confidence && e.confidence >= 1 && e.confidence <= 5) stats.byConfidence[e.confidence]++;
178
+ if (e.status) stats.byStatus[e.status] = (stats.byStatus[e.status] || 0) + 1;
179
+ }
180
+ return stats;
181
+ }
182
+
183
+ module.exports = { parseAutoLearnings };
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+
3
+ const { safeReadJson, moverConfigPath } = require("./paths");
4
+
5
+ // Parse moveros CLI usage from ~/.mover/config.json cfg.cli_usage map.
6
+ // Cross-references with the 17-entry CLI_COMMANDS registry the server
7
+ // exposes at /api/cli/index so the dashboard can show:
8
+ // - count: how many times this command has been run
9
+ // - lastRun: ISO timestamp of last invocation
10
+ // - ageDays: days since lastRun
11
+
12
+ // Canonical command list from the moveros CLI install.js. Mirror of
13
+ // /api/cli/index — kept in sync via the install package's CLI_COMMANDS.
14
+ const COMMANDS = [
15
+ { name: "install", description: "Fresh install wizard" },
16
+ { name: "update", description: "Comprehensive update: agents, rules, skills" },
17
+ { name: "uninstall", description: "Remove Mover OS from vault and agents" },
18
+ { name: "doctor", description: "Health check across all installed agents" },
19
+ { name: "pulse", description: "Terminal dashboard: energy, tasks, streaks" },
20
+ { name: "capture", description: "Quick capture: tasks, links, ideas" },
21
+ { name: "who", description: "Entity memory lookup" },
22
+ { name: "diff", description: "Engine file evolution viewer" },
23
+ { name: "sync", description: "Cross-agent synchronization" },
24
+ { name: "replay", description: "Session replay from Daily Notes" },
25
+ { name: "context", description: "Debug what each agent sees" },
26
+ { name: "settings", description: "View/edit config" },
27
+ { name: "backup", description: "Manual backup wizard" },
28
+ { name: "restore", description: "Restore from backup" },
29
+ { name: "prayer", description: "Manage prayer times" },
30
+ { name: "status", description: "Full system state (vault, agents, version)" },
31
+ { name: "help", description: "Interactive guide to Mover OS" },
32
+ ];
33
+
34
+ function parseCliUsage() {
35
+ // False-zero discipline (T261): distinguish "config absent / never tracked"
36
+ // from "tracked, genuinely zero runs". A present cfg.cli_usage map — even
37
+ // all-zero — is real telemetry and stays available:true. Only true ABSENCE
38
+ // (no config file, or no cli_usage key at all) flips available:false so the
39
+ // consumer renders "—"/"calibrating" instead of a fabricated 0-usage table.
40
+ const cfg = safeReadJson(moverConfigPath());
41
+ if (cfg == null) {
42
+ return {
43
+ available: false,
44
+ unavailableReason: "file_missing",
45
+ commands: [],
46
+ totalUsage: null,
47
+ everUsedCount: null,
48
+ };
49
+ }
50
+ if (cfg.cli_usage == null || typeof cfg.cli_usage !== "object") {
51
+ return {
52
+ available: false,
53
+ unavailableReason: "no_data",
54
+ commands: [],
55
+ totalUsage: null,
56
+ everUsedCount: null,
57
+ };
58
+ }
59
+ const usage = cfg.cli_usage;
60
+
61
+ const commands = COMMANDS.map(c => {
62
+ const u = usage[c.name] || {};
63
+ const count = typeof u.count === "number" ? u.count : 0;
64
+ const last = u.last || null;
65
+ let ageDays = null;
66
+ if (last) {
67
+ try {
68
+ const t = new Date(last).getTime();
69
+ if (!isNaN(t)) ageDays = Math.floor((Date.now() - t) / 86400000);
70
+ } catch (_) { /* skip */ }
71
+ }
72
+ return {
73
+ name: c.name,
74
+ description: c.description,
75
+ count,
76
+ lastRun: last,
77
+ ageDays,
78
+ };
79
+ });
80
+
81
+ const totalUsage = commands.reduce((sum, c) => sum + c.count, 0);
82
+ const everUsed = commands.filter(c => c.count > 0).length;
83
+
84
+ return {
85
+ available: true,
86
+ commands,
87
+ totalUsage,
88
+ everUsedCount: everUsed,
89
+ };
90
+ }
91
+
92
+ module.exports = { parseCliUsage };
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ // config-parser.js — Read/write ~/.mover/config.json. Used by Settings mode +
8
+ // agent-detector (preferredAgent) + first-run UX (vaultPath resolution).
9
+
10
+ const CONFIG_PATH = path.join(os.homedir(), ".mover", "config.json");
11
+
12
+ const DEFAULTS = Object.freeze({
13
+ vaultPath: null,
14
+ preferredAgent: null,
15
+ // Onboarding basic-info (captured on first run, editable in Configure):
16
+ userName: null, // what to call the user
17
+ vaultName: null, // friendly label for their vault/system
18
+ osName: null, // dashboard + desktop-shortcut display name (shortcut.js reads os_name/osName)
19
+ lens: "everyday", // worldview "lingo": everyday | founder | muslim | secular | custom
20
+ lensCustom: { bet: "", seal: "", receipts: "" }, // the custom-lens term overrides
21
+ reviewDay: "Sunday",
22
+ eveningZoneStart: 17,
23
+ midnightCutoff: 5,
24
+ deepWorkHours: [9, 12],
25
+ sleepTarget: 7,
26
+ frictionSensitivity: "normal",
27
+ // #112: per-provider run tuning. null model/effort = the provider's own
28
+ // default (flag omitted). These are "what the Run picker starts on", not a
29
+ // hidden default — the picker writes back the user's last choice.
30
+ providerPrefs: {
31
+ "claude-code": { model: null, effort: null },
32
+ codex: { model: null, effort: null },
33
+ gemini: { model: null, approvalMode: "yolo" },
34
+ },
35
+ redaction: {
36
+ names: true,
37
+ faith: true,
38
+ weight: true,
39
+ money: true,
40
+ projectNames: true,
41
+ exactDates: false,
42
+ fileNames: true,
43
+ },
44
+ studio: {
45
+ density: "comfortable",
46
+ mono: false,
47
+ accent: "#d97a3c",
48
+ showAgentsInTopStrip: true,
49
+ autoOpenBrowser: true,
50
+ }
51
+ });
52
+
53
+ // two-level merge: per-provider objects merge key-by-key so a stored
54
+ // { codex: { effort } } keeps the default gemini approvalMode intact.
55
+ function mergeProviderPrefs(base, raw) {
56
+ const out = {};
57
+ for (const k of Object.keys(base)) out[k] = { ...base[k] };
58
+ for (const k of Object.keys(raw || {})) {
59
+ const next = raw[k];
60
+ out[k] = next && typeof next === "object" ? { ...(out[k] || {}), ...next } : next;
61
+ }
62
+ return out;
63
+ }
64
+
65
+ function read() {
66
+ try {
67
+ if (!fs.existsSync(CONFIG_PATH)) return { ...DEFAULTS, exists: false, path: CONFIG_PATH };
68
+ const text = fs.readFileSync(CONFIG_PATH, "utf8");
69
+ const raw = JSON.parse(text);
70
+ return {
71
+ ...DEFAULTS,
72
+ ...raw,
73
+ // Deep-merge for nested objects
74
+ redaction: { ...DEFAULTS.redaction, ...(raw.redaction || {}) },
75
+ studio: { ...DEFAULTS.studio, ...(raw.studio || {}) },
76
+ lensCustom: { ...DEFAULTS.lensCustom, ...(raw.lensCustom || {}) },
77
+ providerPrefs: mergeProviderPrefs(DEFAULTS.providerPrefs, raw.providerPrefs),
78
+ exists: true,
79
+ path: CONFIG_PATH,
80
+ };
81
+ } catch (e) {
82
+ return { ...DEFAULTS, exists: false, path: CONFIG_PATH, parseError: e.message };
83
+ }
84
+ }
85
+
86
+ function write(updates) {
87
+ // Merge with existing — never overwrite full file
88
+ const current = read();
89
+ delete current.exists;
90
+ delete current.path;
91
+ delete current.parseError;
92
+ const next = {
93
+ ...current,
94
+ ...updates,
95
+ redaction: { ...current.redaction, ...(updates.redaction || {}) },
96
+ studio: { ...current.studio, ...(updates.studio || {}) },
97
+ lensCustom: { ...current.lensCustom, ...(updates.lensCustom || {}) },
98
+ };
99
+ try {
100
+ const dir = path.dirname(CONFIG_PATH);
101
+ fs.mkdirSync(dir, { recursive: true });
102
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(next, null, 2) + "\n", "utf8");
103
+ return { ok: true, written: next };
104
+ } catch (e) {
105
+ return { ok: false, error: e.message };
106
+ }
107
+ }
108
+
109
+ module.exports = { read, write, DEFAULTS, CONFIG_PATH };
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * connect-recommender.js — from a setup-interview profile, build a personalized,
5
+ * domain-grouped "what to feed/connect me" plan. This is the fix for the cold-start gap
6
+ * ("if you don't feed it your day-to-day data it takes a while to pick up"): on day one we
7
+ * tell the user EXACTLY what to feed Mover OS, grouped by domain, in their own context —
8
+ * instead of a generic "upload your notes."
9
+ *
10
+ * Deterministic + zero-dep (mirrors skill-recommender.js). HONESTY CONTRACT: a source is
11
+ * `status:'ready'` ONLY if we can act on it right now — i.e. a file UPLOAD, wired to the real
12
+ * /api/session/ingest. Live OAuth connectors we have NOT built (Slack/Drive/calendar) are
13
+ * `status:'planned'` — an honest recommendation, never a fake "Connect" button. There is always
14
+ * ≥1 ready action so the user can do something real immediately.
15
+ *
16
+ * @param {object} profile free-text setup answers ({work, tools, vitality, faith, goals, ...})
17
+ * @returns {{ diagnosis: string,
18
+ * groups: Array<{ key:string, label:string,
19
+ * sources: Array<{ id, label, why, action:'upload'|'connect', status:'ready'|'planned' }> }> }}
20
+ */
21
+
22
+ // action 'upload' → we can ingest a file today (status ready). 'connect' → live OAuth we have
23
+ // NOT built (status planned). `universal:true` is always offered (the baseline cold-start fix).
24
+ const WORK_SOURCES = [
25
+ { id: "notes-docs", action: "upload", universal: true,
26
+ label: "Upload your key docs & notes",
27
+ why: "the docs, notes and trackers you already have, so Mover reads them instead of asking you to re-explain." },
28
+ { id: "clients-leads", action: "upload",
29
+ signals: ["client", "clients", "lead", "leads", "sales", "crm", "customer", "customers", "deal", "pipeline", "prospect"],
30
+ label: "Upload your leads / client list",
31
+ why: "your pipeline, so Mover tracks who's waiting on what." },
32
+ { id: "whatsapp", action: "upload", signals: ["whatsapp"],
33
+ label: "Upload a WhatsApp export",
34
+ why: "export a key chat. Mover pulls the decisions and commitments out of it." },
35
+ { id: "slack", action: "connect", signals: ["slack", "team", "teammate", "coworker", "co-worker", "channel"],
36
+ label: "Connect Slack",
37
+ why: "your team's back-and-forth, so Mover sees what's actually in flight." },
38
+ { id: "gdrive", action: "connect", signals: ["drive", "google doc", "gdoc", "docs", "document", "sheet", "spreadsheet"],
39
+ label: "Connect Google Drive",
40
+ why: "your working docs in one place, instead of scattered." },
41
+ { id: "calendar", action: "connect", signals: ["calendar", "meeting", "meetings", "schedule", "gcal", "appointment"],
42
+ label: "Connect your calendar",
43
+ why: "so /plan-tomorrow plans around your real day, not a blank one." },
44
+ ];
45
+
46
+ const VITALITY_SOURCES = [
47
+ { id: "health-export", action: "upload",
48
+ signals: ["gym", "fitness", "sleep", "health", "fitbit", "whoop", "oura", "workout", "training", "run", "running", "diet", "calorie", "calories", "steps", "exercise"],
49
+ label: "Upload a health export",
50
+ why: "your sleep/training data, so the battery and floors are real, not guessed." },
51
+ ];
52
+
53
+ const FAITH_SOURCES = [
54
+ { id: "faith-notes", action: "upload",
55
+ signals: ["faith", "prayer", "pray", "quran", "qur'an", "salah", "salat", "spiritual", "deen", "ibadah", "worship", "scripture", "church", "mosque", "masjid"],
56
+ label: "Add your practice / reflection notes",
57
+ why: "your reflections, so the faith domain isn't a placeholder." },
58
+ ];
59
+
60
+ const statusFor = (action) => (action === "upload" ? "ready" : "planned");
61
+
62
+ function profileBlob(profile) {
63
+ const parts = [];
64
+ const walk = (v) => {
65
+ if (v == null) return;
66
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
67
+ parts.push(String(v));
68
+ } else if (Array.isArray(v)) {
69
+ v.forEach(walk);
70
+ } else if (typeof v === "object") {
71
+ Object.values(v).forEach(walk);
72
+ }
73
+ };
74
+ for (const v of Object.values(profile || {})) {
75
+ walk(v);
76
+ }
77
+ return parts.join(" ").toLowerCase();
78
+ }
79
+
80
+ const matches = (text, signals) => (signals || []).some((s) => text.includes(s));
81
+
82
+ function pickSources(catalogue, text, cap) {
83
+ const out = [];
84
+ for (const src of catalogue) {
85
+ if (src.universal || matches(text, src.signals)) {
86
+ out.push({ id: src.id, label: src.label, why: src.why, action: src.action, status: statusFor(src.action) });
87
+ }
88
+ if (out.length >= cap) break;
89
+ }
90
+ return out;
91
+ }
92
+
93
+ function diagnose(profile) {
94
+ let work = String((profile && profile.work) || "").replace(/```[\s\S]*?```/g, "").trim();
95
+ // Strip a first-person lead-in ("I run …", "I'm building …", "my …") so the line reads
96
+ // cleanly after "You're running …" instead of echoing "You're running I run a marketing agency."
97
+ work = work.replace(
98
+ /^(i['’]?m\s+building\s+|i\s+am\s+building\s+|i['’]?m\s+running\s+|i\s+am\s+running\s+|i\s+run\s+|i\s+build\s+|i\s+do\s+|i\s+lead\s+|i\s+manage\s+|i\s+operate\s+|i\s+own\s+|i\s+work\s+on\s+|i['’]?m\s+a\s+|i\s+am\s+a\s+|i['’]?m\s+|i\s+am\s+|my\s+|we\s+|currently\s+)/i,
99
+ ""
100
+ ).trim();
101
+ if (work) {
102
+ const words = work.split(/\s+/);
103
+ const summary = words.slice(0, 14).join(" ") + (words.length > 14 ? "…" : "");
104
+ return `You're running ${summary}. Mover OS is sharpest once it can see your real work. ` +
105
+ `here's what to feed it first, so day one isn't a blank slate.`;
106
+ }
107
+ return "Mover OS gets sharper the more of your real work it can see. " +
108
+ "Here's what to feed it first so day one isn't a blank slate.";
109
+ }
110
+
111
+ function connectPlan(profile = {}) {
112
+ const text = profileBlob(profile);
113
+ const groups = [{ key: "work", label: "Work", sources: pickSources(WORK_SOURCES, text, 6) }];
114
+
115
+ const vitality = pickSources(VITALITY_SOURCES, text, 3);
116
+ if (vitality.length) groups.push({ key: "vitality", label: "Vitality", sources: vitality });
117
+
118
+ const faith = pickSources(FAITH_SOURCES, text, 3);
119
+ if (faith.length) groups.push({ key: "faith", label: "Faith", sources: faith });
120
+
121
+ // Total cap — overwhelm is the enemy. Trim from the LAST group inward; never drop a group's
122
+ // first source (Work's first is the universal upload, the one always-real action).
123
+ let total = groups.reduce((n, g) => n + g.sources.length, 0);
124
+ for (let i = groups.length - 1; i >= 0 && total > 8; i--) {
125
+ while (groups[i].sources.length > 1 && total > 8) { groups[i].sources.pop(); total--; }
126
+ }
127
+
128
+ return { diagnosis: diagnose(profile), groups };
129
+ }
130
+
131
+ module.exports = { connectPlan, WORK_SOURCES, VITALITY_SOURCES, FAITH_SOURCES };