moflo 4.10.20 → 4.10.21

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 (46) hide show
  1. package/.claude/guidance/shipped/moflo-cli-reference.md +1 -0
  2. package/.claude/guidance/shipped/moflo-core-guidance.md +1 -0
  3. package/.claude/guidance/shipped/moflo-skills-reference.md +108 -0
  4. package/.claude/guidance/shipped/moflo-yaml-reference.md +13 -0
  5. package/.claude/skills/commune/SKILL.md +140 -0
  6. package/.claude/skills/divine/SKILL.md +130 -0
  7. package/.claude/skills/meditate/SKILL.md +122 -0
  8. package/README.md +39 -3
  9. package/bin/index-all.mjs +2 -1
  10. package/bin/index-reference.mjs +221 -0
  11. package/bin/lib/file-sync.mjs +50 -1
  12. package/bin/lib/hook-io.mjs +63 -0
  13. package/bin/lib/index-fingerprint.mjs +0 -0
  14. package/bin/lib/internal-skills.mjs +16 -0
  15. package/bin/lib/meditate.mjs +497 -0
  16. package/bin/lib/pii-scrub.mjs +119 -0
  17. package/bin/lib/reference-docs.mjs +218 -0
  18. package/bin/lib/session-continuity.mjs +372 -0
  19. package/bin/lib/shipped-scripts.json +36 -0
  20. package/bin/lib/shipped-scripts.mjs +33 -0
  21. package/bin/lib/yaml-upgrader.mjs +62 -0
  22. package/bin/meditate-capture.mjs +123 -0
  23. package/bin/meditate-distill.mjs +121 -0
  24. package/bin/session-continuity.mjs +206 -0
  25. package/bin/session-start-launcher.mjs +140 -60
  26. package/dist/src/cli/config/moflo-config.js +18 -0
  27. package/dist/src/cli/init/executor.js +11 -17
  28. package/dist/src/cli/init/moflo-init.js +21 -19
  29. package/dist/src/cli/init/moflo-yaml-template.js +21 -0
  30. package/dist/src/cli/init/settings-generator.js +23 -1
  31. package/dist/src/cli/init/shipped-scripts.js +39 -0
  32. package/dist/src/cli/memory/bridge-core.js +20 -0
  33. package/dist/src/cli/memory/bridge-entries.js +8 -2
  34. package/dist/src/cli/memory/memory-bridge.js +6 -2
  35. package/dist/src/cli/memory/memory-initializer.js +6 -2
  36. package/dist/src/cli/services/hook-block-hash.js +9 -1
  37. package/dist/src/cli/services/hook-wiring.js +38 -0
  38. package/dist/src/cli/transfer/anonymization/index.js +146 -40
  39. package/dist/src/cli/transfer/deploy-seraphine.js +1 -1
  40. package/dist/src/cli/transfer/export.js +2 -2
  41. package/dist/src/cli/transfer/store/publish.js +1 -1
  42. package/dist/src/cli/version.js +1 -1
  43. package/package.json +2 -2
  44. package/scripts/post-install-bootstrap.mjs +22 -42
  45. package/dist/src/cli/hooks/llm/index.js +0 -11
  46. package/dist/src/cli/hooks/llm/llm-hooks.js +0 -382
package/bin/index-all.mjs CHANGED
@@ -78,7 +78,7 @@ function isIndexEnabled(key) {
78
78
  if (existsSync(yamlPath)) {
79
79
  try {
80
80
  const content = readFileSync(yamlPath, 'utf-8');
81
- for (const k of ['guidance', 'code_map', 'tests', 'patterns']) {
81
+ for (const k of ['guidance', 'code_map', 'tests', 'patterns', 'reference']) {
82
82
  const re = new RegExp(`auto_index:\\s*\\n(?:.*\\n)*?\\s+${k}:\\s*(true|false)`);
83
83
  const match = content.match(re);
84
84
  _autoIndexFlags[k] = match ? match[1] !== 'false' : true;
@@ -182,6 +182,7 @@ function buildStepPlan() {
182
182
  consider('code-map', 'code_map', 'generate-code-map.mjs', 'flo-codemap', ['--no-embeddings'], 180_000);
183
183
  consider('test-index', 'tests', 'index-tests.mjs', 'flo-testmap', ['--no-embeddings']);
184
184
  consider('patterns-index', 'patterns', 'index-patterns.mjs', 'flo-patterns', []);
185
+ consider('reference-index', 'reference', 'index-reference.mjs', 'flo-reference', []);
185
186
 
186
187
  // Pretrain extracts patterns from the repo via the CLI subcommand. No
187
188
  // direct script — invoke through the local flo binary.
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Index installed library docs into moflo memory under the `reference` namespace.
4
+ *
5
+ * Native, version-pinned alternative to a hosted docs MCP (e.g. Context7). For
6
+ * every package the repo DIRECTLY depends on, this reads the docs that already
7
+ * sit in `node_modules` — the entry `.d.ts` type surface and the README — chunks
8
+ * them, and stores them keyed on the INSTALLED version. Retrieval is free: the
9
+ * chunks land in the same HNSW store every other namespace uses, so the agent's
10
+ * mandated `memory_search` first action surfaces them with navigation crumbs.
11
+ *
12
+ * Why this shape (see issue #1184):
13
+ * - Version-correct by construction — the resolved folder IS the version; we
14
+ * read `node_modules/<pkg>/package.json.version`, so it works identically
15
+ * across npm/yarn/pnpm/bun with no lockfile parsing.
16
+ * - Zero network, fully offline — `fs` reads only.
17
+ * - Cross-platform — `path.join` only, no shelling out.
18
+ * - Bounded — DIRECT deps only (not the transitive tree), with per-doc size
19
+ * and chunk caps so one mega-package can't dominate the index.
20
+ * - Graceful — a package with no README/types contributes nothing; never an
21
+ * error. Wrong docs are worse than none.
22
+ *
23
+ * The pure discovery/chunking/entry-shaping logic lives in
24
+ * `./lib/reference-docs.mjs` (unit-tested); this file is the orchestrator that
25
+ * owns the DB write, the incremental-diff gate, and the background embed spawn.
26
+ *
27
+ * Usage:
28
+ * node node_modules/moflo/bin/index-reference.mjs # Incremental
29
+ * node node_modules/moflo/bin/index-reference.mjs --force # Full reindex
30
+ * node node_modules/moflo/bin/index-reference.mjs --verbose # Detailed logging
31
+ * node node_modules/moflo/bin/index-reference.mjs --stats # Print stats and exit
32
+ */
33
+
34
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
35
+ import { createHash } from 'crypto';
36
+ import { resolve, dirname } from 'path';
37
+ import { fileURLToPath } from 'url';
38
+ import { resolveMofloBin } from './lib/resolve-bin.mjs';
39
+ import { memoryDbPath, MOFLO_DIR, findProjectRoot } from './lib/moflo-paths.mjs';
40
+ import { openBackend } from './lib/get-backend.mjs';
41
+ import { applyIncrementalChunks, computeContentListHash } from './lib/incremental-write.mjs';
42
+ import { createProcessManager } from './lib/process-manager.mjs';
43
+ import { collectReferenceDocs, buildDocEntries } from './lib/reference-docs.mjs';
44
+
45
+ const __dirname = dirname(fileURLToPath(import.meta.url));
46
+
47
+ const projectRoot = findProjectRoot();
48
+ const NAMESPACE = 'reference';
49
+ const DB_PATH = memoryDbPath(projectRoot);
50
+ const HASH_CACHE_PATH = resolve(projectRoot, MOFLO_DIR, 'reference-hash.txt');
51
+
52
+ const args = process.argv.slice(2);
53
+ const force = args.includes('--force');
54
+ const verbose = args.includes('--verbose') || args.includes('-v');
55
+ const statsOnly = args.includes('--stats');
56
+
57
+ function log(msg) { console.log(`[index-reference] ${msg}`); }
58
+ function debug(msg) { if (verbose) console.log(`[index-reference] ${msg}`); }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Database helpers — identical shape to the other indexers (#745 incremental)
62
+ // ---------------------------------------------------------------------------
63
+
64
+ function ensureDbDir() {
65
+ const dir = dirname(DB_PATH);
66
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
67
+ }
68
+
69
+ async function getDb() {
70
+ ensureDbDir();
71
+ const db = await openBackend(projectRoot, { create: true });
72
+ db.run(`
73
+ CREATE TABLE IF NOT EXISTS memory_entries (
74
+ id TEXT PRIMARY KEY,
75
+ key TEXT NOT NULL,
76
+ namespace TEXT DEFAULT 'default',
77
+ content TEXT NOT NULL,
78
+ type TEXT DEFAULT 'semantic',
79
+ embedding TEXT,
80
+ embedding_model TEXT DEFAULT 'local',
81
+ embedding_dimensions INTEGER,
82
+ tags TEXT,
83
+ metadata TEXT,
84
+ owner_id TEXT,
85
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
86
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
87
+ expires_at INTEGER,
88
+ last_accessed_at INTEGER,
89
+ access_count INTEGER DEFAULT 0,
90
+ status TEXT DEFAULT 'active',
91
+ UNIQUE(namespace, key)
92
+ )
93
+ `);
94
+ db.run(`CREATE INDEX IF NOT EXISTS idx_memory_key_ns ON memory_entries(key, namespace)`);
95
+ db.run(`CREATE INDEX IF NOT EXISTS idx_memory_namespace ON memory_entries(namespace)`);
96
+ return db;
97
+ }
98
+
99
+ function countNamespace(db) {
100
+ const stmt = db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE namespace = ?`);
101
+ stmt.bind([NAMESPACE]);
102
+ let count = 0;
103
+ if (stmt.step()) count = stmt.getAsObject().cnt;
104
+ stmt.free();
105
+ return count;
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Main
110
+ // ---------------------------------------------------------------------------
111
+
112
+ async function main() {
113
+ const startTime = Date.now();
114
+
115
+ const { packages, docFiles, depCount } = collectReferenceDocs(projectRoot);
116
+
117
+ if (depCount === 0) {
118
+ log('No dependencies found in package.json (nothing to ground)');
119
+ return;
120
+ }
121
+
122
+ if (statsOnly) {
123
+ const db = await getDb();
124
+ const count = countNamespace(db);
125
+ db.close();
126
+ log(`${packages.length} packages with docs (of ${depCount} deps), ${count} chunks in reference namespace`);
127
+ return;
128
+ }
129
+
130
+ if (packages.length === 0) {
131
+ log(`No installed docs found across ${depCount} dependencies`);
132
+ return;
133
+ }
134
+
135
+ // Outer gate — content hash over the doc files combined with each resolved
136
+ // name@version (so a version bump with byte-identical docs still re-keys).
137
+ // The version line is folded straight into the digest — no sidecar file — so
138
+ // the gate is deterministic and side-effect-free. Skips the whole
139
+ // extract+write pipeline when nothing changed (#746).
140
+ const versionLine = packages.map((p) => `${p.name}@${p.version}`).join(',');
141
+ const currentHash = createHash('sha256')
142
+ .update(versionLine)
143
+ .update('\n')
144
+ .update(computeContentListHash(docFiles))
145
+ .digest('hex');
146
+
147
+ if (!force && existsSync(HASH_CACHE_PATH)) {
148
+ const cached = readFileSync(HASH_CACHE_PATH, 'utf-8').trim();
149
+ if (cached === currentHash) {
150
+ log('No dependency-doc changes detected (use --force to reindex)');
151
+ return;
152
+ }
153
+ }
154
+
155
+ // Extract chunks from every resolved package.
156
+ const allEntries = [];
157
+ let packagesIndexed = 0;
158
+ for (const pkg of packages) {
159
+ let pkgEntries = 0;
160
+ if (pkg.readmePath) {
161
+ try {
162
+ const entries = buildDocEntries(pkg, 'readme', readFileSync(pkg.readmePath, 'utf-8'));
163
+ allEntries.push(...entries);
164
+ pkgEntries += entries.length;
165
+ } catch { /* unreadable README — skip */ }
166
+ }
167
+ if (pkg.typesPath) {
168
+ try {
169
+ const entries = buildDocEntries(pkg, 'types', readFileSync(pkg.typesPath, 'utf-8'));
170
+ allEntries.push(...entries);
171
+ pkgEntries += entries.length;
172
+ } catch { /* unreadable .d.ts — skip */ }
173
+ }
174
+ if (pkgEntries > 0) packagesIndexed++;
175
+ debug(`${pkg.name}@${pkg.version}: ${pkgEntries} chunks`);
176
+ }
177
+
178
+ log(`Extracted ${allEntries.length} doc chunks from ${packagesIndexed} packages`);
179
+
180
+ // Content-aware diff — unchanged rows keep their embeddings; orphaned chunks
181
+ // (including every chunk of an upgraded package's old version) are swept.
182
+ const db = await getDb();
183
+ const counts = applyIncrementalChunks(db, NAMESPACE, allEntries);
184
+ if (counts.inserted + counts.updated + counts.removed > 0) db.save();
185
+ db.close();
186
+
187
+ log(
188
+ `Diff: ${counts.inserted} new, ${counts.updated} updated, ` +
189
+ `${counts.unchanged} unchanged, ${counts.removed} removed`,
190
+ );
191
+
192
+ writeFileSync(HASH_CACHE_PATH, currentHash, 'utf-8');
193
+
194
+ // Embed the new/changed rows in the background, registered with the shared
195
+ // ProcessManager so doctor's zombie scan allowlists it and teardown reaps it.
196
+ // The namespace-derived label dedupes a second index-reference spawn within
197
+ // the lock window; build-embeddings only fills rows whose embedding IS NULL,
198
+ // so index-all's later global pass won't re-embed these.
199
+ try {
200
+ const embeddingScript = resolveMofloBin(
201
+ projectRoot, 'flo-embeddings', 'build-embeddings.mjs', { includeDevFallback: true },
202
+ );
203
+ if (embeddingScript) {
204
+ const pm = createProcessManager(projectRoot);
205
+ const result = pm.spawn('node', [embeddingScript, '--namespace', NAMESPACE], `build-embeddings-${NAMESPACE}`);
206
+ if (result.skipped) {
207
+ debug(`Embedding generation already running (PID: ${result.pid})`);
208
+ } else if (result.pid) {
209
+ debug(`Embedding generation started in background (PID: ${result.pid})`);
210
+ }
211
+ }
212
+ } catch (err) { debug(`embedding spawn skipped: ${err.message}`); }
213
+
214
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
215
+ log(`Done in ${elapsed}s — ${allEntries.length} reference chunks written`);
216
+ }
217
+
218
+ main().catch(err => {
219
+ log(`Error: ${err.message}`);
220
+ process.exit(1);
221
+ });
@@ -28,13 +28,14 @@ import {
28
28
  copyFileSync,
29
29
  existsSync,
30
30
  mkdirSync,
31
+ readdirSync,
31
32
  readFileSync,
32
33
  renameSync,
33
34
  statSync,
34
35
  unlinkSync,
35
36
  } from 'node:fs';
36
37
  import { createHash } from 'node:crypto';
37
- import { dirname } from 'node:path';
38
+ import { dirname, relative, resolve } from 'node:path';
38
39
 
39
40
  export const TRANSIENT_CODES = new Set(['EBUSY', 'EPERM', 'EACCES']);
40
41
  export const RETRY_BACKOFF_MS = [50, 200, 800];
@@ -198,3 +199,51 @@ export function makeSyncer({ onSuccess } = {}) {
198
199
  isCircuitOpen: () => circuitOpen,
199
200
  };
200
201
  }
202
+
203
+ /**
204
+ * Recursively sync every `.md` under `srcDir` into `<projectRoot>/<destPrefix>`,
205
+ * skipping any entry whose top-level directory is listed in `excludeTopLevel`.
206
+ *
207
+ * The session-start launcher uses this to mirror `node_modules/moflo/.claude/
208
+ * agents` and `.claude/skills` into the consumer project. `excludeTopLevel`
209
+ * keeps moflo-internal skills (`bin/lib/internal-skills.mjs`) out of consumer
210
+ * installs — without it the blanket copy would land `/publish` and `/reset-epic`
211
+ * in every consumer (they ship in the tarball but must not be installed).
212
+ *
213
+ * Each file is copied through `syncFile` so the manifest, hash-skip, and
214
+ * retry/breaker behaviour are identical to every other synced path.
215
+ *
216
+ * @param {string} srcDir Absolute source directory (e.g. node_modules/moflo/.claude/skills).
217
+ * @param {string} destPrefix Project-relative destination prefix (e.g. '.claude/skills').
218
+ * @param {object} opts
219
+ * @param {string} opts.projectRoot Consumer project root the destPrefix is resolved against.
220
+ * @param {(src: string, dest: string, key: string) => Promise<unknown>} opts.syncFile From makeSyncer().
221
+ * @param {Set<string>|string[]} [opts.excludeTopLevel] Top-level dir names to skip.
222
+ * @param {(message: string) => void} [opts.onWarn] Non-fatal warning sink.
223
+ */
224
+ export async function syncDirRecursive(srcDir, destPrefix, { projectRoot, syncFile, excludeTopLevel, onWarn } = {}) {
225
+ if (!existsSync(srcDir)) return;
226
+ let entries;
227
+ try {
228
+ entries = readdirSync(srcDir, { recursive: true, withFileTypes: true });
229
+ } catch (err) {
230
+ onWarn?.(`${destPrefix} readdir failed (${errMessage(err)})`);
231
+ return;
232
+ }
233
+ const exclude = excludeTopLevel instanceof Set
234
+ ? excludeTopLevel
235
+ : new Set(excludeTopLevel || []);
236
+ for (const entry of entries) {
237
+ if (!entry.isFile()) continue;
238
+ if (!entry.name.toLowerCase().endsWith('.md')) continue;
239
+ const parent = entry.parentPath || entry.path || srcDir;
240
+ const absSrc = resolve(parent, entry.name);
241
+ // path.relative (not slice) so a trailing separator on srcDir can't shift
242
+ // the top-level segment and silently defeat the exclusion check.
243
+ const rel = relative(srcDir, absSrc).split(/[\\/]/).join('/');
244
+ if (exclude.size > 0 && exclude.has(rel.split('/')[0])) continue;
245
+ const absDest = resolve(projectRoot, destPrefix, rel);
246
+ // syncFile owns dir creation (mkdir recursive) — no outer mkdir needed.
247
+ await syncFile(absSrc, absDest, `${destPrefix}/${rel}`);
248
+ }
249
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Shared hook I/O primitives for moflo's bin/*.mjs hook handlers.
3
+ *
4
+ * Extracted (#1198) so the UserPromptSubmit/Stop capture hook
5
+ * (meditate-capture.mjs) and the passive session-continuity Stop hook
6
+ * (session-continuity.mjs) share ONE implementation of "read the hook's JSON
7
+ * stdin" — a bug in the bounded-read logic gets fixed once, not per copy.
8
+ *
9
+ * Cross-platform (Rule #1): Node fs primitives only; no shell.
10
+ */
11
+
12
+ import { existsSync, openSync, readSync, closeSync, statSync, readFileSync } from 'fs';
13
+
14
+ /**
15
+ * Read a hook's JSON stdin (session_id, transcript_path, prompt, …). Bounded by
16
+ * a 500ms cap so a missing/withheld stdin can never hang the hook; the timer is
17
+ * cleared on a normal end so the process exits immediately instead of lingering
18
+ * up to 500ms. Never throws — returns {} on any parse/IO error.
19
+ *
20
+ * @returns {Promise<Record<string, any>>}
21
+ */
22
+ export async function readHookStdin() {
23
+ if (process.stdin.isTTY) return {};
24
+ return new Promise((res) => {
25
+ let data = '';
26
+ let done = false;
27
+ let timer = null;
28
+ const parse = (s) => { try { return s ? JSON.parse(s) : {}; } catch { return {}; } };
29
+ const finish = () => { if (done) return; done = true; if (timer) clearTimeout(timer); res(parse(data)); };
30
+ process.stdin.setEncoding('utf-8');
31
+ process.stdin.on('data', (c) => { if (!done) data += c; });
32
+ process.stdin.on('end', finish);
33
+ process.stdin.on('error', finish);
34
+ timer = setTimeout(finish, 500);
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Read the last `bytes` of a file as UTF-8. Returns '' on any error. The first
40
+ * (likely partial) line is the caller's concern — JSONL/transcript parsers
41
+ * tolerate a truncated leading line.
42
+ *
43
+ * @param {string} path
44
+ * @param {number} bytes
45
+ * @returns {string}
46
+ */
47
+ export function readFileTail(path, bytes) {
48
+ try {
49
+ if (!path || !existsSync(path)) return '';
50
+ const size = statSync(path).size;
51
+ if (size <= bytes) return readFileSync(path, 'utf-8');
52
+ const fd = openSync(path, 'r');
53
+ try {
54
+ const buf = Buffer.alloc(bytes);
55
+ readSync(fd, buf, 0, bytes, size - bytes);
56
+ return buf.toString('utf-8');
57
+ } finally {
58
+ closeSync(fd);
59
+ }
60
+ } catch {
61
+ return '';
62
+ }
63
+ }
Binary file
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Skills that ship in the npm tarball (under `node_modules/moflo/.claude/skills/`)
3
+ * but must NEVER be installed into consumer projects — strictly moflo-internal
4
+ * dev tooling. `/publish` bumps moflo's own version and publishes to npm;
5
+ * `/reset-epic` torches epic test data. Both are meaningless or harmful in a
6
+ * consumer repo.
7
+ *
8
+ * The session-start launcher's recursive skills sync (`syncDirRecursive` in
9
+ * `file-sync.mjs`) copies every shipped skill into the consumer on each run, so
10
+ * it MUST skip these. The canonical TypeScript copy is `INTERNAL_SKILLS` in
11
+ * `src/cli/init/executor.ts` (used by `flo init`). The launcher is a plain
12
+ * `.mjs` and can't import that TS const across the dist/source depth boundary,
13
+ * so this leaf mirrors it. `tests/bin/internal-skills-parity.test.ts` asserts
14
+ * the two lists never drift.
15
+ */
16
+ export const INTERNAL_SKILLS = ['publish', 'reset-epic'];