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.
Files changed (119) hide show
  1. package/README.md +34 -24
  2. package/install.js +2868 -251
  3. package/package.json +15 -3
  4. package/src/dashboard/build.js +1541 -0
  5. package/src/dashboard/dashboard.js +276 -0
  6. package/src/dashboard/index.js +319 -0
  7. package/src/dashboard/lib/activation-log.js +297 -0
  8. package/src/dashboard/lib/active-context-parser.js +189 -0
  9. package/src/dashboard/lib/agent-command.js +93 -0
  10. package/src/dashboard/lib/agent-detect.js +255 -0
  11. package/src/dashboard/lib/agent-detector.js +92 -0
  12. package/src/dashboard/lib/agent-session.js +483 -0
  13. package/src/dashboard/lib/anti-identity-detector.js +116 -0
  14. package/src/dashboard/lib/approval-registry.js +170 -0
  15. package/src/dashboard/lib/auto-learnings-parser.js +183 -0
  16. package/src/dashboard/lib/cli-usage-parser.js +92 -0
  17. package/src/dashboard/lib/config-parser.js +109 -0
  18. package/src/dashboard/lib/connect-recommender.js +131 -0
  19. package/src/dashboard/lib/correlations-parser.js +231 -0
  20. package/src/dashboard/lib/daily-note-resolver.js +228 -0
  21. package/src/dashboard/lib/date-utils.js +43 -0
  22. package/src/dashboard/lib/distribution-parser.js +137 -0
  23. package/src/dashboard/lib/dossier-parser.js +64 -0
  24. package/src/dashboard/lib/drift-history.js +88 -0
  25. package/src/dashboard/lib/drift-score.js +119 -0
  26. package/src/dashboard/lib/engine-default-fingerprints.json +53 -0
  27. package/src/dashboard/lib/engine-health.js +173 -0
  28. package/src/dashboard/lib/engine-writer.js +1831 -0
  29. package/src/dashboard/lib/execution-plan.js +125 -0
  30. package/src/dashboard/lib/experiments-parser.js +429 -0
  31. package/src/dashboard/lib/feed-parser.js +294 -0
  32. package/src/dashboard/lib/forked-future.js +60 -0
  33. package/src/dashboard/lib/goal-forecast.js +427 -0
  34. package/src/dashboard/lib/goals-parser.js +67 -0
  35. package/src/dashboard/lib/health-protocols-parser.js +133 -0
  36. package/src/dashboard/lib/hook-activity.js +48 -0
  37. package/src/dashboard/lib/hook-indexer.js +169 -0
  38. package/src/dashboard/lib/hourly-activity-parser.js +143 -0
  39. package/src/dashboard/lib/identity-parser.js +85 -0
  40. package/src/dashboard/lib/ingestion.js +418 -0
  41. package/src/dashboard/lib/library-indexer-v2.js +226 -0
  42. package/src/dashboard/lib/library-indexer.js +105 -0
  43. package/src/dashboard/lib/library-search.js +290 -0
  44. package/src/dashboard/lib/log-activation.sh +61 -0
  45. package/src/dashboard/lib/memory-curator.js +97 -0
  46. package/src/dashboard/lib/memory-gardener.js +177 -0
  47. package/src/dashboard/lib/memory-gepa.js +102 -0
  48. package/src/dashboard/lib/memory-index.js +519 -0
  49. package/src/dashboard/lib/memory-rerank.js +72 -0
  50. package/src/dashboard/lib/memory-text.js +136 -0
  51. package/src/dashboard/lib/metrics-log-parser.js +76 -0
  52. package/src/dashboard/lib/moves-usage-parser.js +184 -0
  53. package/src/dashboard/lib/onboarding-forge.js +70 -0
  54. package/src/dashboard/lib/override-outcome-parser.js +68 -0
  55. package/src/dashboard/lib/override-summary.js +73 -0
  56. package/src/dashboard/lib/paths.js +192 -0
  57. package/src/dashboard/lib/pattern-manifest-loader.js +29 -0
  58. package/src/dashboard/lib/phantom-strategy.js +129 -0
  59. package/src/dashboard/lib/pid-markers.js +80 -0
  60. package/src/dashboard/lib/project-scanner.js +121 -0
  61. package/src/dashboard/lib/promise-wall.js +88 -0
  62. package/src/dashboard/lib/record-score.js +173 -0
  63. package/src/dashboard/lib/redaction.js +140 -0
  64. package/src/dashboard/lib/refusal-parser.js +44 -0
  65. package/src/dashboard/lib/regenerate-manifest.js +206 -0
  66. package/src/dashboard/lib/rewind-snapshots.js +689 -0
  67. package/src/dashboard/lib/roast-wall-parser.js +200 -0
  68. package/src/dashboard/lib/run-registry.js +286 -0
  69. package/src/dashboard/lib/safe-write.js +63 -0
  70. package/src/dashboard/lib/session-log-parser.js +145 -0
  71. package/src/dashboard/lib/session-time-parser.js +158 -0
  72. package/src/dashboard/lib/skill-index.js +171 -0
  73. package/src/dashboard/lib/skill-indexer.js +118 -0
  74. package/src/dashboard/lib/skill-recommender.js +689 -0
  75. package/src/dashboard/lib/state-core/backfill.js +298 -0
  76. package/src/dashboard/lib/state-core/dryrun.js +188 -0
  77. package/src/dashboard/lib/state-core/event-log.js +615 -0
  78. package/src/dashboard/lib/state-core/events.js +265 -0
  79. package/src/dashboard/lib/state-core/projections.js +376 -0
  80. package/src/dashboard/lib/state-core/start-close.js +162 -0
  81. package/src/dashboard/lib/state-core/trial.js +96 -0
  82. package/src/dashboard/lib/strategy-parser.js +248 -0
  83. package/src/dashboard/lib/strategy-protocol-parser.js +135 -0
  84. package/src/dashboard/lib/streak-parser.js +95 -0
  85. package/src/dashboard/lib/suggested-now.js +254 -0
  86. package/src/dashboard/lib/tool-awareness.js +125 -0
  87. package/src/dashboard/lib/transcript-parser.js +371 -0
  88. package/src/dashboard/lib/vault-graph-parser.js +205 -0
  89. package/src/dashboard/lib/view-generator.js +163 -0
  90. package/src/dashboard/lib/voice-dna-parser.js +57 -0
  91. package/src/dashboard/lib/walkthrough-script.js +140 -0
  92. package/src/dashboard/lib/workflow-graph-parser.js +170 -0
  93. package/src/dashboard/lib/workflow-library-parser.js +146 -0
  94. package/src/dashboard/server.js +2402 -0
  95. package/src/dashboard/shortcut.js +284 -0
  96. package/src/dashboard/static/setup-poc.html +306 -0
  97. package/src/dashboard/static/walkthrough-poc.html +580 -0
  98. package/src/dashboard/styles.css +1201 -0
  99. package/src/dashboard/templates/index.html +278 -0
  100. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
  101. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
  102. package/src/dashboard/ui/dist/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
  103. package/src/dashboard/ui/dist/assets/hero.png +0 -0
  104. package/src/dashboard/ui/dist/assets/index-ByVKPvLf.js +34 -0
  105. package/src/dashboard/ui/dist/assets/index-CCoKjUcC.js +161 -0
  106. package/src/dashboard/ui/dist/assets/index-CZWNQDt5.css +1 -0
  107. package/src/dashboard/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  108. package/src/dashboard/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  109. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  110. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  111. package/src/dashboard/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  112. package/src/dashboard/ui/dist/assets/mover-system.css +62 -0
  113. package/src/dashboard/ui/dist/assets/xterm-CqkleIqs.js +1 -0
  114. package/src/dashboard/ui/dist/icon.svg +4 -0
  115. package/src/dashboard/ui/dist/index.html +18 -0
  116. package/src/dashboard/ui/dist/manifest.webmanifest +1 -0
  117. package/src/dashboard/ui/dist/registerSW.js +1 -0
  118. package/src/dashboard/ui/dist/sw.js +1 -0
  119. package/src/dashboard/ui/dist/workbox-39fa566e.js +1 -0
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+ const fs = require("fs");
5
+ const {
6
+ libraryDir,
7
+ libraryCheatsheetsDir,
8
+ libraryPrinciplesDir,
9
+ librarySopsDir,
10
+ libraryEntitiesDir,
11
+ libraryEntitiesPeopleDir,
12
+ libraryEntitiesOrgsDir,
13
+ libraryEntitiesPlacesDir,
14
+ libraryEntitiesDecisionsDir,
15
+ libraryInputsDir,
16
+ libraryScriptsDir,
17
+ libraryMocsDir,
18
+ safeListDir,
19
+ safeStat,
20
+ exists
21
+ } = require("./paths");
22
+
23
+ // Index 03_Library/* — counts + recent items per subdirectory.
24
+
25
+ function indexLibrary(vault) {
26
+ if (!exists(libraryDir(vault))) return { available: false };
27
+
28
+ const sections = [
29
+ { name: "Cheatsheets", dir: libraryCheatsheetsDir(vault) },
30
+ { name: "Principles", dir: libraryPrinciplesDir(vault) },
31
+ { name: "SOPs", dir: librarySopsDir(vault) },
32
+ { name: "MOCs", dir: libraryMocsDir(vault) },
33
+ { name: "Scripts", dir: libraryScriptsDir(vault) }
34
+ ];
35
+
36
+ const out = { available: true, sections: [], recent: [], counts: { total: 0 } };
37
+
38
+ for (const sec of sections) {
39
+ if (!exists(sec.dir)) {
40
+ out.sections.push({ name: sec.name, count: 0, recent: [] });
41
+ continue;
42
+ }
43
+ const entries = walkMarkdown(sec.dir);
44
+ entries.sort((a, b) => b.mtime - a.mtime);
45
+ const recent = entries.slice(0, 3).map(e => ({
46
+ name: e.name.replace(/\.md$/, ""),
47
+ mtime: e.mtime,
48
+ ageDays: Math.floor((Date.now() - e.mtime) / 86400000)
49
+ }));
50
+ out.sections.push({ name: sec.name, count: entries.length, recent });
51
+ out.counts[sec.name.toLowerCase()] = entries.length;
52
+ out.counts.total += entries.length;
53
+ }
54
+
55
+ // Entities
56
+ const entitySubs = [
57
+ { name: "People", dir: libraryEntitiesPeopleDir(vault) },
58
+ { name: "Orgs", dir: libraryEntitiesOrgsDir(vault) },
59
+ { name: "Places", dir: libraryEntitiesPlacesDir(vault) },
60
+ { name: "Decisions", dir: libraryEntitiesDecisionsDir(vault) }
61
+ ];
62
+ out.entities = entitySubs.map(s => {
63
+ const entries = exists(s.dir) ? walkMarkdown(s.dir) : [];
64
+ entries.sort((a, b) => b.mtime - a.mtime);
65
+ return {
66
+ name: s.name,
67
+ count: entries.length,
68
+ recent: entries.slice(0, 5).map(e => ({
69
+ name: e.name.replace(/\.md$/, ""),
70
+ mtime: e.mtime,
71
+ ageDays: Math.floor((Date.now() - e.mtime) / 86400000)
72
+ }))
73
+ };
74
+ });
75
+
76
+ // Inputs (Articles, Books, Videos)
77
+ if (exists(libraryInputsDir(vault))) {
78
+ const inputSubs = safeListDir(libraryInputsDir(vault), { dirsOnly: true });
79
+ out.inputs = inputSubs.map(sub => {
80
+ const entries = walkMarkdown(sub.path);
81
+ return { name: sub.name, count: entries.length };
82
+ });
83
+ } else {
84
+ out.inputs = [];
85
+ }
86
+
87
+ return out;
88
+ }
89
+
90
+ function walkMarkdown(dir, depth = 0) {
91
+ if (depth > 3) return [];
92
+ const out = [];
93
+ const entries = safeListDir(dir);
94
+ for (const e of entries) {
95
+ if (e.isFile && e.name.endsWith(".md") && !e.name.startsWith("_")) {
96
+ const stat = safeStat(e.path);
97
+ if (stat) out.push({ name: e.name, path: e.path, mtime: stat.mtime.getTime() });
98
+ } else if (e.isDir && !e.name.startsWith("_") && !e.name.startsWith(".")) {
99
+ out.push(...walkMarkdown(e.path, depth + 1));
100
+ }
101
+ }
102
+ return out;
103
+ }
104
+
105
+ module.exports = { indexLibrary };
@@ -0,0 +1,290 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ // library-search.js — ONE truthful search index for the Library route.
7
+ //
8
+ // Sol full product review (2026-07-11, Library 5.0/10): the "EVERYTHING"
9
+ // search box indexed only the libraryV2 subset while the other shelves were
10
+ // counts with no rows behind them. This module makes the coverage equal the
11
+ // advertised shelves: every shelf the bento names is either content-searchable
12
+ // here or explicitly reported as notIndexable with the reason (CLI commands
13
+ // are usage counts, not files).
14
+ //
15
+ // Contract:
16
+ // - Read-only. Never writes. Never leaves the declared shelf roots.
17
+ // - Bounded: per-file size cap, per-shelf file cap, result cap, passage cap.
18
+ // - Results carry root + vault-relative (or root-relative) path + a 1-based
19
+ // line anchor + the matched passage, so the UI can open the real file AT
20
+ // the passage in the existing viewer.
21
+ // - Honest coverage: shelves whose directory does not exist are reported in
22
+ // coverage.missing; shelves with nothing to read are in
23
+ // coverage.notIndexable. Nothing is silently skipped.
24
+ // - Non-text files (PDFs, images) match by NAME only and say so
25
+ // (nameOnly: true, line/passage null) — never implied content coverage.
26
+
27
+ const MAX_FILE_BYTES = 256 * 1024; // matches /api/file/read cap
28
+ const MAX_FILES_PER_SHELF = 2000;
29
+ const MAX_DEPTH = 6;
30
+ const MAX_LIMIT = 100;
31
+ const DEFAULT_LIMIT = 40;
32
+ const PASSAGE_CAP = 240;
33
+ const EXCLUDED_DIRS = new Set([
34
+ "node_modules", "dist", ".git", ".obsidian", ".trash", "coverage",
35
+ ]);
36
+
37
+ // Extensions whose CONTENT we read as text. Everything else is name-only.
38
+ const TEXT_EXTS = new Set([".md", ".txt", ".sh", ".js", ".mjs", ".ps1", ".canvas", ".json"]);
39
+
40
+ // ── shelf definitions — ids are stable API; labels mirror the bento ─────────
41
+ // dirs(ctx) returns the roots to walk for that shelf, each tagged with the
42
+ // serving root id the file-read endpoint understands (vault|skills|workflows|hooks).
43
+ const SHELVES = [
44
+ {
45
+ id: "engine", label: "Engine files",
46
+ dirs: (ctx) => [{ root: "vault", dir: path.join(ctx.vault, "02_Areas", "Engine"), depth: 1 }],
47
+ },
48
+ {
49
+ id: "projects", label: "Projects",
50
+ dirs: (ctx) => [{ root: "vault", dir: path.join(ctx.vault, "01_Projects"), depth: MAX_DEPTH }],
51
+ },
52
+ {
53
+ id: "library", label: "Library notes",
54
+ dirs: (ctx) => [
55
+ { root: "vault", dir: path.join(ctx.vault, "03_Library", "MOCs"), depth: 3 },
56
+ { root: "vault", dir: path.join(ctx.vault, "03_Library", "Principles"), depth: 3 },
57
+ { root: "vault", dir: path.join(ctx.vault, "03_Library", "SOPs"), depth: 3 },
58
+ { root: "vault", dir: path.join(ctx.vault, "03_Library", "Cheatsheets"), depth: 3 },
59
+ { root: "vault", dir: path.join(ctx.vault, "03_Library", "Scripts"), depth: 3 },
60
+ { root: "vault", dir: path.join(ctx.vault, "03_Library", "Entities", "Decisions"), depth: 3 },
61
+ ],
62
+ },
63
+ {
64
+ id: "skills", label: "Skills",
65
+ dirs: (ctx) => [{ root: "skills", dir: path.join(ctx.claudeDir, "skills"), depth: 3 }],
66
+ },
67
+ {
68
+ id: "hooks", label: "Hooks",
69
+ dirs: (ctx) => [{ root: "hooks", dir: path.join(ctx.bundleRoot, "src", "hooks"), depth: 2 }],
70
+ },
71
+ {
72
+ id: "workflows", label: "Workflows",
73
+ dirs: (ctx) => [{ root: "workflows", dir: path.join(ctx.bundleRoot, "src", "workflows"), depth: 1 }],
74
+ },
75
+ {
76
+ id: "cli", label: "CLI commands",
77
+ notIndexable: "CLI commands are usage counts stored in your config, not files. There is no text to search.",
78
+ },
79
+ {
80
+ id: "people", label: "People",
81
+ dirs: (ctx) => [
82
+ { root: "vault", dir: path.join(ctx.vault, "03_Library", "Entities", "People"), depth: 2 },
83
+ { root: "vault", dir: path.join(ctx.vault, "03_Library", "Entities", "Organizations"), depth: 2 },
84
+ { root: "vault", dir: path.join(ctx.vault, "03_Library", "Entities", "Places"), depth: 2 },
85
+ ],
86
+ },
87
+ {
88
+ id: "captures", label: "Captures",
89
+ dirs: (ctx) => [{ root: "vault", dir: path.join(ctx.vault, "03_Library", "Inputs"), depth: 3 }],
90
+ },
91
+ {
92
+ id: "archives", label: "Archives",
93
+ dirs: (ctx) => [{ root: "vault", dir: path.join(ctx.vault, "04_Archives"), depth: MAX_DEPTH }],
94
+ },
95
+ ];
96
+
97
+ /** Map a serving-root id to its absolute directory. Used by the file-read
98
+ * endpoint to resolve non-vault results with the same realpath containment
99
+ * it already applies to the vault. Returns null for unknown roots. */
100
+ function rootDirFor(ctx, root) {
101
+ if (root === "vault") return ctx.vault;
102
+ if (root === "skills") return path.join(ctx.claudeDir, "skills");
103
+ if (root === "workflows") return path.join(ctx.bundleRoot, "src", "workflows");
104
+ if (root === "hooks") return path.join(ctx.bundleRoot, "src", "hooks");
105
+ return null;
106
+ }
107
+
108
+ function walkFiles(dir, maxDepth, out, state) {
109
+ if (state.count >= MAX_FILES_PER_SHELF) return;
110
+ let entries;
111
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_e) { return; }
112
+ for (const ent of entries) {
113
+ if (state.count >= MAX_FILES_PER_SHELF) return;
114
+ if (ent.name.startsWith(".")) continue;
115
+ const full = path.join(dir, ent.name);
116
+ if (ent.isDirectory()) {
117
+ if (EXCLUDED_DIRS.has(ent.name)) continue;
118
+ if (maxDepth > 1) walkFiles(full, maxDepth - 1, out, state);
119
+ continue;
120
+ }
121
+ if (!ent.isFile()) continue; // symlinks are not followed — containment stays real
122
+ out.push(full);
123
+ state.count += 1;
124
+ }
125
+ }
126
+
127
+ /** Collect the files behind one shelf. Returns { files: [{abs, root, rootDir}], missing } */
128
+ function shelfFiles(shelf, ctx) {
129
+ const roots = shelf.dirs ? shelf.dirs(ctx) : [];
130
+ const files = [];
131
+ let anyDirExists = false;
132
+ const state = { count: 0 };
133
+ for (const r of roots) {
134
+ let stat = null;
135
+ try { stat = fs.statSync(r.dir); } catch (_e) { /* absent */ }
136
+ if (!stat || !stat.isDirectory()) continue;
137
+ anyDirExists = true;
138
+ const abs = [];
139
+ walkFiles(r.dir, r.depth || 1, abs, state);
140
+ for (const a of abs) files.push({ abs: a, root: r.root, rootDir: rootDirFor(ctx, r.root) });
141
+ }
142
+ return { files, missing: roots.length > 0 && !anyDirExists };
143
+ }
144
+
145
+ /** Build the passage around the first match inside a line, capped. */
146
+ function passageFrom(line, matchIdx, qLen) {
147
+ const trimmed = line.trim();
148
+ if (trimmed.length <= PASSAGE_CAP) return trimmed;
149
+ // center the window on the match
150
+ const lead = Math.max(0, matchIdx - Math.floor((PASSAGE_CAP - qLen) / 2));
151
+ const slice = line.slice(lead, lead + PASSAGE_CAP).trim();
152
+ return slice;
153
+ }
154
+
155
+ /**
156
+ * searchLibrary(ctx, opts) — the one index.
157
+ * ctx = { vault, bundleRoot, claudeDir }
158
+ * opts = { q, shelf, limit }
159
+ * q — case-insensitive substring; empty q + shelf = browse listing
160
+ * shelf — shelf id to scope to ("" = every indexed shelf)
161
+ * limit — max results (1..100)
162
+ * Returns { q, shelf, results, truncated, scanned, coverage }.
163
+ */
164
+ function searchLibrary(ctx, opts = {}) {
165
+ const q = String(opts.q || "").slice(0, 200);
166
+ const scope = String(opts.shelf || "");
167
+ const limit = Math.max(1, Math.min(MAX_LIMIT, Number(opts.limit) || DEFAULT_LIMIT));
168
+ const lc = q.toLowerCase();
169
+
170
+ const coverage = { indexed: [], notIndexable: [], missing: [] };
171
+ const results = [];
172
+ let scannedFiles = 0;
173
+ let skippedLarge = 0;
174
+ let truncated = false;
175
+
176
+ for (const shelf of SHELVES) {
177
+ if (shelf.notIndexable) {
178
+ coverage.notIndexable.push({ id: shelf.id, label: shelf.label, reason: shelf.notIndexable });
179
+ continue;
180
+ }
181
+ const { files, missing } = shelfFiles(shelf, ctx);
182
+ if (missing) {
183
+ coverage.missing.push({ id: shelf.id, label: shelf.label });
184
+ continue;
185
+ }
186
+ coverage.indexed.push({ id: shelf.id, label: shelf.label, files: files.length });
187
+
188
+ if (scope && scope !== shelf.id) continue; // counted for coverage, never searched
189
+
190
+ for (const f of files) {
191
+ // Every skill's file is literally named SKILL.md — the skill's actual
192
+ // name is its folder. Rendering the basename made the whole Skills
193
+ // shelf read "SKILL.md, SKILL.md, ..." (owner report + terra CONFIRMED
194
+ // #1, 2026-07-12). Same rule for any convention-named container file.
195
+ const base = path.basename(f.abs);
196
+ let name = base;
197
+ if (/^(SKILL|README|INDEX)\.md$/i.test(base)) {
198
+ const folder = path.basename(path.dirname(f.abs));
199
+ if (folder && folder !== "." && folder !== path.basename(f.rootDir)) name = folder;
200
+ }
201
+ const relPath = path.relative(f.rootDir, f.abs).split(path.sep).join("/");
202
+ // ext/textReadable ALWAYS derive from the real file, never the display
203
+ // name — a folder-derived name has no extension and would silently
204
+ // demote the file to name-only search.
205
+ const ext = path.extname(base).toLowerCase();
206
+ const textReadable = TEXT_EXTS.has(ext);
207
+
208
+ let mtime = null;
209
+ let size = 0;
210
+ try { const st = fs.statSync(f.abs); mtime = st.mtime.toISOString(); size = st.size; } catch (_e) { continue; }
211
+
212
+ // Browse mode: empty query + a shelf scope lists the shelf's real files.
213
+ if (!lc) {
214
+ if (!scope) continue; // browsing "everything" is the catalog's job
215
+ results.push({
216
+ shelf: shelf.id, shelfLabel: shelf.label, name, relPath, root: f.root,
217
+ line: null, passage: null, matches: 0, nameOnly: !textReadable, mtime,
218
+ });
219
+ continue;
220
+ }
221
+
222
+ const nameHit = name.toLowerCase().includes(lc);
223
+
224
+ if (!textReadable) {
225
+ // Content is not text — a name match is the only honest match.
226
+ if (nameHit) {
227
+ results.push({
228
+ shelf: shelf.id, shelfLabel: shelf.label, name, relPath, root: f.root,
229
+ line: null, passage: null, matches: 0, nameOnly: true, mtime,
230
+ });
231
+ }
232
+ continue;
233
+ }
234
+
235
+ if (size > MAX_FILE_BYTES) { skippedLarge += 1; continue; }
236
+
237
+ let content = "";
238
+ try { content = fs.readFileSync(f.abs, "utf8"); } catch (_e) { continue; }
239
+ scannedFiles += 1;
240
+
241
+ let firstLine = null;
242
+ let firstPassage = null;
243
+ let matches = 0;
244
+ const lines = content.split("\n");
245
+ for (let i = 0; i < lines.length; i++) {
246
+ const idx = lines[i].toLowerCase().indexOf(lc);
247
+ if (idx === -1) continue;
248
+ matches += 1;
249
+ if (firstLine === null) {
250
+ firstLine = i + 1;
251
+ firstPassage = passageFrom(lines[i], idx, lc.length);
252
+ }
253
+ }
254
+
255
+ if (!nameHit && matches === 0) continue;
256
+ results.push({
257
+ shelf: shelf.id, shelfLabel: shelf.label, name, relPath, root: f.root,
258
+ line: firstLine, passage: firstPassage, matches,
259
+ nameOnly: matches === 0, mtime,
260
+ });
261
+ }
262
+ }
263
+
264
+ // Rank: name matches first, then by match density, then recency.
265
+ if (lc) {
266
+ results.sort((a, b) => {
267
+ const an = a.name.toLowerCase().includes(lc) ? 1 : 0;
268
+ const bn = b.name.toLowerCase().includes(lc) ? 1 : 0;
269
+ if (an !== bn) return bn - an;
270
+ if (a.matches !== b.matches) return b.matches - a.matches;
271
+ return String(b.mtime || "").localeCompare(String(a.mtime || ""));
272
+ });
273
+ } else {
274
+ results.sort((a, b) => String(b.mtime || "").localeCompare(String(a.mtime || "")));
275
+ }
276
+
277
+ if (results.length > limit) {
278
+ results.length = limit;
279
+ truncated = true;
280
+ }
281
+
282
+ return {
283
+ q, shelf: scope || "all",
284
+ results, truncated,
285
+ scanned: { files: scannedFiles, skippedLarge },
286
+ coverage,
287
+ };
288
+ }
289
+
290
+ module.exports = { searchLibrary, rootDirFor, SHELVES };
@@ -0,0 +1,61 @@
1
+ #!/bin/bash
2
+ # log-activation.sh — POSTs a V5.0 activation event to a running dashboard.
3
+ #
4
+ # Usage:
5
+ # log-activation.sh <event_type> [meta_json]
6
+ #
7
+ # Examples:
8
+ # log-activation.sh log_completed
9
+ # log-activation.sh referral_mentioned '{"source":"twitter"}'
10
+ # log-activation.sh onboarding_finished
11
+ #
12
+ # Exits silently (best-effort) if:
13
+ # - No token file at ~/.mover/dashboard/.token (server not running)
14
+ # - curl missing
15
+ # - HTTP failure
16
+ #
17
+ # Used by workflow Bash steps (log.md, capture.md, pivot-strategy.md)
18
+ # and Stop hooks. Phase 41.0.7 of V5 backlog.
19
+
20
+ set -e
21
+
22
+ EVENT_TYPE="${1:-}"
23
+ META="${2:-}"
24
+
25
+ if [ -z "$EVENT_TYPE" ]; then
26
+ exit 0 # silent — no-op when invoked without args
27
+ fi
28
+
29
+ TOKEN_FILE="$HOME/.mover/dashboard/.token"
30
+ if [ ! -f "$TOKEN_FILE" ]; then
31
+ exit 0 # silent — server not running
32
+ fi
33
+
34
+ # Extract token + url with minimal JSON parsing
35
+ TOKEN=$(grep -o '"token"[[:space:]]*:[[:space:]]*"[^"]*"' "$TOKEN_FILE" | head -1 | sed 's/.*: *"//;s/"//')
36
+ URL=$(grep -o '"url"[[:space:]]*:[[:space:]]*"[^"]*"' "$TOKEN_FILE" | head -1 | sed 's/.*: *"//;s/"//')
37
+
38
+ if [ -z "$TOKEN" ] || [ -z "$URL" ]; then
39
+ exit 0
40
+ fi
41
+
42
+ # Build body
43
+ if [ -n "$META" ]; then
44
+ BODY=$(printf '{"type":"%s","meta":%s}' "$EVENT_TYPE" "$META")
45
+ else
46
+ BODY=$(printf '{"type":"%s"}' "$EVENT_TYPE")
47
+ fi
48
+
49
+ # Best-effort POST. Suppress all output — workflow logs shouldn't show this.
50
+ # Timeout = 2s so a hung server can't stall a workflow.
51
+ if command -v curl >/dev/null 2>&1; then
52
+ curl -s -m 2 \
53
+ -X POST \
54
+ -H "Content-Type: application/json" \
55
+ -H "X-Mover-Token: $TOKEN" \
56
+ -H "Origin: $URL" \
57
+ -d "$BODY" \
58
+ "$URL/api/event" >/dev/null 2>&1 || true
59
+ fi
60
+
61
+ exit 0
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+
3
+ // memory-curator.js — Mover memory engine, Layer 5 (curator lifecycle + reflection loop).
4
+ //
5
+ // curate(): ages every indexed session active -> stale (30d) -> archived (90d) and decays its
6
+ // trust so recall naturally favours fresh context. It NEVER deletes — old sessions stay
7
+ // searchable, just lower-weighted (the Curator pattern: forget gracefully, lose nothing).
8
+ //
9
+ // reflect(): drafts the DATA half of /analyse-day from the index (recent arcs + trending
10
+ // entities) for the owner to ENGAGE with. Auto-analysis nobody reads is theatre — the
11
+ // judgment half stays human. Writes nothing.
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+ const { moverDir, safeReadJson } = require("./paths");
16
+ const { parseSessionLogs } = require("./session-log-parser");
17
+
18
+ const MEM_DIR = process.env.MOVER_MEMORY_DIR || path.join(moverDir(), "memory");
19
+ const SEG_DIR = path.join(MEM_DIR, "segments");
20
+ const MANIFEST = path.join(MEM_DIR, "manifest.json");
21
+
22
+ const ACTIVE_DAYS = 30, ARCHIVE_DAYS = 90;
23
+ const TRUST = { active: 1.0, stale: 0.85, archived: 0.6 };
24
+
25
+ function loadManifest() { return safeReadJson(MANIFEST); }
26
+ function loadSeg(n) { return safeReadJson(path.join(SEG_DIR, n + ".json")); }
27
+ function atomicWrite(file, obj) { const t = file + ".tmp"; fs.writeFileSync(t, JSON.stringify(obj)); fs.renameSync(t, file); }
28
+
29
+ function tier(ts) {
30
+ const t = ts ? Date.parse(ts) : NaN;
31
+ if (!Number.isFinite(t)) return "active";
32
+ const days = (Date.now() - t) / 86400000;
33
+ if (days > ARCHIVE_DAYS) return "archived";
34
+ if (days > ACTIVE_DAYS) return "stale";
35
+ return "active";
36
+ }
37
+
38
+ // Age sessions + write trust weights into the manifest (consumed by memory-index query()).
39
+ function curate(opts = {}) {
40
+ const man = loadManifest();
41
+ if (!man) return { available: false, active: 0, stale: 0, archived: 0, adjusted: 0, written: 0 };
42
+ man.trust = man.trust || {};
43
+ const counts = { active: 0, stale: 0, archived: 0 };
44
+ let adjusted = 0;
45
+ for (const seg of (man.segments || []).filter((s) => s.startsWith("raw-"))) {
46
+ const s = loadSeg(seg);
47
+ if (!s) continue;
48
+ for (const id in s.docs) {
49
+ const tr = tier(s.docs[id].ts);
50
+ counts[tr]++;
51
+ const w = TRUST[tr];
52
+ if (w !== 1.0) {
53
+ if (man.trust[id] !== w) { man.trust[id] = w; adjusted++; }
54
+ } else if (man.trust[id] != null && man.trust[id] !== 1.0) {
55
+ delete man.trust[id]; adjusted++; // a session refreshed back into the active window
56
+ }
57
+ }
58
+ }
59
+ man.curatedAt = new Date().toISOString();
60
+ atomicWrite(MANIFEST, man);
61
+ return { available: true, ...counts, adjusted, total: counts.active + counts.stale + counts.archived, written: adjusted };
62
+ }
63
+
64
+ // Draft a reflection from the index: recent arcs + entities trending across sessions.
65
+ function reflect(opts = {}) {
66
+ const man = loadManifest();
67
+ if (!man) return { available: false, draft: "", trendingEntities: [], recentArcs: [], written: 0 };
68
+ const N = Object.keys(man.indexedSessions || {}).length || 1;
69
+
70
+ const trendingEntities = Object.keys(man.linkFreq || {})
71
+ .map((note) => { const f = man.linkFreq[note]; const idf = Math.log(N / Math.max(1, f)); return { note, sessions: f, score: +(f * idf).toFixed(2) }; })
72
+ .filter((x) => x.sessions / N <= 0.6) // drop the ubiquitous (Engine files etc.)
73
+ .sort((a, b) => b.score - a.score)
74
+ .slice(0, opts.limit || 10);
75
+
76
+ let recentArcs = [];
77
+ try {
78
+ const vault = opts.vault || man.vault;
79
+ if (vault) recentArcs = parseSessionLogs(vault).docs.slice(0, 8).map((d) => ({ date: d.date, title: d.title }));
80
+ } catch {}
81
+
82
+ const lines = ["## Reflection (auto-draft — engage with it, don't just read it)", ""];
83
+ if (recentArcs.length) { lines.push("Recent arcs:"); recentArcs.forEach((a) => lines.push(`- ${a.date} — ${a.title}`)); lines.push(""); }
84
+ if (trendingEntities.length) { lines.push("Entities trending across your sessions:"); trendingEntities.forEach((t) => lines.push(`- [[${t.note}]] (${t.sessions} sessions)`)); lines.push(""); }
85
+ lines.push("What pattern is forming? What's the one decision to make next? (your call — this is the /analyse-day judgment half.)");
86
+
87
+ return { available: true, draft: lines.join("\n"), trendingEntities, recentArcs, written: 0 };
88
+ }
89
+
90
+ module.exports = { curate, reflect, MEM_DIR };
91
+
92
+ // ── CLI ──
93
+ if (require.main === module) {
94
+ const vault = require("./paths").resolveVault();
95
+ console.log("curate:", JSON.stringify(curate()));
96
+ console.log("\n" + reflect({ vault }).draft);
97
+ }