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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module v3/cli/bridge-entries
|
|
9
9
|
*/
|
|
10
|
-
import { cosineSim, execRows, generateId, logBridgeError, persistBridgeDb, refreshVectorStatsCache, withDb } from './bridge-core.js';
|
|
10
|
+
import { cosineSim, execRows, generateId, logBridgeError, persistBridgeDb, refreshVectorStatsCache, searchCandidateCap, withDb } from './bridge-core.js';
|
|
11
11
|
import { embeddingResponseFrom, getBridgeEmbedder, resolveBridgeEmbedding } from './bridge-embedder.js';
|
|
12
12
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
13
13
|
/**
|
|
@@ -461,11 +461,17 @@ export async function bridgeSearchEntries(options) {
|
|
|
461
461
|
const nsFilter = namespace !== 'all' ? `AND namespace = ?` : '';
|
|
462
462
|
let rows;
|
|
463
463
|
try {
|
|
464
|
+
// #1201 — ORDER BY created_at DESC before the cap. A bare `LIMIT 1000`
|
|
465
|
+
// (no ORDER BY) truncated the candidate pool by rowid, so on a populated
|
|
466
|
+
// DB the first 1000 rows were all bulk-indexed code-map and a
|
|
467
|
+
// no-namespace search never scored learnings/patterns/etc. Recency
|
|
468
|
+
// ordering keeps recent curated entries in the pool when truncation hits.
|
|
464
469
|
const sql = `
|
|
465
470
|
SELECT id, key, namespace, content, metadata, embedding
|
|
466
471
|
FROM memory_entries
|
|
467
472
|
WHERE status = 'active' ${nsFilter}
|
|
468
|
-
|
|
473
|
+
ORDER BY created_at DESC
|
|
474
|
+
LIMIT ${searchCandidateCap()}
|
|
469
475
|
`;
|
|
470
476
|
rows = namespace !== 'all' ? execRows(ctx.db, sql, [namespace]) : execRows(ctx.db, sql);
|
|
471
477
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module v3/cli/memory-bridge
|
|
11
11
|
*/
|
|
12
|
-
import { cosineSim, execRows, generateId, getRegistry, persistBridgeDb, withDb, } from './bridge-core.js';
|
|
12
|
+
import { cosineSim, execRows, generateId, getRegistry, persistBridgeDb, searchCandidateCap, withDb, } from './bridge-core.js';
|
|
13
13
|
import { bridgeSearchEntries, bridgeStoreEntry, } from './bridge-entries.js';
|
|
14
14
|
import { BRIDGE_EMBEDDING_MODEL, getBridgeEmbedder, } from './bridge-embedder.js';
|
|
15
15
|
// ===== Re-exports: primitives =====
|
|
@@ -73,11 +73,15 @@ export async function bridgeSearchHNSW(queryEmbedding, options, dbPath) {
|
|
|
73
73
|
: '';
|
|
74
74
|
let rows;
|
|
75
75
|
try {
|
|
76
|
+
// #1201 — recency-ordered candidate cap; see searchCandidateCap. A bare
|
|
77
|
+
// LIMIT truncated by rowid, hiding recent non-code-map namespaces from a
|
|
78
|
+
// no-namespace search.
|
|
76
79
|
const sql = `
|
|
77
80
|
SELECT id, key, namespace, content, embedding
|
|
78
81
|
FROM memory_entries
|
|
79
82
|
WHERE status = 'active' AND embedding IS NOT NULL ${nsFilter}
|
|
80
|
-
|
|
83
|
+
ORDER BY created_at DESC
|
|
84
|
+
LIMIT ${searchCandidateCap()}
|
|
81
85
|
`;
|
|
82
86
|
rows = nsFilter ? execRows(ctx.db, sql, [options.namespace]) : execRows(ctx.db, sql);
|
|
83
87
|
}
|
|
@@ -22,7 +22,7 @@ import { HnswLite } from './hnsw-lite.js';
|
|
|
22
22
|
import { tryLoadHnswSidecar } from './hnsw-persistence.js';
|
|
23
23
|
import { EMBEDDING_MODEL_OPT_OUT, getBridgeEmbedder, isEphemeralNamespace } from './bridge-embedder.js';
|
|
24
24
|
import { parseEmbeddingJson, toFloat32 } from './controllers/_shared.js';
|
|
25
|
-
import { writeVectorStatsJson } from './bridge-core.js';
|
|
25
|
+
import { searchCandidateCap, writeVectorStatsJson } from './bridge-core.js';
|
|
26
26
|
import { serialiseMetadata } from './bridge-entries.js';
|
|
27
27
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
28
28
|
import { MOFLO_DIR, hnswIndexPath, legacyMemoryDbPath, memoryDbPath, } from '../services/moflo-paths.js';
|
|
@@ -1836,12 +1836,16 @@ export async function searchEntries(options) {
|
|
|
1836
1836
|
// Fall back to brute-force SQLite search via the unified factory.
|
|
1837
1837
|
const db = openDaemonDatabase(dbPath);
|
|
1838
1838
|
// Get entries with embeddings
|
|
1839
|
+
// #1201 — recency-ordered candidate cap (see searchCandidateCap). A bare
|
|
1840
|
+
// LIMIT truncated by rowid, hiding recent non-code-map namespaces from a
|
|
1841
|
+
// no-namespace search.
|
|
1839
1842
|
const entries = db.exec(`
|
|
1840
1843
|
SELECT id, key, namespace, content, metadata, embedding
|
|
1841
1844
|
FROM memory_entries
|
|
1842
1845
|
WHERE status = 'active'
|
|
1843
1846
|
${namespace !== 'all' ? `AND namespace = '${namespace.replace(/'/g, "''")}'` : ''}
|
|
1844
|
-
|
|
1847
|
+
ORDER BY created_at DESC
|
|
1848
|
+
LIMIT ${searchCandidateCap()}
|
|
1845
1849
|
`);
|
|
1846
1850
|
const results = [];
|
|
1847
1851
|
if (entries[0]?.values) {
|
|
@@ -37,6 +37,12 @@ const scriptHook = (file, timeout) => ({
|
|
|
37
37
|
command: `node "${SCRIPTS_PREFIX}/${file}"`,
|
|
38
38
|
timeout,
|
|
39
39
|
});
|
|
40
|
+
/** Build a `node "<scripts/file>" <subcommand>` hook entry. */
|
|
41
|
+
const scriptHookSub = (file, sub, timeout) => ({
|
|
42
|
+
type: 'command',
|
|
43
|
+
command: `node "${SCRIPTS_PREFIX}/${file}"${sub ? ` ${sub}` : ''}`,
|
|
44
|
+
timeout,
|
|
45
|
+
});
|
|
40
46
|
const gateHook = (sub, timeout) => helperHook('gate-hook.mjs', sub, timeout);
|
|
41
47
|
const gateCjs = (sub, timeout) => helperHook('gate.cjs', sub, timeout);
|
|
42
48
|
const handler = (sub, timeout) => helperHook('hook-handler.cjs', sub, timeout);
|
|
@@ -97,6 +103,8 @@ export function getReferenceHookBlock() {
|
|
|
97
103
|
{ hooks: [helperHook('prompt-hook.mjs', '', 3000)] },
|
|
98
104
|
// #931 — Defensive safety-net hook. State reset only, no emission.
|
|
99
105
|
{ hooks: [gateHook('prompt-state-reset', 3000)] },
|
|
106
|
+
// #1198 — auto-meditate capture (detect). Default-ON (opt-out in-script).
|
|
107
|
+
{ hooks: [scriptHookSub('meditate-capture.mjs', 'meditate-detect', 3000)] },
|
|
100
108
|
],
|
|
101
109
|
SubagentStart: [
|
|
102
110
|
{ hooks: [helperHook('subagent-start.cjs', '', 2000)] },
|
|
@@ -107,7 +115,7 @@ export function getReferenceHookBlock() {
|
|
|
107
115
|
},
|
|
108
116
|
],
|
|
109
117
|
Stop: [
|
|
110
|
-
{ hooks: [handler('session-end', 5000), autoMemory('sync', 10000)] },
|
|
118
|
+
{ hooks: [handler('session-end', 5000), autoMemory('sync', 10000), scriptHookSub('session-continuity.mjs', 'capture', 5000), scriptHookSub('meditate-capture.mjs', 'meditate-scrape', 5000)] },
|
|
111
119
|
],
|
|
112
120
|
PreCompact: [
|
|
113
121
|
{ hooks: [gateCjs('compact-guidance', 3000)] },
|
|
@@ -37,6 +37,19 @@ export const REQUIRED_HOOK_WIRING = [
|
|
|
37
37
|
// the duplicate `prompt-reminder` wiring that was emitting the TaskCreate
|
|
38
38
|
// REMINDER twice per prompt (#931).
|
|
39
39
|
{ event: 'UserPromptSubmit', pattern: 'prompt-state-reset' },
|
|
40
|
+
// #1185 — passive session-continuity capture on the Stop hook. Self-heals
|
|
41
|
+
// into existing consumers on upgrade so the feature reaches everyone, not
|
|
42
|
+
// just fresh `flo init`. Capture is default-on (silent); injection is
|
|
43
|
+
// relevance-gated at session-start.
|
|
44
|
+
{ event: 'Stop', pattern: 'session-continuity.mjs' },
|
|
45
|
+
// #1198 — auto-meditate capture (default-ON; opt out via
|
|
46
|
+
// auto_meditate.enabled: false). `meditate-detect` (UserPromptSubmit) injects the
|
|
47
|
+
// answer-first directive on a strong signal; `meditate-scrape` (Stop) harvests
|
|
48
|
+
// <meditate-capture> tags into the ledger. Both share meditate-capture.mjs, so
|
|
49
|
+
// the unique subcommand token (not the shared filename) is the presence
|
|
50
|
+
// discriminator — same convention as gate-hook subcommands like record-test-run.
|
|
51
|
+
{ event: 'UserPromptSubmit', pattern: 'meditate-detect' },
|
|
52
|
+
{ event: 'Stop', pattern: 'meditate-scrape' },
|
|
40
53
|
];
|
|
41
54
|
/**
|
|
42
55
|
* Map gate pattern → hook entry to add when missing from settings.json.
|
|
@@ -78,6 +91,15 @@ export const HOOK_ENTRY_MAP = {
|
|
|
78
91
|
// Defensive safety-net — runs gate.cjs `prompt-state-reset` if prompt-hook.mjs
|
|
79
92
|
// throws before completing the per-prompt state reset. State-only, no emission.
|
|
80
93
|
'prompt-state-reset': { event: 'UserPromptSubmit', matcher: '', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" prompt-state-reset', timeout: 3000 } },
|
|
94
|
+
// #1185 — passive session-continuity capture (Stop hook). Bare block (no
|
|
95
|
+
// matcher), like the UserPromptSubmit entries; Claude Code fires every Stop
|
|
96
|
+
// block, so a separate block alongside session-end/sync is fine.
|
|
97
|
+
'session-continuity.mjs': { event: 'Stop', matcher: '', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/scripts/session-continuity.mjs" capture', timeout: 5000 } },
|
|
98
|
+
// #1198 — auto-meditate capture. Default-ON (the script no-ops only when
|
|
99
|
+
// auto_meditate.enabled is false). Bare blocks (no matcher), like the other
|
|
100
|
+
// UserPromptSubmit/Stop entries.
|
|
101
|
+
'meditate-detect': { event: 'UserPromptSubmit', matcher: '', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/scripts/meditate-capture.mjs" meditate-detect', timeout: 3000 } },
|
|
102
|
+
'meditate-scrape': { event: 'Stop', matcher: '', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/scripts/meditate-capture.mjs" meditate-scrape', timeout: 5000 } },
|
|
81
103
|
};
|
|
82
104
|
/**
|
|
83
105
|
* Inspect a parsed settings.json object for missing required hook wirings
|
|
@@ -156,6 +178,22 @@ export const HOOK_REWRITE_RULES = [
|
|
|
156
178
|
from: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" check-before-agent',
|
|
157
179
|
to: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-before-agent',
|
|
158
180
|
},
|
|
181
|
+
// Auto-meditate rebrand — the auto-reflect capture script + subcommands were
|
|
182
|
+
// renamed (reflect-capture.mjs → meditate-capture.mjs; reflect-detect/scrape →
|
|
183
|
+
// meditate-detect/scrape). Existing consumers self-heal here: the stale hook
|
|
184
|
+
// command is rewritten in place on session-start, so no dead hook is left
|
|
185
|
+
// pointing at the pruned reflect-capture.mjs. Idempotent (a command already at
|
|
186
|
+
// `to` won't match `from`).
|
|
187
|
+
{
|
|
188
|
+
name: 'auto-meditate rebrand: reflect-capture detect → meditate-capture detect',
|
|
189
|
+
from: 'node "$CLAUDE_PROJECT_DIR/.claude/scripts/reflect-capture.mjs" reflect-detect',
|
|
190
|
+
to: 'node "$CLAUDE_PROJECT_DIR/.claude/scripts/meditate-capture.mjs" meditate-detect',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'auto-meditate rebrand: reflect-capture scrape → meditate-capture scrape',
|
|
194
|
+
from: 'node "$CLAUDE_PROJECT_DIR/.claude/scripts/reflect-capture.mjs" reflect-scrape',
|
|
195
|
+
to: 'node "$CLAUDE_PROJECT_DIR/.claude/scripts/meditate-capture.mjs" meditate-scrape',
|
|
196
|
+
},
|
|
159
197
|
];
|
|
160
198
|
export const MATCHER_REWRITE_RULES = [
|
|
161
199
|
// Issue #929 — Claude Code anchors hook matchers (`^…$` semantics), so a
|
|
@@ -1,34 +1,102 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Anonymization Pipeline
|
|
3
|
-
* PII detection and redaction
|
|
3
|
+
* PII detection and redaction before a CFP is shared **externally**.
|
|
4
|
+
*
|
|
5
|
+
* ── Secret-pattern sourcing (#1193) ─────────────────────────────────────────
|
|
6
|
+
* The secret/credential *detection shapes* (Anthropic/OpenAI `sk-`/`sk-ant-`
|
|
7
|
+
* keys, GitHub `ghp_`/`github_pat_`, AWS `AKIA…`, Slack `xox…`, Google `AIza…`,
|
|
8
|
+
* JWTs, PEM private-key blocks, bearer / url / assigned secrets) are
|
|
9
|
+
* SINGLE-SOURCED from the session-continuity scrubber `bin/lib/pii-scrub.mjs`
|
|
10
|
+
* (its exported `SECRET_PATTERNS` + `scrubSecrets`).
|
|
11
|
+
*
|
|
12
|
+
* Before #1193 this module carried its own narrow `apiKey` regex
|
|
13
|
+
* (`/\b(sk-|pk-|api[_-]?key[_-]?)[a-zA-Z0-9]{20,}\b/gi`). For `sk-ant-api03-…`
|
|
14
|
+
* the hyphen after `sk-ant` broke the `[a-zA-Z0-9]{20,}` run, so a *live*
|
|
15
|
+
* Anthropic key — plus GitHub/AWS/Slack tokens — survived anonymization and
|
|
16
|
+
* could leak in a CFP published externally. Rather than re-type the broader
|
|
17
|
+
* shapes here (which would drift from the scrubber), we load the canonical set.
|
|
18
|
+
*
|
|
19
|
+
* The two modules keep DIFFERENT threat models, so only the SECRET shapes are
|
|
20
|
+
* shared, not the policies:
|
|
21
|
+
* - `pii-scrub.mjs` protects the user's OWN local disk → deliberately KEEPS
|
|
22
|
+
* benign context (file paths, IPs) and only nukes literal secrets.
|
|
23
|
+
* - this module pseudonymises for EXTERNAL publication → additionally strips
|
|
24
|
+
* emails / phones / IPs / home paths. Those non-secret PII patterns and
|
|
25
|
+
* their replacement policy stay defined locally below.
|
|
26
|
+
*
|
|
27
|
+
* ── Cross-boundary loading (CLAUDE.md Rule #1 + dogfooding) ──────────────────
|
|
28
|
+
* `bin/lib/pii-scrub.mjs` lives outside this module's TypeScript compile unit.
|
|
29
|
+
* A static `../../../../bin/lib/pii-scrub.mjs` import is the banned depth-mismatch
|
|
30
|
+
* anti-pattern (the path differs between `src/cli/**` and `dist/src/cli/**`), so
|
|
31
|
+
* the scrubber is reached the sanctioned way: `locateMofloRootPath()` (the
|
|
32
|
+
* shared moflo-package anchor, which also existence-checks) + `pathToFileURL()`
|
|
33
|
+
* + dynamic `import()`. `pathToFileURL` is what makes the ESM
|
|
34
|
+
* `import()` work on Windows; the join is platform-agnostic. That dynamic load
|
|
35
|
+
* is why `detectPII` / `redactPII` / `anonymizeCFP` / `scanCFPForPII` are async.
|
|
36
|
+
*
|
|
37
|
+
* The loader is fail-CLOSED: if the scrubber can't be resolved we throw rather
|
|
38
|
+
* than silently export un-redacted content — a credential leak is the worse
|
|
39
|
+
* outcome for an external-share path.
|
|
4
40
|
*/
|
|
5
41
|
import * as crypto from 'crypto';
|
|
42
|
+
import { join } from 'path';
|
|
43
|
+
import { pathToFileURL } from 'url';
|
|
44
|
+
import { locateMofloRootPath } from '../../services/moflo-require.js';
|
|
6
45
|
/**
|
|
7
|
-
* PII detection patterns
|
|
46
|
+
* Non-secret PII detection patterns owned by THIS module's external-share
|
|
47
|
+
* policy. Secret shapes are NOT here — they come from `bin/lib/pii-scrub.mjs`
|
|
48
|
+
* (see file header) so they only have to be maintained in one place.
|
|
8
49
|
*/
|
|
9
50
|
const PII_PATTERNS = {
|
|
10
51
|
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
|
11
52
|
phone: /\b(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
|
|
12
53
|
ipv4: /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g,
|
|
13
54
|
ipv6: /\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b/g,
|
|
14
|
-
apiKey: /\b(sk-|pk-|api[_-]?key[_-]?)[a-zA-Z0-9]{20,}\b/gi,
|
|
15
|
-
jwt: /\beyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*\b/g,
|
|
16
55
|
homePath: /\/(Users|home|Documents)\/[a-zA-Z0-9_.-]+/g,
|
|
17
56
|
windowsPath: /[A-Z]:\\Users\\[a-zA-Z0-9_.-]+/g,
|
|
18
57
|
};
|
|
19
58
|
/**
|
|
20
|
-
*
|
|
59
|
+
* Replacement policy for the local non-secret PII patterns above. Secret
|
|
60
|
+
* replacements are applied by `scrubSecrets` from the shared module.
|
|
21
61
|
*/
|
|
22
62
|
const REDACTIONS = {
|
|
23
63
|
email: (match) => `user_${hash(match).slice(0, 8)}@example.com`,
|
|
24
64
|
phone: '[REDACTED_PHONE]',
|
|
25
65
|
ipv4: '0.0.0.0',
|
|
26
66
|
ipv6: '::1',
|
|
27
|
-
apiKey: '[REDACTED_API_KEY]',
|
|
28
|
-
jwt: '[REDACTED_JWT]',
|
|
29
67
|
homePath: '/user/anonymous',
|
|
30
68
|
windowsPath: 'C:\\Users\\anonymous',
|
|
31
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* Lazily import the shared scrubber once per process and cache the promise.
|
|
72
|
+
* Fail-closed: a missing/garbled scrubber throws so the export aborts instead
|
|
73
|
+
* of shipping un-redacted credentials.
|
|
74
|
+
*/
|
|
75
|
+
let piiScrubPromise = null;
|
|
76
|
+
function loadPiiScrub() {
|
|
77
|
+
if (!piiScrubPromise) {
|
|
78
|
+
piiScrubPromise = (async () => {
|
|
79
|
+
const scrubPath = locateMofloRootPath(join('bin', 'lib', 'pii-scrub.mjs'));
|
|
80
|
+
if (!scrubPath) {
|
|
81
|
+
throw new Error('[anonymization] bin/lib/pii-scrub.mjs not found under the moflo package root — ' +
|
|
82
|
+
'refusing to anonymize without the shared secret scrubber (would risk leaking ' +
|
|
83
|
+
'credentials in an external share).');
|
|
84
|
+
}
|
|
85
|
+
const mod = (await import(pathToFileURL(scrubPath).href));
|
|
86
|
+
if (!Array.isArray(mod.SECRET_PATTERNS) || typeof mod.scrubSecrets !== 'function') {
|
|
87
|
+
throw new Error('[anonymization] bin/lib/pii-scrub.mjs is missing SECRET_PATTERNS/scrubSecrets — broken moflo install.');
|
|
88
|
+
}
|
|
89
|
+
return mod;
|
|
90
|
+
})().catch((err) => {
|
|
91
|
+
// Never cache a rejected promise — a transient/early failure must not
|
|
92
|
+
// poison every later anonymize call. Clear the singleton so a subsequent
|
|
93
|
+
// call re-attempts the load (still fail-closed: it re-throws here).
|
|
94
|
+
piiScrubPromise = null;
|
|
95
|
+
throw err;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return piiScrubPromise;
|
|
99
|
+
}
|
|
32
100
|
/**
|
|
33
101
|
* Hash a string for consistent pseudonymization
|
|
34
102
|
*/
|
|
@@ -36,41 +104,51 @@ function hash(input) {
|
|
|
36
104
|
return crypto.createHash('sha256').update(input).digest('hex');
|
|
37
105
|
}
|
|
38
106
|
/**
|
|
39
|
-
* Detect PII in a string
|
|
107
|
+
* Detect PII in a string. Combines the shared secret shapes (always `critical`)
|
|
108
|
+
* with this module's local non-secret PII patterns.
|
|
40
109
|
*/
|
|
41
|
-
export function detectPII(content) {
|
|
110
|
+
export async function detectPII(content) {
|
|
42
111
|
const result = {
|
|
43
112
|
found: false,
|
|
44
113
|
count: 0,
|
|
45
114
|
types: {},
|
|
46
115
|
locations: [],
|
|
47
116
|
};
|
|
48
|
-
|
|
117
|
+
const record = (type, pattern, severity) => {
|
|
118
|
+
// Reset lastIndex defensively — these regexes are module-shared and carry
|
|
119
|
+
// /g, so a prior .test()/.exec() elsewhere must not leak state.
|
|
120
|
+
pattern.lastIndex = 0;
|
|
49
121
|
const matches = content.match(pattern);
|
|
50
|
-
if (matches)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
122
|
+
if (!matches)
|
|
123
|
+
return;
|
|
124
|
+
result.found = true;
|
|
125
|
+
result.count += matches.length;
|
|
126
|
+
result.types[type] = (result.types[type] ?? 0) + matches.length;
|
|
127
|
+
for (const match of matches.slice(0, 5)) {
|
|
128
|
+
// Limit to first 5 samples
|
|
129
|
+
result.locations.push({
|
|
130
|
+
type,
|
|
131
|
+
path: 'content',
|
|
132
|
+
sample: match.slice(0, 20) + (match.length > 20 ? '...' : ''),
|
|
133
|
+
severity,
|
|
134
|
+
});
|
|
62
135
|
}
|
|
136
|
+
};
|
|
137
|
+
const { SECRET_PATTERNS } = await loadPiiScrub();
|
|
138
|
+
for (const { name, pattern } of SECRET_PATTERNS) {
|
|
139
|
+
record(name, pattern, 'critical');
|
|
140
|
+
}
|
|
141
|
+
for (const [type, pattern] of Object.entries(PII_PATTERNS)) {
|
|
142
|
+
record(type, pattern, getSeverity(type));
|
|
63
143
|
}
|
|
64
144
|
return result;
|
|
65
145
|
}
|
|
66
146
|
/**
|
|
67
|
-
* Get severity for PII type
|
|
147
|
+
* Get severity for a local (non-secret) PII type. Secret shapes are always
|
|
148
|
+
* reported as `critical` by `detectPII`, so they never reach this helper.
|
|
68
149
|
*/
|
|
69
150
|
function getSeverity(type) {
|
|
70
151
|
switch (type) {
|
|
71
|
-
case 'apiKey':
|
|
72
|
-
case 'jwt':
|
|
73
|
-
return 'critical';
|
|
74
152
|
case 'email':
|
|
75
153
|
case 'phone':
|
|
76
154
|
return 'high';
|
|
@@ -82,25 +160,30 @@ function getSeverity(type) {
|
|
|
82
160
|
}
|
|
83
161
|
}
|
|
84
162
|
/**
|
|
85
|
-
* Redact PII from a string
|
|
163
|
+
* Redact PII from a string. Secrets are removed first via the shared scrubber
|
|
164
|
+
* (broad vendor-token coverage), then this module's external-share PII policy
|
|
165
|
+
* pseudonymises emails / phones / IPs / home paths.
|
|
86
166
|
*/
|
|
87
|
-
export function redactPII(content) {
|
|
88
|
-
|
|
167
|
+
export async function redactPII(content) {
|
|
168
|
+
const { scrubSecrets } = await loadPiiScrub();
|
|
169
|
+
let result = scrubSecrets(content);
|
|
89
170
|
for (const [type, pattern] of Object.entries(PII_PATTERNS)) {
|
|
171
|
+
pattern.lastIndex = 0;
|
|
90
172
|
const replacement = REDACTIONS[type];
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
173
|
+
// Narrow the union for `String.replace`'s overloads: email is a function
|
|
174
|
+
// replacer, the rest are literal strings. A blanket `as string` cast would
|
|
175
|
+
// mis-describe (and risk silently breaking) the function case.
|
|
176
|
+
result =
|
|
177
|
+
typeof replacement === 'function'
|
|
178
|
+
? result.replace(pattern, replacement)
|
|
179
|
+
: result.replace(pattern, replacement);
|
|
97
180
|
}
|
|
98
181
|
return result;
|
|
99
182
|
}
|
|
100
183
|
/**
|
|
101
184
|
* Apply anonymization to CFP document
|
|
102
185
|
*/
|
|
103
|
-
export function anonymizeCFP(cfp, level) {
|
|
186
|
+
export async function anonymizeCFP(cfp, level) {
|
|
104
187
|
const transforms = [];
|
|
105
188
|
const anonymized = JSON.parse(JSON.stringify(cfp));
|
|
106
189
|
// Level: Minimal
|
|
@@ -113,10 +196,24 @@ export function anonymizeCFP(cfp, level) {
|
|
|
113
196
|
}
|
|
114
197
|
// Level: Standard
|
|
115
198
|
if (['standard', 'strict', 'paranoid'].includes(level)) {
|
|
116
|
-
// Redact PII from
|
|
199
|
+
// Redact secrets/PII from the patterns body...
|
|
117
200
|
const jsonStr = JSON.stringify(anonymized.patterns);
|
|
118
|
-
const redacted = redactPII(jsonStr);
|
|
201
|
+
const redacted = await redactPII(jsonStr);
|
|
119
202
|
anonymized.patterns = JSON.parse(redacted);
|
|
203
|
+
// ...and from free-text metadata (name / description / tags), which is the
|
|
204
|
+
// SAME external-share leak vector (#1193): a secret pasted into a CFP's
|
|
205
|
+
// description or a tag would otherwise survive at every level. The rest of
|
|
206
|
+
// metadata is structured (ids, timestamps, license); author displayName was
|
|
207
|
+
// already dropped at the minimal level above.
|
|
208
|
+
if (anonymized.metadata.name) {
|
|
209
|
+
anonymized.metadata.name = await redactPII(anonymized.metadata.name);
|
|
210
|
+
}
|
|
211
|
+
if (anonymized.metadata.description) {
|
|
212
|
+
anonymized.metadata.description = await redactPII(anonymized.metadata.description);
|
|
213
|
+
}
|
|
214
|
+
if (Array.isArray(anonymized.metadata.tags) && anonymized.metadata.tags.length > 0) {
|
|
215
|
+
anonymized.metadata.tags = await Promise.all(anonymized.metadata.tags.map((tag) => redactPII(tag)));
|
|
216
|
+
}
|
|
120
217
|
transforms.push('pii-redacted');
|
|
121
218
|
// Generalize timestamps
|
|
122
219
|
anonymized.anonymization.timestampsGeneralized = true;
|
|
@@ -168,8 +265,17 @@ export function anonymizeCFP(cfp, level) {
|
|
|
168
265
|
/**
|
|
169
266
|
* Scan CFP for PII without modification
|
|
170
267
|
*/
|
|
171
|
-
export function scanCFPForPII(cfp) {
|
|
172
|
-
|
|
268
|
+
export async function scanCFPForPII(cfp) {
|
|
269
|
+
// Scan the same surface anonymizeCFP redacts: the patterns body plus the
|
|
270
|
+
// free-text metadata fields (name / description / tags) — so the scan can't
|
|
271
|
+
// report "clean" on a CFP whose description carries a secret (#1193).
|
|
272
|
+
const meta = cfp.metadata;
|
|
273
|
+
const content = JSON.stringify({
|
|
274
|
+
patterns: cfp.patterns,
|
|
275
|
+
name: meta?.name,
|
|
276
|
+
description: meta?.description,
|
|
277
|
+
tags: meta?.tags,
|
|
278
|
+
});
|
|
173
279
|
return detectPII(content);
|
|
174
280
|
}
|
|
175
281
|
//# sourceMappingURL=index.js.map
|
|
@@ -136,7 +136,7 @@ async function deploy() {
|
|
|
136
136
|
}
|
|
137
137
|
// Step 3: Scan for PII
|
|
138
138
|
console.log('🔍 Scanning for PII...');
|
|
139
|
-
const piiScan = scanCFPForPII(genesis);
|
|
139
|
+
const piiScan = await scanCFPForPII(genesis);
|
|
140
140
|
if (piiScan.found) {
|
|
141
141
|
console.log(` Found ${piiScan.count} PII items:`);
|
|
142
142
|
for (const [type, count] of Object.entries(piiScan.types)) {
|
|
@@ -13,12 +13,12 @@ import { uploadToIPFS } from './ipfs/upload.js';
|
|
|
13
13
|
export async function exportPatterns(cfp, options = {}) {
|
|
14
14
|
const { output, format = 'json', anonymize = 'standard', redactPii = true, stripPaths = false, toIpfs = false, pin = true, gateway = 'https://w3s.link', } = options;
|
|
15
15
|
// Step 1: Scan for PII
|
|
16
|
-
const piiScan = scanCFPForPII(cfp);
|
|
16
|
+
const piiScan = await scanCFPForPII(cfp);
|
|
17
17
|
if (piiScan.found && redactPii) {
|
|
18
18
|
console.log(`Found ${piiScan.count} PII items, will be redacted`);
|
|
19
19
|
}
|
|
20
20
|
// Step 2: Apply anonymization
|
|
21
|
-
const { cfp: anonymizedCfp, transforms } = anonymizeCFP(cfp, anonymize);
|
|
21
|
+
const { cfp: anonymizedCfp, transforms } = await anonymizeCFP(cfp, anonymize);
|
|
22
22
|
console.log(`Applied ${transforms.length} anonymization transforms: ${transforms.join(', ')}`);
|
|
23
23
|
// Step 3: Serialize
|
|
24
24
|
const serialized = format === 'json'
|
|
@@ -23,7 +23,7 @@ export class PatternPublisher {
|
|
|
23
23
|
console.log(`[Publish] Starting publish: ${options.name}`);
|
|
24
24
|
try {
|
|
25
25
|
// Step 1: Anonymize if needed
|
|
26
|
-
const anonymized = anonymizeCFP(cfp, options.anonymize);
|
|
26
|
+
const anonymized = await anonymizeCFP(cfp, options.anonymize);
|
|
27
27
|
console.log(`[Publish] Anonymization level: ${options.anonymize}`);
|
|
28
28
|
// Step 2: Serialize content
|
|
29
29
|
const content = JSON.stringify(anonymized, null, 2);
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.10.
|
|
3
|
+
"version": "4.10.21",
|
|
4
4
|
"description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
|
|
5
5
|
"main": "dist/src/cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
96
96
|
"@typescript-eslint/parser": "^7.18.0",
|
|
97
97
|
"eslint": "^8.0.0",
|
|
98
|
-
"moflo": "^4.10.
|
|
98
|
+
"moflo": "^4.10.21-rc.1",
|
|
99
99
|
"tsx": "^4.21.0",
|
|
100
100
|
"typescript": "^5.9.3",
|
|
101
101
|
"vitest": "^4.0.0"
|
|
@@ -24,10 +24,9 @@
|
|
|
24
24
|
*
|
|
25
25
|
* The bootstrap only has to do enough to break the deadlock.
|
|
26
26
|
*
|
|
27
|
-
* The lists
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* SOURCE_HELPER_FILES exports.
|
|
27
|
+
* The script + helper lists are read at runtime from the canonical manifest
|
|
28
|
+
* bin/lib/shipped-scripts.json (#1191) — the SAME source the launcher §3 reads —
|
|
29
|
+
* so the npm-install path and the session-start path can no longer drift.
|
|
31
30
|
*
|
|
32
31
|
* Failure posture:
|
|
33
32
|
* - Surface per-file failures on stderr with `flo doctor --fix` advice
|
|
@@ -47,46 +46,16 @@ import {
|
|
|
47
46
|
import { dirname, join, resolve } from 'node:path';
|
|
48
47
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
49
48
|
import { errMessage, makeSyncer } from '../bin/lib/file-sync.mjs';
|
|
49
|
+
import { loadShippedScripts } from '../bin/lib/shipped-scripts.mjs';
|
|
50
50
|
|
|
51
51
|
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
52
52
|
const MOFLO_ROOT = resolve(dirname(SCRIPT_PATH), '..');
|
|
53
53
|
|
|
54
|
-
// ── Sync lists
|
|
54
|
+
// ── Sync lists ───────────────────────────────────────────────────────────────
|
|
55
55
|
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
export const SCRIPT_FILES = [
|
|
60
|
-
'hooks.mjs',
|
|
61
|
-
'session-start-launcher.mjs',
|
|
62
|
-
'index-guidance.mjs',
|
|
63
|
-
'build-embeddings.mjs',
|
|
64
|
-
'generate-code-map.mjs',
|
|
65
|
-
'semantic-search.mjs',
|
|
66
|
-
'index-tests.mjs',
|
|
67
|
-
'index-patterns.mjs',
|
|
68
|
-
'index-all.mjs',
|
|
69
|
-
'setup-project.mjs',
|
|
70
|
-
'run-migrations.mjs',
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
export const BIN_HELPER_FILES = [
|
|
74
|
-
'gate.cjs',
|
|
75
|
-
'gate-hook.mjs',
|
|
76
|
-
'prompt-hook.mjs',
|
|
77
|
-
'hook-handler.cjs',
|
|
78
|
-
'simplify-classify.cjs',
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
export const SOURCE_HELPER_FILES = [
|
|
82
|
-
'auto-memory-hook.mjs',
|
|
83
|
-
'statusline.cjs',
|
|
84
|
-
'intelligence.cjs',
|
|
85
|
-
'subagent-start.cjs',
|
|
86
|
-
'subagent-bootstrap.json',
|
|
87
|
-
'pre-commit',
|
|
88
|
-
'post-commit',
|
|
89
|
-
];
|
|
56
|
+
// The script + helper lists are read at runtime from the canonical manifest
|
|
57
|
+
// bin/lib/shipped-scripts.json (#1191) — the SAME source the launcher §3 reads —
|
|
58
|
+
// so the npm-install path and the session-start path can no longer drift.
|
|
90
59
|
|
|
91
60
|
// ── Retry + atomic copy + circuit breaker (#854 / #975) ─────────────────────
|
|
92
61
|
//
|
|
@@ -148,13 +117,24 @@ export async function runBootstrap({
|
|
|
148
117
|
return { ran: false, reason: 'no-bin-dir' };
|
|
149
118
|
}
|
|
150
119
|
|
|
120
|
+
// Canonical sync lists (#1191) — read from the freshly-installed package's
|
|
121
|
+
// bin/lib so the bootstrap and the launcher §3 can't drift. A broken manifest
|
|
122
|
+
// aborts gracefully (postinstall must never throw / block npm install).
|
|
123
|
+
let scriptFiles, binHelperFiles, sourceHelperFiles;
|
|
124
|
+
try {
|
|
125
|
+
({ scriptFiles, binHelperFiles, sourceHelperFiles } = loadShippedScripts(resolve(binDir, 'lib')));
|
|
126
|
+
} catch (err) {
|
|
127
|
+
log(`bootstrap: shipped-scripts manifest unreadable (${errMessage(err)}) — skipping`);
|
|
128
|
+
return { ran: false, reason: 'no-manifest' };
|
|
129
|
+
}
|
|
130
|
+
|
|
151
131
|
const { syncFile, failures } = makeSyncer();
|
|
152
132
|
let synced = 0;
|
|
153
133
|
|
|
154
134
|
// 1. Top-level scripts → .claude/scripts/
|
|
155
135
|
const scriptsDir = resolve(claudeDir, 'scripts');
|
|
156
136
|
if (!existsSync(scriptsDir)) mkdirSync(scriptsDir, { recursive: true });
|
|
157
|
-
for (const file of
|
|
137
|
+
for (const file of scriptFiles) {
|
|
158
138
|
const result = await syncFile(
|
|
159
139
|
resolve(binDir, file),
|
|
160
140
|
resolve(scriptsDir, file),
|
|
@@ -210,7 +190,7 @@ export async function runBootstrap({
|
|
|
210
190
|
// 4. bin/ helpers → .claude/helpers/
|
|
211
191
|
const helpersDir = resolve(claudeDir, 'helpers');
|
|
212
192
|
if (!existsSync(helpersDir)) mkdirSync(helpersDir, { recursive: true });
|
|
213
|
-
for (const file of
|
|
193
|
+
for (const file of binHelperFiles) {
|
|
214
194
|
const result = await syncFile(
|
|
215
195
|
resolve(binDir, file),
|
|
216
196
|
resolve(helpersDir, file),
|
|
@@ -223,7 +203,7 @@ export async function runBootstrap({
|
|
|
223
203
|
// (these never lived in bin/ — they're shipped via .claude/helpers/** in files[])
|
|
224
204
|
const sourceHelpersDir = resolve(mofloRoot, '.claude/helpers');
|
|
225
205
|
if (existsSync(sourceHelpersDir)) {
|
|
226
|
-
for (const file of
|
|
206
|
+
for (const file of sourceHelperFiles) {
|
|
227
207
|
const src = resolve(sourceHelpersDir, file);
|
|
228
208
|
if (!existsSync(src)) continue;
|
|
229
209
|
const result = await syncFile(src, resolve(helpersDir, file), `.claude/helpers/${file}`);
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* V3 LLM Hooks Module
|
|
3
|
-
*
|
|
4
|
-
* Exports LLM-specific hooks for request caching,
|
|
5
|
-
* optimization, cost tracking, and pattern learning.
|
|
6
|
-
*
|
|
7
|
-
* @module moflo/cli/hooks/llm
|
|
8
|
-
*/
|
|
9
|
-
export * from './llm-hooks.js';
|
|
10
|
-
export { llmHooks as default } from './llm-hooks.js';
|
|
11
|
-
//# sourceMappingURL=index.js.map
|