mover-os 4.7.7 → 4.7.9
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/README.md +34 -24
- package/install.js +2868 -251
- package/package.json +15 -3
- package/src/dashboard/build.js +1541 -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 +189 -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 +483 -0
- package/src/dashboard/lib/anti-identity-detector.js +116 -0
- package/src/dashboard/lib/approval-registry.js +170 -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 +228 -0
- package/src/dashboard/lib/date-utils.js +43 -0
- package/src/dashboard/lib/distribution-parser.js +137 -0
- package/src/dashboard/lib/dossier-parser.js +64 -0
- package/src/dashboard/lib/drift-history.js +88 -0
- package/src/dashboard/lib/drift-score.js +119 -0
- package/src/dashboard/lib/engine-default-fingerprints.json +53 -0
- package/src/dashboard/lib/engine-health.js +173 -0
- package/src/dashboard/lib/engine-writer.js +1831 -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 +427 -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 +226 -0
- package/src/dashboard/lib/library-indexer.js +105 -0
- package/src/dashboard/lib/library-search.js +290 -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 +519 -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/pid-markers.js +80 -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 +286 -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/state-core/backfill.js +298 -0
- package/src/dashboard/lib/state-core/dryrun.js +188 -0
- package/src/dashboard/lib/state-core/event-log.js +615 -0
- package/src/dashboard/lib/state-core/events.js +265 -0
- package/src/dashboard/lib/state-core/projections.js +376 -0
- package/src/dashboard/lib/state-core/start-close.js +162 -0
- package/src/dashboard/lib/state-core/trial.js +96 -0
- package/src/dashboard/lib/strategy-parser.js +248 -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 +371 -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 +2402 -0
- package/src/dashboard/shortcut.js +284 -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-ByVKPvLf.js +34 -0
- package/src/dashboard/ui/dist/assets/index-CCoKjUcC.js +161 -0
- package/src/dashboard/ui/dist/assets/index-CZWNQDt5.css +1 -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,418 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ingestion.js — D3 data ingestion for the Mover OS onboarding engine.
|
|
5
|
+
*
|
|
6
|
+
* Normalizes heterogeneous user-exported notes (Apple Notes export, Google Keep
|
|
7
|
+
* Takeout, Obsidian/Notion markdown, plain .txt/.md/.json/.html) into ONE compact
|
|
8
|
+
* text digest suitable for injection into an AgentSession as a framed context turn.
|
|
9
|
+
*
|
|
10
|
+
* DESIGN CONSTRAINTS (from onboarding-engine-SPEC.md §4 + §0):
|
|
11
|
+
* - Zero network calls — fully local, privacy-first.
|
|
12
|
+
* - Zero permissions required — user drags in their own export; we never read the fs.
|
|
13
|
+
* - Zero external dependencies — pure Node.js built-ins only.
|
|
14
|
+
* - Digest cap: ~6-8k chars total across all files, so it fits comfortably in the
|
|
15
|
+
* setup agent's context without flooding it and crowding out the interview itself.
|
|
16
|
+
*
|
|
17
|
+
* DEFERRED: Chrome-MCP auto-fetch, OAuth connectors, zip extraction, vault connector.
|
|
18
|
+
* See D3-ingestion/SPEC.md for the full deferred tail.
|
|
19
|
+
*
|
|
20
|
+
* Public API:
|
|
21
|
+
* buildDigest(files) → string
|
|
22
|
+
* normalizeFile(file) → string (exported for testing)
|
|
23
|
+
* SUPPORTED_TYPES → string[]
|
|
24
|
+
* DIGEST_CAP → number
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// ─── constants ────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/** Hard cap on total digest output (chars). 7k leaves breathing room inside 8k. */
|
|
30
|
+
const DIGEST_CAP = 7_000;
|
|
31
|
+
|
|
32
|
+
/** Per-file char budget before truncation — files share the cap equally up to this. */
|
|
33
|
+
const PER_FILE_CAP = 3_500;
|
|
34
|
+
|
|
35
|
+
/** File extensions we accept. Soft reject happens in the UI; this is the server guard. */
|
|
36
|
+
const SUPPORTED_TYPES = [".txt", ".md", ".json", ".html", ".htm"];
|
|
37
|
+
|
|
38
|
+
// ─── normalizers ──────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Strip HTML to plain text. Removes script/style blocks first (they contain
|
|
42
|
+
* no useful note content and are char-expensive), then removes all remaining
|
|
43
|
+
* tags, collapses whitespace, and trims.
|
|
44
|
+
*
|
|
45
|
+
* Handles Google Keep Takeout (.html files per note) and Apple Notes HTML export.
|
|
46
|
+
*/
|
|
47
|
+
function stripHtml(raw) {
|
|
48
|
+
// Remove <script>…</script> and <style>…</style> blocks (multiline).
|
|
49
|
+
let s = raw.replace(/<script[\s\S]*?<\/script>/gi, " ");
|
|
50
|
+
s = s.replace(/<style[\s\S]*?<\/style>/gi, " ");
|
|
51
|
+
// Remove HTML comments.
|
|
52
|
+
s = s.replace(/<!--[\s\S]*?-->/g, " ");
|
|
53
|
+
// Swap block-level close tags for newlines so paragraphs survive.
|
|
54
|
+
s = s.replace(/<\/(p|div|li|h[1-6]|br|tr|td|th|blockquote|section|article|header|footer|nav|main)>/gi, "\n");
|
|
55
|
+
s = s.replace(/<br\s*\/?>/gi, "\n");
|
|
56
|
+
// Strip all remaining tags.
|
|
57
|
+
s = s.replace(/<[^>]+>/g, " ");
|
|
58
|
+
// Decode the most common HTML entities.
|
|
59
|
+
s = s.replace(/&/g, "&")
|
|
60
|
+
.replace(/</g, "<")
|
|
61
|
+
.replace(/>/g, ">")
|
|
62
|
+
.replace(/"/g, '"')
|
|
63
|
+
.replace(/'/g, "'")
|
|
64
|
+
.replace(/ /g, " ")
|
|
65
|
+
.replace(/—/g, "—")
|
|
66
|
+
.replace(/–/g, "–")
|
|
67
|
+
.replace(/…/g, "…");
|
|
68
|
+
// Collapse runs of whitespace (preserve single newlines for paragraph breaks).
|
|
69
|
+
s = s.replace(/[ \t]+/g, " ");
|
|
70
|
+
s = s.replace(/\n[ \t]+/g, "\n");
|
|
71
|
+
s = s.replace(/\n{3,}/g, "\n\n");
|
|
72
|
+
return s.trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Normalize a JSON file. Rather than dumping the raw structure (which is often
|
|
77
|
+
* deeply nested and char-expensive), we flatten it to a bullet list of
|
|
78
|
+
* "key: value" pairs for scalar leaves. Arrays become an item count + sample.
|
|
79
|
+
* This works well for Google Keep Takeout (array of note objects).
|
|
80
|
+
*/
|
|
81
|
+
function normalizeJson(raw) {
|
|
82
|
+
let parsed;
|
|
83
|
+
try { parsed = JSON.parse(raw); }
|
|
84
|
+
catch (_) { return raw.slice(0, PER_FILE_CAP); } // unparseable — return raw excerpt
|
|
85
|
+
|
|
86
|
+
const lines = [];
|
|
87
|
+
|
|
88
|
+
function walk(obj, prefix, depth) {
|
|
89
|
+
if (depth > 4) return; // don't recurse into deeply nested structures
|
|
90
|
+
if (Array.isArray(obj)) {
|
|
91
|
+
lines.push(`${prefix}[${obj.length} items]`);
|
|
92
|
+
// Sample up to 3 items for context.
|
|
93
|
+
const sample = obj.slice(0, 3);
|
|
94
|
+
sample.forEach((item, i) => walk(item, `${prefix}[${i}] `, depth + 1));
|
|
95
|
+
if (obj.length > 3) lines.push(`${prefix}… and ${obj.length - 3} more`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (obj !== null && typeof obj === "object") {
|
|
99
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
100
|
+
walk(v, prefix ? `${prefix}.${k}` : k, depth + 1);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Scalar — emit as "key: value"
|
|
105
|
+
const val = String(obj);
|
|
106
|
+
if (val.length > 300) {
|
|
107
|
+
lines.push(`${prefix}: ${val.slice(0, 300)}…`);
|
|
108
|
+
} else if (val.trim()) {
|
|
109
|
+
lines.push(`${prefix}: ${val}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
walk(parsed, "", 0);
|
|
114
|
+
return lines.join("\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Light Markdown normalization. Strips front-matter (---…---), removes image
|
|
119
|
+
* syntax (), collapses code blocks to a one-liner stub, and normalises
|
|
120
|
+
* whitespace. Preserves prose and headings — that's what the agent needs.
|
|
121
|
+
*/
|
|
122
|
+
function normalizeMd(raw) {
|
|
123
|
+
let s = raw;
|
|
124
|
+
// Strip YAML front-matter block.
|
|
125
|
+
s = s.replace(/^---[\s\S]*?---\n?/, "");
|
|
126
|
+
// Collapse code fences to a stub.
|
|
127
|
+
s = s.replace(/```[\s\S]*?```/g, "[code block]");
|
|
128
|
+
s = s.replace(/`[^`\n]+`/g, (m) => m.slice(1, -1)); // inline code → plain text
|
|
129
|
+
// Remove image syntax.
|
|
130
|
+
s = s.replace(/!\[([^\]]*)\]\([^)]*\)/g, (_, alt) => alt ? `[image: ${alt}]` : "[image]");
|
|
131
|
+
// Preserve link text, drop URL.
|
|
132
|
+
s = s.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
133
|
+
// Wikilinks — keep the display part.
|
|
134
|
+
s = s.replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, "$2");
|
|
135
|
+
s = s.replace(/\[\[([^\]]+)\]\]/g, "$1");
|
|
136
|
+
// Collapse checkbox markers.
|
|
137
|
+
s = s.replace(/^- \[[ xX]\] /gm, "- ");
|
|
138
|
+
// Collapse multiple blank lines.
|
|
139
|
+
s = s.replace(/\n{3,}/g, "\n\n");
|
|
140
|
+
return s.trim();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── file-type dispatch ───────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Detect a file's effective type from its name.
|
|
147
|
+
* Returns one of: "html" | "json" | "md" | "txt" | "unsupported"
|
|
148
|
+
*/
|
|
149
|
+
function detectType(name) {
|
|
150
|
+
if (!name) return "unsupported";
|
|
151
|
+
const n = name.toLowerCase();
|
|
152
|
+
if (n.endsWith(".html") || n.endsWith(".htm")) return "html";
|
|
153
|
+
if (n.endsWith(".json")) return "json";
|
|
154
|
+
if (n.endsWith(".md") || n.endsWith(".markdown")) return "md";
|
|
155
|
+
if (n.endsWith(".txt")) return "txt";
|
|
156
|
+
return "unsupported";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Normalize a single file to plain text.
|
|
161
|
+
*
|
|
162
|
+
* @param {{ name: string, content: string }} file
|
|
163
|
+
* `name` — original filename (used for type detection + section header)
|
|
164
|
+
* `content` — raw string content of the file
|
|
165
|
+
* @returns {string} Normalized plain text, not yet capped.
|
|
166
|
+
*/
|
|
167
|
+
function normalizeFile(file) {
|
|
168
|
+
if (!file || typeof file.content !== "string") return "";
|
|
169
|
+
const type = detectType(file.name || "");
|
|
170
|
+
switch (type) {
|
|
171
|
+
case "html": return stripHtml(file.content);
|
|
172
|
+
case "json": return normalizeJson(file.content);
|
|
173
|
+
case "md": return normalizeMd(file.content);
|
|
174
|
+
case "txt": return file.content.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
175
|
+
default: return ""; // unsupported type — skip silently
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── digest builder ───────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Build a compact, labeled digest from a list of uploaded files.
|
|
183
|
+
*
|
|
184
|
+
* The digest format is:
|
|
185
|
+
* --- [filename] (type) ---
|
|
186
|
+
* <normalized content, capped to PER_FILE_CAP chars>
|
|
187
|
+
* (truncated) ← only if truncated
|
|
188
|
+
*
|
|
189
|
+
* Total output is capped at DIGEST_CAP chars. When the cap is hit,
|
|
190
|
+
* remaining files get a "[omitted — digest cap reached]" stub so the
|
|
191
|
+
* agent knows there was more content rather than silently missing it.
|
|
192
|
+
*
|
|
193
|
+
* @param {Array<{ name: string, content: string }>} files
|
|
194
|
+
* @returns {string} The assembled digest, ready for AgentSession.send().
|
|
195
|
+
*/
|
|
196
|
+
function buildDigest(files) {
|
|
197
|
+
if (!Array.isArray(files) || files.length === 0) return "";
|
|
198
|
+
|
|
199
|
+
// MF-4: bound the file count. The 2MB request body allows tens of thousands of
|
|
200
|
+
// tiny file objects; without this, unsupported/over-cap stubs could still blow
|
|
201
|
+
// up the sections array far past DIGEST_CAP. 50 is generous for a real import.
|
|
202
|
+
if (files.length > 50) files = files.slice(0, 50);
|
|
203
|
+
|
|
204
|
+
const sections = [];
|
|
205
|
+
let totalChars = 0;
|
|
206
|
+
|
|
207
|
+
for (const file of files) {
|
|
208
|
+
const name = String(file.name || "unknown");
|
|
209
|
+
const type = detectType(name);
|
|
210
|
+
|
|
211
|
+
if (type === "unsupported") {
|
|
212
|
+
const stub = `--- [${name}] (unsupported — skipped) ---`;
|
|
213
|
+
sections.push(stub);
|
|
214
|
+
totalChars += stub.length; // MF-4: stubs count toward the cap too
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (totalChars >= DIGEST_CAP) {
|
|
219
|
+
sections.push(`--- [${name}] (${type}) [omitted — digest cap reached] ---`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let normalized = normalizeFile(file);
|
|
224
|
+
|
|
225
|
+
// Per-file cap — truncate at a word boundary if possible.
|
|
226
|
+
if (normalized.length > PER_FILE_CAP) {
|
|
227
|
+
const cutoff = normalized.lastIndexOf(" ", PER_FILE_CAP);
|
|
228
|
+
normalized = normalized.slice(0, cutoff > 0 ? cutoff : PER_FILE_CAP);
|
|
229
|
+
normalized += "\n(truncated)";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Enforce remaining budget within the total cap. MF-4 part A: account for
|
|
233
|
+
// the header length so the assembled section can't overshoot DIGEST_CAP.
|
|
234
|
+
const header = `--- [${name}] (${type}) ---\n`;
|
|
235
|
+
const remaining = DIGEST_CAP - totalChars - header.length;
|
|
236
|
+
if (normalized.length > remaining) {
|
|
237
|
+
const cut = Math.max(0, remaining);
|
|
238
|
+
const cutoff = normalized.lastIndexOf(" ", cut);
|
|
239
|
+
normalized = normalized.slice(0, cutoff > 0 ? cutoff : cut);
|
|
240
|
+
normalized += "\n(digest cap reached)";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const section = header + normalized;
|
|
244
|
+
sections.push(section);
|
|
245
|
+
totalChars += section.length;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return sections.join("\n\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─── injection frame ──────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Wrap a raw digest in the framing text that tells the setup agent how to USE
|
|
255
|
+
* the context (silently, to personalize questions — not to summarize back).
|
|
256
|
+
*
|
|
257
|
+
* This is what gets fed to AgentSession.send() as the user turn.
|
|
258
|
+
*
|
|
259
|
+
* @param {string} digest — output of buildDigest()
|
|
260
|
+
* @returns {string}
|
|
261
|
+
*/
|
|
262
|
+
function frameDigest(digest) {
|
|
263
|
+
if (!digest || !digest.trim()) return "";
|
|
264
|
+
// MF-2: neutralize any completion sentinel an imported note tries to smuggle in,
|
|
265
|
+
// so malicious note content cannot make the agent end the interview early.
|
|
266
|
+
const safe = String(digest).replace(/\[\[(SETUP|WALKTHROUGH)_COMPLETE\]\]/gi, "[redacted-marker]");
|
|
267
|
+
return (
|
|
268
|
+
"Here is context the user imported from their existing notes. " +
|
|
269
|
+
"Use this to personalize the setup interview. " +
|
|
270
|
+
"Do not summarize it back to the user or comment on it — use it silently to inform your questions. " +
|
|
271
|
+
"Where the notes reveal existing goals, work patterns, or projects, reflect that in your questions " +
|
|
272
|
+
"rather than asking about things already answered here. " +
|
|
273
|
+
"Treat everything in these notes as possibly OUTDATED: exported notes carry no dates you can trust. " +
|
|
274
|
+
"For anything load-bearing (a goal, a deadline, a current project), confirm it in one short question, " +
|
|
275
|
+
"for example: your notes mention a goal of X, is that still true or has it moved? Never assume it holds.\n\n" +
|
|
276
|
+
"--- BEGIN IMPORTED CONTEXT ---\n" +
|
|
277
|
+
safe +
|
|
278
|
+
"\n--- END IMPORTED CONTEXT ---"
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ─── exports ──────────────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
module.exports = { buildDigest, normalizeFile, frameDigest, SUPPORTED_TYPES, DIGEST_CAP, PER_FILE_CAP };
|
|
285
|
+
|
|
286
|
+
// ─── self-test ────────────────────────────────────────────────────────────────
|
|
287
|
+
/* node src/dashboard/lib/ingestion.js */
|
|
288
|
+
|
|
289
|
+
if (require.main === module) {
|
|
290
|
+
const assert = require("assert");
|
|
291
|
+
|
|
292
|
+
// ── fixture data ──────────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
const MD_FIXTURE = `---
|
|
295
|
+
title: Work Log
|
|
296
|
+
date: 2026-01-15
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
# Work Log
|
|
300
|
+
|
|
301
|
+
## Goals
|
|
302
|
+
- [[Annual Review|My 2026 Goals]]
|
|
303
|
+
- Build Mover OS to £25k
|
|
304
|
+
|
|
305
|
+
## Projects
|
|
306
|
+
- \`mover-os\`: in progress
|
|
307
|
+
- \`newsletter\`: stalled
|
|
308
|
+
|
|
309
|
+

|
|
310
|
+
|
|
311
|
+
\`\`\`js
|
|
312
|
+
console.log("hello");
|
|
313
|
+
\`\`\`
|
|
314
|
+
|
|
315
|
+
Some prose about my weekly habits.
|
|
316
|
+
`;
|
|
317
|
+
|
|
318
|
+
const JSON_FIXTURE = JSON.stringify({
|
|
319
|
+
kind: "notes#notesList",
|
|
320
|
+
items: [
|
|
321
|
+
{ title: "Q1 Plan", textContent: "Ship Mover OS. Get first 10 users.", labels: [{ name: "work" }] },
|
|
322
|
+
{ title: "Reading list", textContent: "Thinking Fast and Slow. Zero to One.", labels: [{ name: "books" }] },
|
|
323
|
+
{ title: "Empty note", textContent: "" },
|
|
324
|
+
],
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const HTML_FIXTURE = `<!DOCTYPE html><html><head><title>Note</title>
|
|
328
|
+
<style>body { font-family: sans-serif; }</style>
|
|
329
|
+
<script>var x=1;</script>
|
|
330
|
+
</head><body>
|
|
331
|
+
<div class="note">
|
|
332
|
+
<h1>My Apple Note</h1>
|
|
333
|
+
<p>This is the content of my note. It has & symbols <like this>.</p>
|
|
334
|
+
<ul><li>Task one</li><li>Task two</li></ul>
|
|
335
|
+
</div></body></html>`;
|
|
336
|
+
|
|
337
|
+
const TXT_FIXTURE = "Just some plain text.\n\nWith multiple paragraphs.\n\nAnd a third one.";
|
|
338
|
+
|
|
339
|
+
// ── test: normalizeFile ───────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
const mdNorm = normalizeFile({ name: "work.md", content: MD_FIXTURE });
|
|
342
|
+
assert.ok(!mdNorm.includes("---"), "md: front-matter stripped");
|
|
343
|
+
assert.ok(mdNorm.includes("Build Mover OS"), "md: prose preserved");
|
|
344
|
+
assert.ok(!mdNorm.includes("console.log"), "md: code block removed");
|
|
345
|
+
assert.ok(!mdNorm.includes("](image.png)"), "md: image markdown syntax removed");
|
|
346
|
+
assert.ok(mdNorm.includes("My 2026 Goals"), "md: wikilink display alias kept");
|
|
347
|
+
console.log("normalizeFile md: OK");
|
|
348
|
+
|
|
349
|
+
const jsonNorm = normalizeFile({ name: "keep.json", content: JSON_FIXTURE });
|
|
350
|
+
assert.ok(jsonNorm.includes("Q1 Plan"), "json: title value present");
|
|
351
|
+
assert.ok(jsonNorm.includes("Ship Mover OS"), "json: textContent present");
|
|
352
|
+
console.log("normalizeFile json: OK");
|
|
353
|
+
|
|
354
|
+
const htmlNorm = normalizeFile({ name: "note.html", content: HTML_FIXTURE });
|
|
355
|
+
assert.ok(!htmlNorm.includes("<html>"), "html: tags stripped");
|
|
356
|
+
assert.ok(!htmlNorm.includes("font-family"), "html: style block removed");
|
|
357
|
+
assert.ok(!htmlNorm.includes("var x"), "html: script block removed");
|
|
358
|
+
assert.ok(htmlNorm.includes("Apple Note"), "html: heading text kept");
|
|
359
|
+
assert.ok(htmlNorm.includes("& symbols <like this>"), "html: entities decoded");
|
|
360
|
+
console.log("normalizeFile html: OK");
|
|
361
|
+
|
|
362
|
+
const txtNorm = normalizeFile({ name: "notes.txt", content: TXT_FIXTURE });
|
|
363
|
+
assert.ok(txtNorm.includes("plain text"), "txt: content preserved");
|
|
364
|
+
console.log("normalizeFile txt: OK");
|
|
365
|
+
|
|
366
|
+
const unsupp = normalizeFile({ name: "data.xlsx", content: "binary" });
|
|
367
|
+
assert.strictEqual(unsupp, "", "unsupported type → empty string");
|
|
368
|
+
console.log("normalizeFile unsupported: OK");
|
|
369
|
+
|
|
370
|
+
// ── test: buildDigest ─────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
const files = [
|
|
373
|
+
{ name: "work.md", content: MD_FIXTURE },
|
|
374
|
+
{ name: "keep.json", content: JSON_FIXTURE },
|
|
375
|
+
{ name: "note.html", content: HTML_FIXTURE },
|
|
376
|
+
{ name: "notes.txt", content: TXT_FIXTURE },
|
|
377
|
+
{ name: "ignored.xlsx", content: "binary" },
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
const digest = buildDigest(files);
|
|
381
|
+
assert.ok(digest.length <= DIGEST_CAP, `digest within cap: ${digest.length} <= ${DIGEST_CAP}`);
|
|
382
|
+
assert.ok(digest.includes("--- [work.md] (md) ---"), "digest: md section header present");
|
|
383
|
+
assert.ok(digest.includes("--- [keep.json] (json) ---"), "digest: json section header present");
|
|
384
|
+
assert.ok(digest.includes("--- [note.html] (html) ---"), "digest: html section header present");
|
|
385
|
+
assert.ok(digest.includes("--- [notes.txt] (txt) ---"), "digest: txt section header present");
|
|
386
|
+
assert.ok(digest.includes("--- [ignored.xlsx] (unsupported — skipped) ---"), "digest: unsupported flagged");
|
|
387
|
+
console.log(`buildDigest: OK (${digest.length} chars)`);
|
|
388
|
+
|
|
389
|
+
// ── test: per-file cap ────────────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
const bigContent = "word ".repeat(5_000); // ~25k chars
|
|
392
|
+
const bigDigest = buildDigest([{ name: "big.txt", content: bigContent }]);
|
|
393
|
+
assert.ok(bigDigest.length <= DIGEST_CAP + 200, `big file capped: ${bigDigest.length}`);
|
|
394
|
+
assert.ok(bigDigest.includes("(truncated)") || bigDigest.includes("(digest cap"), "big file truncation noted");
|
|
395
|
+
console.log("per-file cap: OK");
|
|
396
|
+
|
|
397
|
+
// ── test: frameDigest ────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
const framed = frameDigest(digest);
|
|
400
|
+
assert.ok(framed.includes("--- BEGIN IMPORTED CONTEXT ---"), "framed: opening marker");
|
|
401
|
+
assert.ok(framed.includes("--- END IMPORTED CONTEXT ---"), "framed: closing marker");
|
|
402
|
+
assert.ok(framed.includes("silently"), "framed: silent-use instruction present");
|
|
403
|
+
console.log("frameDigest: OK");
|
|
404
|
+
|
|
405
|
+
// ── test: empty inputs ────────────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
assert.strictEqual(buildDigest([]), "", "empty array → empty string");
|
|
408
|
+
assert.strictEqual(buildDigest(null), "", "null → empty string");
|
|
409
|
+
assert.strictEqual(frameDigest(""), "", "empty digest → empty frame");
|
|
410
|
+
console.log("edge cases: OK");
|
|
411
|
+
|
|
412
|
+
// ── print sample output ───────────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
console.log("\n─── SAMPLE FRAMED DIGEST ───────────────────────────────────────────");
|
|
415
|
+
console.log(framed.slice(0, 1_200) + (framed.length > 1_200 ? "\n[…truncated for display]" : ""));
|
|
416
|
+
console.log("────────────────────────────────────────────────────────────────────");
|
|
417
|
+
console.log("\nAll tests passed.");
|
|
418
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const {
|
|
5
|
+
archivesDir,
|
|
6
|
+
engineDir,
|
|
7
|
+
libraryDir,
|
|
8
|
+
libraryCheatsheetsDir,
|
|
9
|
+
libraryPrinciplesDir,
|
|
10
|
+
librarySopsDir,
|
|
11
|
+
libraryEntitiesPeopleDir,
|
|
12
|
+
libraryEntitiesOrgsDir,
|
|
13
|
+
libraryEntitiesPlacesDir,
|
|
14
|
+
libraryEntitiesDecisionsDir,
|
|
15
|
+
libraryInputsDir,
|
|
16
|
+
libraryScriptsDir,
|
|
17
|
+
libraryMocsDir,
|
|
18
|
+
safeListDir,
|
|
19
|
+
safeRead,
|
|
20
|
+
safeStat,
|
|
21
|
+
exists,
|
|
22
|
+
} = require("./paths");
|
|
23
|
+
|
|
24
|
+
// library-indexer-v2 — full file inventory of 03_Library/* with metadata.
|
|
25
|
+
//
|
|
26
|
+
// Replaces library-indexer.js for the v2 dashboard:
|
|
27
|
+
// - Includes file PATHS so clicking can openFile() in Obsidian.
|
|
28
|
+
// - Parses YAML frontmatter (title, tags, type, status).
|
|
29
|
+
// - Extracts first paragraph (200 chars) as a list preview.
|
|
30
|
+
// - Pulls wiki-links to surface backlinks.
|
|
31
|
+
//
|
|
32
|
+
// Output shape:
|
|
33
|
+
// {
|
|
34
|
+
// available: true,
|
|
35
|
+
// totalCount,
|
|
36
|
+
// sections: {
|
|
37
|
+
// mocs: { count, items: [LibraryItem] },
|
|
38
|
+
// principles: ...,
|
|
39
|
+
// cheatsheets: ...,
|
|
40
|
+
// sops: ...,
|
|
41
|
+
// scripts: ...,
|
|
42
|
+
// inputs: { count, byType: { Articles: [], Books: [], Videos: [] } },
|
|
43
|
+
// entities: { count, people: [], orgs: [], places: [], decisions: [] }
|
|
44
|
+
// }
|
|
45
|
+
// }
|
|
46
|
+
|
|
47
|
+
function parseFrontmatter(content) {
|
|
48
|
+
if (!content) return {};
|
|
49
|
+
const m = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
50
|
+
if (!m) return {};
|
|
51
|
+
const body = m[1];
|
|
52
|
+
const out = {};
|
|
53
|
+
body.split(/\r?\n/).forEach(line => {
|
|
54
|
+
const km = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
55
|
+
if (km) {
|
|
56
|
+
const key = km[1].trim();
|
|
57
|
+
let val = km[2].trim();
|
|
58
|
+
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
59
|
+
(val.startsWith("'") && val.endsWith("'"))) {
|
|
60
|
+
val = val.slice(1, -1);
|
|
61
|
+
}
|
|
62
|
+
out[key] = val;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function stripFrontmatter(content) {
|
|
69
|
+
if (!content) return "";
|
|
70
|
+
return content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, "");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function firstParagraph(content) {
|
|
74
|
+
const body = stripFrontmatter(content);
|
|
75
|
+
// Skip headings and quote blocks; find first prose paragraph.
|
|
76
|
+
const paragraphs = body.split(/\n\s*\n/);
|
|
77
|
+
for (const para of paragraphs) {
|
|
78
|
+
const trimmed = para.trim();
|
|
79
|
+
if (!trimmed) continue;
|
|
80
|
+
if (trimmed.startsWith("#")) continue;
|
|
81
|
+
if (trimmed.startsWith(">")) continue;
|
|
82
|
+
if (trimmed.startsWith("|")) continue;
|
|
83
|
+
if (trimmed.startsWith("-") || trimmed.startsWith("*")) continue;
|
|
84
|
+
// Strip markdown bold/italic/links for a cleaner preview
|
|
85
|
+
const clean = trimmed
|
|
86
|
+
.replace(/\[\[([^\|\]]+)(?:\|[^\]]+)?\]\]/g, "$1")
|
|
87
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
88
|
+
.replace(/[*_`]/g, "")
|
|
89
|
+
.replace(/\s+/g, " ")
|
|
90
|
+
.trim();
|
|
91
|
+
return clean.slice(0, 200);
|
|
92
|
+
}
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function extractWikilinks(content) {
|
|
97
|
+
if (!content) return [];
|
|
98
|
+
const links = [];
|
|
99
|
+
const re = /\[\[([^\|\]]+)(?:\|[^\]]+)?\]\]/g;
|
|
100
|
+
let m;
|
|
101
|
+
while ((m = re.exec(content)) !== null) {
|
|
102
|
+
links.push(m[1].trim());
|
|
103
|
+
}
|
|
104
|
+
return Array.from(new Set(links)).slice(0, 20);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readItem(filePath, name) {
|
|
108
|
+
const stat = safeStat(filePath);
|
|
109
|
+
if (!stat) return null;
|
|
110
|
+
const content = safeRead(filePath);
|
|
111
|
+
const fm = parseFrontmatter(content);
|
|
112
|
+
return {
|
|
113
|
+
name: name.replace(/\.md$/, ""),
|
|
114
|
+
path: filePath,
|
|
115
|
+
mtime: stat.mtime.getTime(),
|
|
116
|
+
ageDays: Math.floor((Date.now() - stat.mtime.getTime()) / 86400000),
|
|
117
|
+
frontmatter: fm,
|
|
118
|
+
firstParagraph: firstParagraph(content),
|
|
119
|
+
wikilinks: extractWikilinks(content),
|
|
120
|
+
size: stat.size,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function walkAndIndex(dir, depth = 0) {
|
|
125
|
+
if (depth > 3) return [];
|
|
126
|
+
const out = [];
|
|
127
|
+
const entries = safeListDir(dir);
|
|
128
|
+
for (const e of entries) {
|
|
129
|
+
if (e.isFile && e.name.endsWith(".md") && !e.name.startsWith("_")) {
|
|
130
|
+
const item = readItem(e.path, e.name);
|
|
131
|
+
if (item) out.push(item);
|
|
132
|
+
} else if (e.isDir && !e.name.startsWith("_") && !e.name.startsWith(".")) {
|
|
133
|
+
out.push(...walkAndIndex(e.path, depth + 1));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function sortByMtimeDesc(items) {
|
|
140
|
+
return [...items].sort((a, b) => b.mtime - a.mtime);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function indexSection(dir) {
|
|
144
|
+
if (!exists(dir)) return { count: 0, items: [] };
|
|
145
|
+
const items = sortByMtimeDesc(walkAndIndex(dir));
|
|
146
|
+
return { count: items.length, items };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function indexLibraryV2(vault) {
|
|
150
|
+
if (!exists(libraryDir(vault))) return { available: false };
|
|
151
|
+
|
|
152
|
+
const mocs = indexSection(libraryMocsDir(vault));
|
|
153
|
+
const principles = indexSection(libraryPrinciplesDir(vault));
|
|
154
|
+
const cheatsheets = indexSection(libraryCheatsheetsDir(vault));
|
|
155
|
+
const sops = indexSection(librarySopsDir(vault));
|
|
156
|
+
const scripts = indexSection(libraryScriptsDir(vault));
|
|
157
|
+
|
|
158
|
+
// Inputs split by sub-type
|
|
159
|
+
const inputs = { count: 0, byType: {} };
|
|
160
|
+
if (exists(libraryInputsDir(vault))) {
|
|
161
|
+
const inputSubs = safeListDir(libraryInputsDir(vault), { dirsOnly: true });
|
|
162
|
+
for (const sub of inputSubs) {
|
|
163
|
+
if (sub.name.startsWith("_") || sub.name.startsWith(".")) continue;
|
|
164
|
+
const items = sortByMtimeDesc(walkAndIndex(sub.path));
|
|
165
|
+
inputs.byType[sub.name] = items;
|
|
166
|
+
inputs.count += items.length;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Entities split by sub-type
|
|
171
|
+
const people = indexSection(libraryEntitiesPeopleDir(vault));
|
|
172
|
+
const orgs = indexSection(libraryEntitiesOrgsDir(vault));
|
|
173
|
+
const places = indexSection(libraryEntitiesPlacesDir(vault));
|
|
174
|
+
const decisions = indexSection(libraryEntitiesDecisionsDir(vault));
|
|
175
|
+
const entities = {
|
|
176
|
+
count: people.count + orgs.count + places.count + decisions.count,
|
|
177
|
+
people: people.items,
|
|
178
|
+
orgs: orgs.items,
|
|
179
|
+
places: places.items,
|
|
180
|
+
decisions: decisions.items,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Archives: count-only, top-level entries of 04_Archives (dirs + files).
|
|
184
|
+
// Deliberately NOT walked: backup trees in there would pollute search and
|
|
185
|
+
// cost a deep walk. The count is all the Library shelf needs. Not added to
|
|
186
|
+
// totalCount (that stays "indexed" files; archives are counted, not indexed).
|
|
187
|
+
const archives = { count: 0 };
|
|
188
|
+
if (exists(archivesDir(vault))) {
|
|
189
|
+
archives.count = safeListDir(archivesDir(vault))
|
|
190
|
+
.filter((e) => !e.name.startsWith(".") && !e.name.startsWith("_")).length;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Engine: count-only, top-level FILES of 02_Areas/Engine (subdirs like
|
|
194
|
+
// Dailies/ and State/ are their own worlds; the Library shelf browse lists
|
|
195
|
+
// exactly these files). Replaces a hardcoded 12 in the UI mapper — terra
|
|
196
|
+
// confirmed it fabricated (real vault held 15) and it contaminated the
|
|
197
|
+
// "indexed" headline sum (2026-07-12). Not added to totalCount, same
|
|
198
|
+
// reasoning as archives.
|
|
199
|
+
const engine = { count: 0 };
|
|
200
|
+
if (exists(engineDir(vault))) {
|
|
201
|
+
engine.count = safeListDir(engineDir(vault))
|
|
202
|
+
.filter((e) => e.isFile && !e.name.startsWith(".")).length;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const totalCount =
|
|
206
|
+
mocs.count + principles.count + cheatsheets.count + sops.count +
|
|
207
|
+
scripts.count + inputs.count + entities.count;
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
available: true,
|
|
211
|
+
totalCount,
|
|
212
|
+
sections: {
|
|
213
|
+
mocs,
|
|
214
|
+
principles,
|
|
215
|
+
cheatsheets,
|
|
216
|
+
sops,
|
|
217
|
+
scripts,
|
|
218
|
+
inputs,
|
|
219
|
+
entities,
|
|
220
|
+
archives,
|
|
221
|
+
engine,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { indexLibraryV2 };
|