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.
- package/.claude/guidance/shipped/moflo-cli-reference.md +1 -0
- package/.claude/guidance/shipped/moflo-core-guidance.md +1 -0
- package/.claude/guidance/shipped/moflo-skills-reference.md +108 -0
- package/.claude/guidance/shipped/moflo-yaml-reference.md +13 -0
- package/.claude/skills/commune/SKILL.md +140 -0
- package/.claude/skills/divine/SKILL.md +130 -0
- package/.claude/skills/meditate/SKILL.md +122 -0
- package/README.md +39 -3
- package/bin/index-all.mjs +2 -1
- package/bin/index-reference.mjs +221 -0
- package/bin/lib/file-sync.mjs +50 -1
- package/bin/lib/hook-io.mjs +63 -0
- package/bin/lib/index-fingerprint.mjs +0 -0
- package/bin/lib/internal-skills.mjs +16 -0
- package/bin/lib/meditate.mjs +497 -0
- package/bin/lib/pii-scrub.mjs +119 -0
- package/bin/lib/reference-docs.mjs +218 -0
- package/bin/lib/session-continuity.mjs +372 -0
- package/bin/lib/shipped-scripts.json +36 -0
- package/bin/lib/shipped-scripts.mjs +33 -0
- package/bin/lib/yaml-upgrader.mjs +62 -0
- package/bin/meditate-capture.mjs +123 -0
- package/bin/meditate-distill.mjs +121 -0
- package/bin/session-continuity.mjs +206 -0
- package/bin/session-start-launcher.mjs +140 -60
- package/dist/src/cli/config/moflo-config.js +18 -0
- package/dist/src/cli/init/executor.js +11 -17
- package/dist/src/cli/init/moflo-init.js +21 -19
- package/dist/src/cli/init/moflo-yaml-template.js +21 -0
- package/dist/src/cli/init/settings-generator.js +23 -1
- package/dist/src/cli/init/shipped-scripts.js +39 -0
- package/dist/src/cli/memory/bridge-core.js +20 -0
- package/dist/src/cli/memory/bridge-entries.js +8 -2
- package/dist/src/cli/memory/memory-bridge.js +6 -2
- package/dist/src/cli/memory/memory-initializer.js +6 -2
- package/dist/src/cli/services/hook-block-hash.js +9 -1
- package/dist/src/cli/services/hook-wiring.js +38 -0
- package/dist/src/cli/transfer/anonymization/index.js +146 -40
- package/dist/src/cli/transfer/deploy-seraphine.js +1 -1
- package/dist/src/cli/transfer/export.js +2 -2
- package/dist/src/cli/transfer/store/publish.js +1 -1
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/scripts/post-install-bootstrap.mjs +22 -42
- package/dist/src/cli/hooks/llm/index.js +0 -11
- 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
|
+
});
|
package/bin/lib/file-sync.mjs
CHANGED
|
@@ -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'];
|