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,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { metricsLogPath, healthAnalysisPath, safeRead, safeStat } = require("./paths");
|
|
4
|
+
|
|
5
|
+
// Parse Metrics_Log.md + Health_Analysis_Full.md for vitality dashboard data.
|
|
6
|
+
|
|
7
|
+
function parseMetricsLog(vault) {
|
|
8
|
+
const mPath = metricsLogPath(vault);
|
|
9
|
+
const hPath = healthAnalysisPath(vault);
|
|
10
|
+
const mText = safeRead(mPath);
|
|
11
|
+
const hText = safeRead(hPath);
|
|
12
|
+
const mStat = safeStat(mPath);
|
|
13
|
+
|
|
14
|
+
const out = {
|
|
15
|
+
available: !!(mText || hText),
|
|
16
|
+
metricsLogPath: mPath,
|
|
17
|
+
healthAnalysisPath: hPath,
|
|
18
|
+
lastUpdated: mStat ? mStat.mtime : null,
|
|
19
|
+
latestMetrics: {},
|
|
20
|
+
recentEntries: []
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (mText) {
|
|
24
|
+
// Extract bold-key fields (most recent ones first)
|
|
25
|
+
const fieldRe = /\*\*([^*]+):\*\*\s*([^\n]+)/g;
|
|
26
|
+
let m;
|
|
27
|
+
while ((m = fieldRe.exec(mText)) !== null) {
|
|
28
|
+
const k = m[1].trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_|_$/g, "");
|
|
29
|
+
if (!out.latestMetrics[k]) out.latestMetrics[k] = m[2].trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Extract date-prefixed log entries
|
|
33
|
+
const lines = mText.split("\n");
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
const dateMatch = line.match(/^[-*]?\s*\[?(\d{4}-\d{2}-\d{2})\]?\s+(.+)$/);
|
|
36
|
+
if (dateMatch) {
|
|
37
|
+
out.recentEntries.push({ date: dateMatch[1], text: dateMatch[2].trim() });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
out.recentEntries = out.recentEntries.slice(-10).reverse();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Health Analysis: extract HRV, sleep, RHR, weight summary.
|
|
44
|
+
//
|
|
45
|
+
// T242 bug hunt fix (C#7): keep the relaxed key-then-anything-then-value
|
|
46
|
+
// greedy regex (it was finding correct values like sleep=6.73h, hrv=8.9ms
|
|
47
|
+
// in prose), but add a sanity-range filter. Previous bug: `Weight gain
|
|
48
|
+
// (+3-4 kg since baseline)` captured "4" as weight. The lazy `[^|]*?`
|
|
49
|
+
// matched " gain (+3-", then `\d+` grabbed "4", then `\s*kg` matched
|
|
50
|
+
// " kg". The 4 was a delta, not the weight. Range 30-300 rejects it.
|
|
51
|
+
// Same fix kills RHR=0.43 (some Fitbit entropy decimal) — out of 30-200.
|
|
52
|
+
if (hText) {
|
|
53
|
+
out.hrvLatest = extractMetricInRange(hText, /HRV[^|]*?(\d+(?:\.\d+)?)\s*(?:ms|rmssd)/i, 5, 200);
|
|
54
|
+
out.sleepLatest = extractMetricInRange(hText, /Sleep[^|]*?(\d+(?:\.\d+)?)\s*(?:h|hours?)/i, 0, 16);
|
|
55
|
+
out.rhrLatest = extractMetricInRange(hText, /(?:RHR|Resting HR)[^|]*?(\d+(?:\.\d+)?)\s*(?:bpm)?/i, 30, 200);
|
|
56
|
+
out.weightLatest = extractMetricInRange(hText, /Weight[^|]*?(\d+(?:\.\d+)?)\s*(?:kg|lb)/i, 30, 300);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function extractMetricInRange(text, re, min, max) {
|
|
63
|
+
const m = text.match(re);
|
|
64
|
+
if (!m) return null;
|
|
65
|
+
const v = parseFloat(m[1]);
|
|
66
|
+
if (!Number.isFinite(v)) return null;
|
|
67
|
+
if (v < min || v > max) return null;
|
|
68
|
+
return m[1];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extractMetricFromText(text, re) {
|
|
72
|
+
const m = text.match(re);
|
|
73
|
+
return m ? m[1] : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { parseMetricsLog };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Moves usage parser — lights the Moves map (~25 Mover moves) by how often
|
|
4
|
+
// each move has ACTUALLY run, read from Claude Code's real append-only
|
|
5
|
+
// command history at ~/.claude/history.jsonl.
|
|
6
|
+
//
|
|
7
|
+
// Per the dashboard's "TELEMETRY NEVER" promise: this reads a local file
|
|
8
|
+
// Claude Code already writes. Nothing is sent anywhere.
|
|
9
|
+
//
|
|
10
|
+
// Source line shape (one JSON object per line, append-only):
|
|
11
|
+
// { "display": "/log ", "pastedContents": {}, "timestamp": 1771088938619,
|
|
12
|
+
// "project": "<vault>/01_Projects/Mover OS Bundle",
|
|
13
|
+
// "sessionId": "50ba...-2562" }
|
|
14
|
+
//
|
|
15
|
+
// - `display` : the raw prompt text the user submitted. Slash commands
|
|
16
|
+
// and `moveros <sub>` invocations start here.
|
|
17
|
+
// - `timestamp` : epoch ms of the invocation.
|
|
18
|
+
// - `project` : the LITERAL cwd string at invocation time. NOT a slug.
|
|
19
|
+
// On a machine that migrated platforms, this field can carry
|
|
20
|
+
// both old and current absolute paths for the same vault.
|
|
21
|
+
//
|
|
22
|
+
// SCOPING (the crux): we count only invocations made inside THIS vault.
|
|
23
|
+
// - The dashboard server's canonical vault identity is
|
|
24
|
+
// `fs.realpathSync(path.resolve(vault))` (server.js:63). We mirror that
|
|
25
|
+
// so a symlinked vault resolves to the same identity here.
|
|
26
|
+
// - We then keep a history row only if its `project`, normalized to a
|
|
27
|
+
// boundary-aligned path-segment array, CONTAINS the vault realpath's
|
|
28
|
+
// distinctive tail segments as a contiguous run. This is a path-BOUNDARY
|
|
29
|
+
// match, NOT a substring `includes` — so a sibling like
|
|
30
|
+
// `Architect_OS_backup/` is correctly excluded (its segment is
|
|
31
|
+
// `Architect_OS_backup`, which never equals `Architect_OS`).
|
|
32
|
+
// - Old Windows-era rows can't be realpath-resolved on this machine, but
|
|
33
|
+
// they ARE this vault's history. The segment-anchor match recovers them
|
|
34
|
+
// (separators normalized `\` → `/`) without loosening the boundary.
|
|
35
|
+
//
|
|
36
|
+
// FALSE-ZERO DISCIPLINE (T261): history.jsonl absent or unreadable returns
|
|
37
|
+
// { available:false, source:'', totalRuns:null, byCommand:{} }
|
|
38
|
+
// so the UI shows "calibrating", never a fabricated all-zero map. We NEVER
|
|
39
|
+
// invent counts. A move the user simply hasn't run is ABSENT from byCommand
|
|
40
|
+
// — the UI treats absent as a REAL zero ("never").
|
|
41
|
+
|
|
42
|
+
const fs = require("fs");
|
|
43
|
+
const path = require("path");
|
|
44
|
+
const os = require("os");
|
|
45
|
+
|
|
46
|
+
function historyPath() {
|
|
47
|
+
return path.join(os.homedir(), ".claude", "history.jsonl");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Normalize any path string (Windows or POSIX) to a clean, trailing-slash-
|
|
51
|
+
// stripped, forward-slash form, then split into non-empty segments.
|
|
52
|
+
function pathSegments(p) {
|
|
53
|
+
return String(p)
|
|
54
|
+
.replace(/\\/g, "/") // Windows separators → POSIX
|
|
55
|
+
.replace(/\/+$/, "") // drop trailing slash(es)
|
|
56
|
+
.split("/")
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Build the vault's distinctive tail anchor: the last up-to-3 segments of the
|
|
61
|
+
// vault realpath (e.g. ["Documents","Obsidian","Architect_OS"]). Three
|
|
62
|
+
// segments is distinctive enough to avoid collisions while still matching the
|
|
63
|
+
// cross-platform paths, which share the `Documents/Obsidian/Architect_OS`
|
|
64
|
+
// tail. Falls back to whatever segments exist for shallow vault paths.
|
|
65
|
+
function vaultAnchorSegments(vault) {
|
|
66
|
+
let vaultReal;
|
|
67
|
+
try {
|
|
68
|
+
vaultReal = fs.realpathSync(path.resolve(vault));
|
|
69
|
+
} catch (_) {
|
|
70
|
+
// Vault path doesn't resolve (missing / not yet created). Fall back to the
|
|
71
|
+
// literal resolved string so scoping still has an anchor to match against.
|
|
72
|
+
vaultReal = path.resolve(vault);
|
|
73
|
+
}
|
|
74
|
+
const segs = pathSegments(vaultReal);
|
|
75
|
+
return segs.slice(Math.max(0, segs.length - 3));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// True if `projectRaw` (a literal cwd from history) sits inside the vault.
|
|
79
|
+
// Boundary-aligned: the anchor must appear as a contiguous run of WHOLE
|
|
80
|
+
// segments. `Architect_OS_backup` never matches an `Architect_OS` anchor
|
|
81
|
+
// segment, so sibling dirs are excluded.
|
|
82
|
+
function projectMatchesVault(projectRaw, anchor) {
|
|
83
|
+
if (!projectRaw || anchor.length === 0) return false;
|
|
84
|
+
const segs = pathSegments(projectRaw);
|
|
85
|
+
for (let i = 0; i + anchor.length <= segs.length; i++) {
|
|
86
|
+
let hit = true;
|
|
87
|
+
for (let j = 0; j < anchor.length; j++) {
|
|
88
|
+
if (segs[i + j] !== anchor[j]) { hit = false; break; }
|
|
89
|
+
}
|
|
90
|
+
if (hit) return true; // everything after the anchor is a descendant → in
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Extract a Mover-move identity from a raw `display` string.
|
|
96
|
+
// Returns { cmd, label } or null.
|
|
97
|
+
// - /slash-command → cmd "log", label "/log"
|
|
98
|
+
// - moveros <subcmd> → cmd "pulse", label "moveros pulse"
|
|
99
|
+
// Per the contract: command names are lowercase, may contain hyphens, and the
|
|
100
|
+
// first letter is a-z (so we don't match `/1`, `/-x`, paths, etc.).
|
|
101
|
+
function extractMove(display) {
|
|
102
|
+
const text = String(display || "");
|
|
103
|
+
const slash = text.match(/^\s*(\/[a-z][a-z-]+)/);
|
|
104
|
+
if (slash) {
|
|
105
|
+
const label = slash[1];
|
|
106
|
+
return { cmd: label.slice(1), label };
|
|
107
|
+
}
|
|
108
|
+
const cli = text.match(/^\s*moveros\s+([a-z-]+)/);
|
|
109
|
+
if (cli) {
|
|
110
|
+
return { cmd: cli[1], label: `moveros ${cli[1]}` };
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// computeMovesUsage({ vault }) -> {
|
|
116
|
+
// available: boolean, // false only when source is unreadable
|
|
117
|
+
// source: string, // absolute history.jsonl path, or '' when absent
|
|
118
|
+
// totalRuns: number | null, // total in-scope move invocations; null when unavailable
|
|
119
|
+
// byCommand: { [cmd]: { count, lastIso, lastLabel } } // absent cmd === real zero
|
|
120
|
+
// }
|
|
121
|
+
function computeMovesUsage(opts = {}) {
|
|
122
|
+
const { vault } = opts;
|
|
123
|
+
const src = historyPath();
|
|
124
|
+
|
|
125
|
+
// False-zero gate: no readable history → unavailable, never a fake map.
|
|
126
|
+
let raw;
|
|
127
|
+
try {
|
|
128
|
+
raw = fs.readFileSync(src, "utf8");
|
|
129
|
+
} catch (_) {
|
|
130
|
+
return { available: false, source: "", totalRuns: null, byCommand: {} };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const anchor = vaultAnchorSegments(vault);
|
|
134
|
+
const byCommand = {};
|
|
135
|
+
// Track newest timestamp per command separately so the public shape only
|
|
136
|
+
// carries { count, lastIso, lastLabel } (no internal bookkeeping leaks out).
|
|
137
|
+
const lastTsByCmd = Object.create(null);
|
|
138
|
+
let totalRuns = 0;
|
|
139
|
+
|
|
140
|
+
const lines = raw.split("\n");
|
|
141
|
+
for (let i = 0; i < lines.length; i++) {
|
|
142
|
+
const line = lines[i];
|
|
143
|
+
if (!line) continue;
|
|
144
|
+
let row;
|
|
145
|
+
try {
|
|
146
|
+
row = JSON.parse(line);
|
|
147
|
+
} catch (_) {
|
|
148
|
+
// Forward-compat: a malformed line never aborts the scan. Skip it.
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!projectMatchesVault(row.project, anchor)) continue;
|
|
152
|
+
|
|
153
|
+
const move = extractMove(row.display);
|
|
154
|
+
if (!move) continue;
|
|
155
|
+
|
|
156
|
+
const ts = Number(row.timestamp);
|
|
157
|
+
const hasTs = Number.isFinite(ts);
|
|
158
|
+
const iso = hasTs ? new Date(ts).toISOString() : null;
|
|
159
|
+
|
|
160
|
+
totalRuns += 1;
|
|
161
|
+
let entry = byCommand[move.cmd];
|
|
162
|
+
if (!entry) {
|
|
163
|
+
entry = byCommand[move.cmd] = { count: 0, lastIso: null, lastLabel: null };
|
|
164
|
+
lastTsByCmd[move.cmd] = -Infinity;
|
|
165
|
+
}
|
|
166
|
+
entry.count += 1;
|
|
167
|
+
// Keep the most-recent invocation's iso + label. Rows without a usable
|
|
168
|
+
// timestamp never overwrite a dated "last" (they stay as a fallback only
|
|
169
|
+
// if nothing dated was ever seen).
|
|
170
|
+
if (hasTs && ts > lastTsByCmd[move.cmd]) {
|
|
171
|
+
lastTsByCmd[move.cmd] = ts;
|
|
172
|
+
entry.lastIso = iso;
|
|
173
|
+
entry.lastLabel = move.label;
|
|
174
|
+
} else if (entry.lastLabel == null) {
|
|
175
|
+
// First sighting was undated — record at least the label so the UI has
|
|
176
|
+
// a name to show. lastIso stays null (honest: timestamp unknown).
|
|
177
|
+
entry.lastLabel = move.label;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { available: true, source: src, totalRuns, byCommand };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = { computeMovesUsage };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* onboarding-forge.js — turn the REAL list of Engine files being written into an
|
|
5
|
+
* ordered, human "forge" sequence for the onboarding synthesis beat (the moment the
|
|
6
|
+
* system composes your operating record from your answers).
|
|
7
|
+
*
|
|
8
|
+
* HONESTY CONTRACT: every beat after the lead-in corresponds 1:1 to a file the writer
|
|
9
|
+
* actually produces — never an invented step, never padding. Unknown-but-real files are
|
|
10
|
+
* appended (still honest) rather than dropped. Deterministic + zero-dep (mirrors the
|
|
11
|
+
* connect-recommender / skill-recommender pattern: pure logic here, rendered by the jsx).
|
|
12
|
+
*
|
|
13
|
+
* @param {Array<string>} files file paths or basenames the finalize step will write
|
|
14
|
+
* @param {{cap?:number}} [opts]
|
|
15
|
+
* @returns {Array<{ key:string, label:string, file:string|null }>} lead beat first
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// canonical compose order — the record is built identity-first, then the bet, then leverage
|
|
19
|
+
const ORDER = [
|
|
20
|
+
"Identity_Prime", "Strategy", "Goals", "Mover_Dossier",
|
|
21
|
+
"Active_Context", "Auto_Learnings", "Metrics_Log", "Voice_DNA",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// each label is a present-tense act of composition, warm + specific (never "Writing file 3/5")
|
|
25
|
+
const LABELS = {
|
|
26
|
+
Identity_Prime: "Shaping who you are",
|
|
27
|
+
Strategy: "Setting your strategy",
|
|
28
|
+
Goals: "Naming the bet",
|
|
29
|
+
Mover_Dossier: "Taking stock of your leverage",
|
|
30
|
+
Active_Context: "Opening your working memory",
|
|
31
|
+
Auto_Learnings: "Preparing to learn your patterns",
|
|
32
|
+
Metrics_Log: "Laying out your scoreboard",
|
|
33
|
+
Voice_DNA: "Tuning to your voice",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function basename(name) {
|
|
37
|
+
return String(name || "").split(/[\\/]/).pop().replace(/\.md$/i, "").trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function forgeBeats(files, opts) {
|
|
41
|
+
const cap = (opts && Number.isFinite(opts.cap)) ? opts.cap : 6;
|
|
42
|
+
const list = Array.isArray(files) ? files : [];
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
const ordered = [];
|
|
45
|
+
|
|
46
|
+
// 1. canonical order first (Identity → Strategy → Goals → Dossier → …)
|
|
47
|
+
for (const key of ORDER) {
|
|
48
|
+
if (!seen.has(key) && list.some((f) => basename(f) === key)) {
|
|
49
|
+
seen.add(key);
|
|
50
|
+
ordered.push(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// 2. any real file we don't have a canonical slot for — appended, never dropped (honest)
|
|
54
|
+
for (const f of list) {
|
|
55
|
+
const k = basename(f);
|
|
56
|
+
if (k && !seen.has(k)) { seen.add(k); ordered.push(k); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const beats = ordered.slice(0, cap).map((k) => ({
|
|
60
|
+
key: k,
|
|
61
|
+
label: LABELS[k] || ("Writing " + k.replace(/_/g, " ")),
|
|
62
|
+
file: k + ".md",
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// lead beat — the real first act: parsing the transcript into a profile. file:null because
|
|
66
|
+
// it isn't a file being written, it's the read that precedes the writes.
|
|
67
|
+
return [{ key: "_read", label: "Reading your answers", file: null }, ...beats];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { forgeBeats, LABELS, ORDER };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { dailiesDir } = require("./paths");
|
|
6
|
+
const { localDateKey } = require("./date-utils");
|
|
7
|
+
|
|
8
|
+
// override-outcome-parser.js — F18 / Codex viral #3. Cross-references each
|
|
9
|
+
// override entry against subsequent Daily Notes to detect:
|
|
10
|
+
// - Did the deferred workflow actually run later? (resolution outcome)
|
|
11
|
+
// - Was the chosen alternative followed through? (justification outcome)
|
|
12
|
+
//
|
|
13
|
+
// Returns: array of { override, resolution: 'kept'|'reverted'|'unknown', daysToResolve, evidenceFiles }
|
|
14
|
+
|
|
15
|
+
function compute({ vault, overrides = [], windowDays = 90 } = {}) {
|
|
16
|
+
if (!vault || !Array.isArray(overrides) || overrides.length === 0) {
|
|
17
|
+
return { available: false, outcomes: [] };
|
|
18
|
+
}
|
|
19
|
+
const dDir = dailiesDir(vault);
|
|
20
|
+
if (!fs.existsSync(dDir)) return { available: false, outcomes: [] };
|
|
21
|
+
|
|
22
|
+
const outcomes = overrides.slice(-20).map((o) => {
|
|
23
|
+
const overrideDate = o.at ? new Date(o.at) : null;
|
|
24
|
+
if (!overrideDate || isNaN(overrideDate.getTime())) {
|
|
25
|
+
return { override: o, resolution: 'unknown', daysToResolve: null, evidenceFiles: [], outcome7d: null, outcome30d: null };
|
|
26
|
+
}
|
|
27
|
+
const deferredWorkflow = (o.over || '').toLowerCase();
|
|
28
|
+
const startTs = overrideDate.getTime();
|
|
29
|
+
const evidenceFiles = [];
|
|
30
|
+
let firstSubsequentRun = null;
|
|
31
|
+
|
|
32
|
+
// Walk daily notes within windowDays after the override
|
|
33
|
+
for (let i = 0; i <= windowDays; i++) {
|
|
34
|
+
const d = new Date(startTs + i * 24 * 3600 * 1000);
|
|
35
|
+
const key = localDateKey(d);
|
|
36
|
+
const monthDir = key.slice(0, 7);
|
|
37
|
+
const fpath = path.join(dDir, monthDir, `Daily - ${key}.md`);
|
|
38
|
+
try {
|
|
39
|
+
const text = fs.readFileSync(fpath, "utf8");
|
|
40
|
+
// Look for the deferred workflow keyword in this daily note
|
|
41
|
+
if (deferredWorkflow && text.toLowerCase().includes(deferredWorkflow)) {
|
|
42
|
+
if (!firstSubsequentRun) firstSubsequentRun = { date: key, daysAfter: i };
|
|
43
|
+
evidenceFiles.push({ date: key, file: `Daily - ${key}.md` });
|
|
44
|
+
if (evidenceFiles.length >= 3) break;
|
|
45
|
+
}
|
|
46
|
+
} catch (_) { /* skip */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolution = firstSubsequentRun ? 'kept' : 'unknown';
|
|
50
|
+
return {
|
|
51
|
+
override: o,
|
|
52
|
+
resolution,
|
|
53
|
+
daysToResolve: firstSubsequentRun ? firstSubsequentRun.daysAfter : null,
|
|
54
|
+
evidenceFiles,
|
|
55
|
+
outcome7d: firstSubsequentRun && firstSubsequentRun.daysAfter <= 7 ? 'kept' : 'pending',
|
|
56
|
+
outcome30d: firstSubsequentRun && firstSubsequentRun.daysAfter <= 30 ? 'kept' : 'pending',
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
available: true,
|
|
62
|
+
outcomes,
|
|
63
|
+
kept: outcomes.filter(x => x.resolution === 'kept').length,
|
|
64
|
+
unresolved: outcomes.filter(x => x.resolution === 'unknown').length,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { compute };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Normalize override entries from pattern-manifest.json.
|
|
4
|
+
//
|
|
5
|
+
// T242 bug hunt fix (C#5): manifest sometimes carries `at` as the short
|
|
6
|
+
// "2026-04-20T11:11" form (no seconds). Per ECMAScript spec, that string
|
|
7
|
+
// is non-standard — Safari parses it as NaN. Force full ISO-8601 with
|
|
8
|
+
// seconds + UTC marker so Date.parse() is consistent across V8/Safari/
|
|
9
|
+
// Firefox. If the input is already valid, normalize to a canonical form.
|
|
10
|
+
function toFullIso(raw) {
|
|
11
|
+
if (!raw) return "";
|
|
12
|
+
const s = String(raw).trim();
|
|
13
|
+
// Already a full ISO with seconds + zone? Keep.
|
|
14
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$/.test(s)) return s;
|
|
15
|
+
// Short form: pad to seconds and treat as UTC. Date constructor accepts
|
|
16
|
+
// this form universally.
|
|
17
|
+
const m = s.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/);
|
|
18
|
+
if (m) {
|
|
19
|
+
const ss = m[4] || "00";
|
|
20
|
+
return `${m[1]}T${m[2]}:${m[3]}:${ss}.000Z`;
|
|
21
|
+
}
|
|
22
|
+
// Date-only? Anchor to midnight UTC.
|
|
23
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return `${s}T00:00:00.000Z`;
|
|
24
|
+
// Anything else — let Date.parse have a try. If valid, re-emit ISO.
|
|
25
|
+
const t = Date.parse(s);
|
|
26
|
+
return Number.isFinite(t) ? new Date(t).toISOString() : s;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeOverrides(manifest) {
|
|
30
|
+
const raw = (manifest && (manifest.overrideHistory || manifest.override_history)) || [];
|
|
31
|
+
if (!Array.isArray(raw)) return [];
|
|
32
|
+
return raw.map((o, i) => ({
|
|
33
|
+
id: o.id || `${o.timestamp || o.date || "x"}-${i}`,
|
|
34
|
+
at: toFullIso(o.timestamp || o.date),
|
|
35
|
+
project: o.project || "",
|
|
36
|
+
level: o.level || "",
|
|
37
|
+
what: o.what || o.action || o.task || "",
|
|
38
|
+
over: o.over || o.plan_item || o.conflicted_with || "",
|
|
39
|
+
reason: o.reason || o.justification || o.user_override || "",
|
|
40
|
+
outcome: o.outcome || "",
|
|
41
|
+
})).filter(o => o.at || o.what || o.reason);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function computeOverrideSummary(overrides, now = new Date()) {
|
|
45
|
+
const entries = Array.isArray(overrides) ? overrides : [];
|
|
46
|
+
const nowMs = now.getTime();
|
|
47
|
+
const day = 24 * 3600 * 1000;
|
|
48
|
+
const countWithin = (days) => entries.filter(o => {
|
|
49
|
+
const t = Date.parse(o.at || "");
|
|
50
|
+
return Number.isFinite(t) && nowMs - t <= days * day;
|
|
51
|
+
}).length;
|
|
52
|
+
const byLevel = entries.reduce((acc, o) => {
|
|
53
|
+
const level = String(o.level || "UNKNOWN").toUpperCase() || "UNKNOWN";
|
|
54
|
+
acc[level] = (acc[level] || 0) + 1;
|
|
55
|
+
return acc;
|
|
56
|
+
}, {});
|
|
57
|
+
const last = entries
|
|
58
|
+
.slice()
|
|
59
|
+
.sort((a, b) => (Date.parse(b.at || "") || 0) - (Date.parse(a.at || "") || 0))
|
|
60
|
+
.slice(0, 30);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
total: entries.length,
|
|
64
|
+
last7d: countWithin(7),
|
|
65
|
+
last14d: countWithin(14),
|
|
66
|
+
last30d: countWithin(30),
|
|
67
|
+
last90d: countWithin(90),
|
|
68
|
+
byLevel,
|
|
69
|
+
recent: last,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { normalizeOverrides, computeOverrideSummary };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
|
|
7
|
+
function moverDir() {
|
|
8
|
+
return path.join(os.homedir(), ".mover");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function moverConfigPath() {
|
|
12
|
+
return path.join(moverDir(), "config.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function moverPatternManifestPath() {
|
|
16
|
+
return path.join(moverDir(), "pattern-manifest.json");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function dashboardOutDir() {
|
|
20
|
+
return path.join(moverDir(), "dashboard");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function dashboardIndexPath() {
|
|
24
|
+
return path.join(dashboardOutDir(), "index.html");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function dashboardBuildLogPath() {
|
|
28
|
+
return path.join(dashboardOutDir(), "build.log");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function dashboardSnoozesPath() {
|
|
32
|
+
return path.join(dashboardOutDir(), "snoozes.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Vault-relative paths (caller passes vault root)
|
|
36
|
+
function engineDir(vault) { return path.join(vault, "02_Areas", "Engine"); }
|
|
37
|
+
|
|
38
|
+
function engineFile(vault, name) { return path.join(engineDir(vault), name); }
|
|
39
|
+
|
|
40
|
+
function dailiesDir(vault) { return path.join(engineDir(vault), "Dailies"); }
|
|
41
|
+
|
|
42
|
+
function weeklyReviewsDir(vault) { return path.join(engineDir(vault), "Weekly Reviews"); }
|
|
43
|
+
|
|
44
|
+
function projectsDir(vault) { return path.join(vault, "01_Projects"); }
|
|
45
|
+
|
|
46
|
+
function libraryDir(vault) { return path.join(vault, "03_Library"); }
|
|
47
|
+
|
|
48
|
+
function inboxDir(vault) { return path.join(vault, "00_Inbox"); }
|
|
49
|
+
|
|
50
|
+
function archivesDir(vault) { return path.join(vault, "04_Archives"); }
|
|
51
|
+
|
|
52
|
+
// Engine file shortcuts
|
|
53
|
+
function activeContextPath(vault) { return engineFile(vault, "Active_Context.md"); }
|
|
54
|
+
function strategyPath(vault) { return engineFile(vault, "Strategy.md"); }
|
|
55
|
+
function identityPrimePath(vault) { return engineFile(vault, "Identity_Prime.md"); }
|
|
56
|
+
function goalsPath(vault) { return engineFile(vault, "Goals.md"); }
|
|
57
|
+
function autoLearningsPath(vault) { return engineFile(vault, "Auto_Learnings.md"); }
|
|
58
|
+
// Dossier filename varies by user: Mover_Dossier.md (default), Architect_Dossier.md, Red_Team_Dossier.md.
|
|
59
|
+
// Returns the first existing file from candidate list, or default if none exist.
|
|
60
|
+
function moverDossierPath(vault) {
|
|
61
|
+
const candidates = ["Mover_Dossier.md", "Architect_Dossier.md", "Red_Team_Dossier.md"];
|
|
62
|
+
for (const name of candidates) {
|
|
63
|
+
const p = engineFile(vault, name);
|
|
64
|
+
try { if (fs.existsSync(p)) return p; } catch (_) {}
|
|
65
|
+
}
|
|
66
|
+
return engineFile(vault, candidates[0]);
|
|
67
|
+
}
|
|
68
|
+
function voiceDnaPath(vault) { return engineFile(vault, "Voice_DNA.md"); }
|
|
69
|
+
function metricsLogPath(vault) { return engineFile(vault, "Metrics_Log.md"); }
|
|
70
|
+
function healthAnalysisPath(vault) { return engineFile(vault, "Health_Analysis_Full.md"); }
|
|
71
|
+
|
|
72
|
+
// Daily note resolver — supports YYYY-MM/Daily - YYYY-MM-DD.md format
|
|
73
|
+
function dailyNotePath(vault, date) {
|
|
74
|
+
const yyyy = date.getFullYear();
|
|
75
|
+
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
|
76
|
+
const dd = String(date.getDate()).padStart(2, "0");
|
|
77
|
+
return path.join(dailiesDir(vault), `${yyyy}-${mm}`, `Daily - ${yyyy}-${mm}-${dd}.md`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Library subdirs
|
|
81
|
+
function libraryCheatsheetsDir(vault) { return path.join(libraryDir(vault), "Cheatsheets"); }
|
|
82
|
+
function libraryPrinciplesDir(vault) { return path.join(libraryDir(vault), "Principles"); }
|
|
83
|
+
function librarySopsDir(vault) { return path.join(libraryDir(vault), "SOPs"); }
|
|
84
|
+
function libraryEntitiesDir(vault) { return path.join(libraryDir(vault), "Entities"); }
|
|
85
|
+
function libraryEntitiesPeopleDir(vault) { return path.join(libraryEntitiesDir(vault), "People"); }
|
|
86
|
+
function libraryEntitiesOrgsDir(vault) { return path.join(libraryEntitiesDir(vault), "Organizations"); }
|
|
87
|
+
function libraryEntitiesPlacesDir(vault) { return path.join(libraryEntitiesDir(vault), "Places"); }
|
|
88
|
+
function libraryEntitiesDecisionsDir(vault) { return path.join(libraryEntitiesDir(vault), "Decisions"); }
|
|
89
|
+
function libraryInputsDir(vault) { return path.join(libraryDir(vault), "Inputs"); }
|
|
90
|
+
function libraryScriptsDir(vault) { return path.join(libraryDir(vault), "Scripts"); }
|
|
91
|
+
function libraryMocsDir(vault) { return path.join(libraryDir(vault), "MOCs"); }
|
|
92
|
+
|
|
93
|
+
function exists(p) {
|
|
94
|
+
try { fs.accessSync(p); return true; } catch { return false; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Resolve the vault root with NO hardcoded/PII path (so lib CLI entrypoints never ship a machine
|
|
98
|
+
// path): env MOVER_VAULT → ~/.mover/config.json vaultPath → walk up from cwd for 02_Areas/Engine → cwd.
|
|
99
|
+
function resolveVault() {
|
|
100
|
+
const hasEngine = (v) => { try { return !!v && fs.existsSync(path.join(v, "02_Areas", "Engine")); } catch { return false; } };
|
|
101
|
+
if (hasEngine(process.env.MOVER_VAULT)) return process.env.MOVER_VAULT;
|
|
102
|
+
let cfg = {}; try { cfg = JSON.parse(fs.readFileSync(moverConfigPath(), "utf8")) || {}; } catch {}
|
|
103
|
+
if (hasEngine(cfg.vaultPath)) return cfg.vaultPath;
|
|
104
|
+
let dir = process.cwd();
|
|
105
|
+
for (let i = 0; i < 8; i++) {
|
|
106
|
+
if (hasEngine(dir)) return dir;
|
|
107
|
+
const up = path.dirname(dir);
|
|
108
|
+
if (up === dir) break;
|
|
109
|
+
dir = up;
|
|
110
|
+
}
|
|
111
|
+
return process.env.MOVER_VAULT || cfg.vaultPath || process.cwd();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function safeRead(p, fallback = null) {
|
|
115
|
+
try { return fs.readFileSync(p, "utf8"); } catch { return fallback; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function safeReadJson(p, fallback = null) {
|
|
119
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return fallback; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function safeStat(p) {
|
|
123
|
+
try { return fs.statSync(p); } catch { return null; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function safeListDir(p, options = {}) {
|
|
127
|
+
try {
|
|
128
|
+
let entries = fs.readdirSync(p, { withFileTypes: true });
|
|
129
|
+
if (options.dirsOnly) entries = entries.filter(e => e.isDirectory());
|
|
130
|
+
if (options.filesOnly) entries = entries.filter(e => e.isFile());
|
|
131
|
+
if (options.extension) entries = entries.filter(e => e.isFile() && e.name.endsWith(options.extension));
|
|
132
|
+
if (options.skipUnderscore) entries = entries.filter(e => !e.name.startsWith("_") && !e.name.startsWith("."));
|
|
133
|
+
return entries.map(e => ({
|
|
134
|
+
name: e.name,
|
|
135
|
+
path: path.join(p, e.name),
|
|
136
|
+
isDir: e.isDirectory(),
|
|
137
|
+
isFile: e.isFile()
|
|
138
|
+
}));
|
|
139
|
+
} catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
// Mover dirs
|
|
146
|
+
moverDir,
|
|
147
|
+
moverConfigPath,
|
|
148
|
+
moverPatternManifestPath,
|
|
149
|
+
dashboardOutDir,
|
|
150
|
+
dashboardIndexPath,
|
|
151
|
+
dashboardBuildLogPath,
|
|
152
|
+
dashboardSnoozesPath,
|
|
153
|
+
// Vault dirs
|
|
154
|
+
engineDir,
|
|
155
|
+
engineFile,
|
|
156
|
+
dailiesDir,
|
|
157
|
+
weeklyReviewsDir,
|
|
158
|
+
projectsDir,
|
|
159
|
+
libraryDir,
|
|
160
|
+
inboxDir,
|
|
161
|
+
archivesDir,
|
|
162
|
+
// Engine files
|
|
163
|
+
activeContextPath,
|
|
164
|
+
strategyPath,
|
|
165
|
+
identityPrimePath,
|
|
166
|
+
goalsPath,
|
|
167
|
+
autoLearningsPath,
|
|
168
|
+
moverDossierPath,
|
|
169
|
+
voiceDnaPath,
|
|
170
|
+
metricsLogPath,
|
|
171
|
+
healthAnalysisPath,
|
|
172
|
+
dailyNotePath,
|
|
173
|
+
// Library
|
|
174
|
+
libraryCheatsheetsDir,
|
|
175
|
+
libraryPrinciplesDir,
|
|
176
|
+
librarySopsDir,
|
|
177
|
+
libraryEntitiesDir,
|
|
178
|
+
libraryEntitiesPeopleDir,
|
|
179
|
+
libraryEntitiesOrgsDir,
|
|
180
|
+
libraryEntitiesPlacesDir,
|
|
181
|
+
libraryEntitiesDecisionsDir,
|
|
182
|
+
libraryInputsDir,
|
|
183
|
+
libraryScriptsDir,
|
|
184
|
+
libraryMocsDir,
|
|
185
|
+
// Helpers
|
|
186
|
+
resolveVault,
|
|
187
|
+
exists,
|
|
188
|
+
safeRead,
|
|
189
|
+
safeReadJson,
|
|
190
|
+
safeStat,
|
|
191
|
+
safeListDir
|
|
192
|
+
};
|