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.
- package/install.js +763 -70
- 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,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 };
|