@pcircle/memesh 4.5.1 → 4.6.1
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-plugin/marketplace.json +5 -3
- package/.claude-plugin/plugin.json +6 -4
- package/AGENTS.md +116 -0
- package/README.de.md +141 -48
- package/README.md +173 -48
- package/README.zh-TW.md +142 -48
- package/dashboard/dist/index.html +15 -14
- package/dist/cli/view-live.js +3 -3
- package/dist/core/analytics.d.ts +9 -0
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js +36 -18
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/auto-tagger.d.ts.map +1 -1
- package/dist/core/auto-tagger.js +4 -9
- package/dist/core/auto-tagger.js.map +1 -1
- package/dist/core/briefing.d.ts +8 -0
- package/dist/core/briefing.d.ts.map +1 -0
- package/dist/core/briefing.js +92 -0
- package/dist/core/briefing.js.map +1 -0
- package/dist/core/capture-flag.d.ts +5 -0
- package/dist/core/capture-flag.d.ts.map +1 -0
- package/dist/core/capture-flag.js +10 -0
- package/dist/core/capture-flag.js.map +1 -0
- package/dist/core/config.d.ts +0 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/conflict-candidates.d.ts +20 -0
- package/dist/core/conflict-candidates.d.ts.map +1 -0
- package/dist/core/conflict-candidates.js +79 -0
- package/dist/core/conflict-candidates.js.map +1 -0
- package/dist/core/conflict-judge.d.ts +47 -0
- package/dist/core/conflict-judge.d.ts.map +1 -0
- package/dist/core/conflict-judge.js +189 -0
- package/dist/core/conflict-judge.js.map +1 -0
- package/dist/core/demo.d.ts.map +1 -1
- package/dist/core/demo.js +1 -1
- package/dist/core/demo.js.map +1 -1
- package/dist/core/digest-validator.d.ts.map +1 -1
- package/dist/core/digest-validator.js +3 -5
- package/dist/core/digest-validator.js.map +1 -1
- package/dist/core/doctor.d.ts +2 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +59 -62
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +5 -2
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +329 -25
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/embedder.d.ts +8 -4
- package/dist/core/embedder.d.ts.map +1 -1
- package/dist/core/embedder.js +82 -24
- package/dist/core/embedder.js.map +1 -1
- package/dist/core/failure-analyzer.d.ts.map +1 -1
- package/dist/core/failure-analyzer.js +7 -12
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/graph.d.ts +12 -0
- package/dist/core/graph.d.ts.map +1 -1
- package/dist/core/graph.js +56 -1
- package/dist/core/graph.js.map +1 -1
- package/dist/core/guards.d.ts +20 -0
- package/dist/core/guards.d.ts.map +1 -0
- package/dist/core/guards.js +103 -0
- package/dist/core/guards.js.map +1 -0
- package/dist/core/install-channel.d.ts +1 -1
- package/dist/core/install-channel.d.ts.map +1 -1
- package/dist/core/install-channel.js +16 -5
- package/dist/core/install-channel.js.map +1 -1
- package/dist/core/install-hooks.d.ts +5 -0
- package/dist/core/install-hooks.d.ts.map +1 -1
- package/dist/core/install-hooks.js +0 -0
- package/dist/core/install-hooks.js.map +1 -1
- package/dist/core/json-utils.d.ts +1 -0
- package/dist/core/json-utils.d.ts.map +1 -1
- package/dist/core/json-utils.js +19 -10
- package/dist/core/json-utils.js.map +1 -1
- package/dist/core/kg-backfill.d.ts +5 -2
- package/dist/core/kg-backfill.d.ts.map +1 -1
- package/dist/core/kg-backfill.js +155 -5
- package/dist/core/kg-backfill.js.map +1 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +14 -21
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/memory-tool.d.ts.map +1 -1
- package/dist/core/memory-tool.js +4 -4
- package/dist/core/memory-tool.js.map +1 -1
- package/dist/core/operations.d.ts +13 -2
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +115 -28
- package/dist/core/operations.js.map +1 -1
- package/dist/core/prompt-safety.d.ts +1 -0
- package/dist/core/prompt-safety.d.ts.map +1 -1
- package/dist/core/prompt-safety.js +7 -0
- package/dist/core/prompt-safety.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +31 -0
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +8 -0
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/setup.d.ts +29 -0
- package/dist/core/setup.d.ts.map +1 -0
- package/dist/core/setup.js +127 -0
- package/dist/core/setup.js.map +1 -0
- package/dist/core/task-state-store.d.ts +17 -0
- package/dist/core/task-state-store.d.ts.map +1 -0
- package/dist/core/task-state-store.js +45 -0
- package/dist/core/task-state-store.js.map +1 -0
- package/dist/core/task-state.d.ts +19 -0
- package/dist/core/task-state.d.ts.map +1 -0
- package/dist/core/task-state.js +91 -0
- package/dist/core/task-state.js.map +1 -0
- package/dist/core/time-utils.d.ts +2 -0
- package/dist/core/time-utils.d.ts.map +1 -0
- package/dist/core/time-utils.js +14 -0
- package/dist/core/time-utils.js.map +1 -0
- package/dist/core/title.d.ts +5 -0
- package/dist/core/title.d.ts.map +1 -0
- package/dist/core/title.js +14 -0
- package/dist/core/title.js.map +1 -0
- package/dist/core/transcript-source.d.ts.map +1 -1
- package/dist/core/transcript-source.js +2 -3
- package/dist/core/transcript-source.js.map +1 -1
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/why.d.ts +54 -0
- package/dist/core/why.d.ts.map +1 -0
- package/dist/core/why.js +168 -0
- package/dist/core/why.js.map +1 -0
- package/dist/core/work-topology.d.ts +36 -0
- package/dist/core/work-topology.d.ts.map +1 -0
- package/dist/core/work-topology.js +192 -0
- package/dist/core/work-topology.js.map +1 -0
- package/dist/db.d.ts +33 -11
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +307 -315
- package/dist/db.js.map +1 -1
- package/dist/knowledge-graph.d.ts +1 -0
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +50 -40
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/skills-manifest.json +62 -22
- package/dist/storage/conflicts.d.ts.map +1 -1
- package/dist/storage/conflicts.js +2 -7
- package/dist/storage/conflicts.js.map +1 -1
- package/dist/storage/fts-index.d.ts +4 -2
- package/dist/storage/fts-index.d.ts.map +1 -1
- package/dist/storage/fts-index.js +16 -4
- package/dist/storage/fts-index.js.map +1 -1
- package/dist/storage/schema.d.ts +20 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +274 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +1 -1
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/transports/cli/cli.d.ts +1 -4
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +579 -66
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +242 -303
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +46 -0
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +59 -4
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +29 -10
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +33 -8
- package/dist/transports/schemas.js.map +1 -1
- package/hooks/hooks.json +10 -0
- package/llms-install.md +138 -0
- package/package.json +14 -9
- package/scripts/hooks/_generated/capture-flag.js +17 -0
- package/scripts/hooks/_generated/fts-index.js +16 -4
- package/scripts/hooks/_generated/guards.js +110 -0
- package/scripts/hooks/_generated/schema.js +281 -0
- package/scripts/hooks/_generated/sqlite.js +1 -1
- package/scripts/hooks/_generated/task-state.js +98 -0
- package/scripts/hooks/_generated/time-utils.js +21 -0
- package/scripts/hooks/_generated/title.js +21 -0
- package/scripts/hooks/_generated/work-topology.js +199 -0
- package/scripts/hooks/_shared.js +197 -480
- package/scripts/hooks/guard-check.js +76 -0
- package/scripts/hooks/post-commit.js +31 -1
- package/scripts/hooks/pre-compact.js +13 -1
- package/scripts/hooks/pre-edit-recall.js +158 -120
- package/scripts/hooks/session-start.js +169 -82
- package/scripts/hooks/session-summary.js +78 -90
- package/skills/memesh/SKILL.md +108 -76
- package/README.es.md +0 -467
- package/README.fr.md +0 -459
- package/README.ja.md +0 -467
- package/README.ko.md +0 -467
- package/README.pt.md +0 -459
- package/README.th.md +0 -460
- package/README.vi.md +0 -459
- package/README.zh-CN.md +0 -466
- package/dist/cli/view.d.ts +0 -3
- package/dist/cli/view.d.ts.map +0 -1
- package/dist/cli/view.js +0 -523
- package/dist/cli/view.js.map +0 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Lesson guards — PreToolUse hook for Bash (G1).
|
|
4
|
+
//
|
|
5
|
+
// A guard is a failure-lesson a human promoted (via the dream proposal
|
|
6
|
+
// queue) to speak at the moment its mistake is about to repeat: a regex
|
|
7
|
+
// over the command about to run. This hook is the Bash evaluation point;
|
|
8
|
+
// the Edit/Write point lives inside pre-edit-recall, which is already
|
|
9
|
+
// wired to that matcher.
|
|
10
|
+
//
|
|
11
|
+
// Contract, in order of importance:
|
|
12
|
+
// 1. NEVER block or slow the user's work: every failure path is a
|
|
13
|
+
// silent pass (with a stderr trace), and v1 guards only WARN — the
|
|
14
|
+
// warning is additionalContext, exit code stays 0.
|
|
15
|
+
// 2. The guard message is memory content — attacker-influenced in the
|
|
16
|
+
// general case — so it is fenced by buildReferenceContext exactly
|
|
17
|
+
// like every other injection path.
|
|
18
|
+
// 3. Every fire is counted (metadata.guard.fires): a guard that never
|
|
19
|
+
// fires or fires constantly is a review item, and the count is what
|
|
20
|
+
// surfaces it.
|
|
21
|
+
|
|
22
|
+
import { existsSync } from 'fs';
|
|
23
|
+
import {
|
|
24
|
+
buildReferenceContext,
|
|
25
|
+
getDbPath,
|
|
26
|
+
loadActiveGuards,
|
|
27
|
+
matchingGuards,
|
|
28
|
+
guardWarningLines,
|
|
29
|
+
recordGuardFires,
|
|
30
|
+
} from './_shared.js';
|
|
31
|
+
import { MemeshDatabase } from './_generated/sqlite.js';
|
|
32
|
+
|
|
33
|
+
const dbPath = getDbPath();
|
|
34
|
+
|
|
35
|
+
let input = '';
|
|
36
|
+
process.stdin.setEncoding('utf8');
|
|
37
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
38
|
+
process.stdin.on('end', () => {
|
|
39
|
+
try {
|
|
40
|
+
const data = JSON.parse(input);
|
|
41
|
+
const command = data?.tool_input?.command;
|
|
42
|
+
if (!command || typeof command !== 'string') return pass();
|
|
43
|
+
if (!existsSync(dbPath)) return pass();
|
|
44
|
+
|
|
45
|
+
// `readOnly`, not `readonly`: node:sqlite ignores the lowercase
|
|
46
|
+
// spelling and hands back a WRITABLE handle. This hook only reads;
|
|
47
|
+
// the fire counter opens its own writable handle for the one UPDATE.
|
|
48
|
+
const db = new MemeshDatabase(dbPath, { readOnly: true });
|
|
49
|
+
let matches;
|
|
50
|
+
try {
|
|
51
|
+
matches = matchingGuards(loadActiveGuards(db, 'Bash'), 'Bash', command);
|
|
52
|
+
} finally {
|
|
53
|
+
db.close();
|
|
54
|
+
}
|
|
55
|
+
if (!matches || matches.length === 0) return pass();
|
|
56
|
+
|
|
57
|
+
recordGuardFires(dbPath, matches.map((g) => g.lessonId));
|
|
58
|
+
|
|
59
|
+
console.log(JSON.stringify({
|
|
60
|
+
hookSpecificOutput: {
|
|
61
|
+
hookEventName: 'PreToolUse',
|
|
62
|
+
additionalContext: buildReferenceContext(guardWarningLines(matches, 'Bash')),
|
|
63
|
+
},
|
|
64
|
+
}));
|
|
65
|
+
process.exit(0);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// Never crash Claude Code, but trace — a silent break here means
|
|
68
|
+
// every accepted guard stops firing and nothing reports it.
|
|
69
|
+
try { process.stderr.write(`[memesh guard-check] ${err?.message || err}\n`); } catch {}
|
|
70
|
+
pass();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function pass() {
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execFileSync } from 'child_process';
|
|
4
|
-
import { AUTO_CAPTURE_TAG, captureEntity, getProjectName, isAutoCaptureEnabled, openHookDb, recordHookRun } from './_shared.js';
|
|
4
|
+
import { AUTO_CAPTURE_TAG, captureEntity, getProjectName, isAutoCaptureEnabled, openHookDb, recordHookRun, truncateTitle } from './_shared.js';
|
|
5
5
|
|
|
6
6
|
let input = '';
|
|
7
7
|
process.stdin.setEncoding('utf8');
|
|
@@ -133,6 +133,32 @@ process.stdin.on('end', () => {
|
|
|
133
133
|
// git show failed — no diff stats recorded, existing behavior unchanged
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
// Commit → session linkage, the hop `memesh why` walks. The payload has
|
|
137
|
+
// carried session_id all along; this hook just never recorded it, so
|
|
138
|
+
// every commit entity was an island (no file list, no session, no
|
|
139
|
+
// relations). Recorded as METADATA, not tags, on purpose: a `file:*`
|
|
140
|
+
// tag here would make pre-edit-recall inject commit noise into every
|
|
141
|
+
// edit of a touched file (Strategy 1 joins on exactly that tag).
|
|
142
|
+
const whyMetadata = {};
|
|
143
|
+
if (typeof data.session_id === 'string' && data.session_id) {
|
|
144
|
+
whyMetadata.session_id = data.session_id;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const nameOnly = execFileSync('git', ['-C', data.cwd, 'show', '--name-only', '--format=', commitHash], {
|
|
148
|
+
encoding: 'utf8',
|
|
149
|
+
timeout: 5000,
|
|
150
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
151
|
+
}).trim();
|
|
152
|
+
if (nameOnly) {
|
|
153
|
+
// Repo-relative paths, capped: a lockfile-churn commit can touch
|
|
154
|
+
// thousands of files and metadata rides every entity read.
|
|
155
|
+
const files = nameOnly.split('\n').map(l => l.trim()).filter(Boolean);
|
|
156
|
+
if (files.length > 0) whyMetadata.files = files.slice(0, 50);
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// Best-effort like the diff stats above — no file list recorded.
|
|
160
|
+
}
|
|
161
|
+
|
|
136
162
|
// Shared write dance — upsert entity + observations + tags AND reindex FTS.
|
|
137
163
|
//
|
|
138
164
|
// `source:auto-capture` is the provenance marker every capture hook
|
|
@@ -140,11 +166,15 @@ process.stdin.on('end', () => {
|
|
|
140
166
|
// this one did not). `memesh doctor` counts it to answer "is the
|
|
141
167
|
// auto-capture loop alive" — a question it used to answer from entity
|
|
142
168
|
// TYPE, which a hand-typed `memesh learn` satisfied all by itself.
|
|
169
|
+
// The commit subject IS the title — git authors already wrote a
|
|
170
|
+
// one-line human summary; nothing to synthesize.
|
|
143
171
|
const written = captureEntity(db, {
|
|
144
172
|
name: entityName,
|
|
145
173
|
type: 'commit',
|
|
146
174
|
observations,
|
|
147
175
|
tags: [AUTO_CAPTURE_TAG, `project:${projectName}`],
|
|
176
|
+
title: truncateTitle(commitMsg),
|
|
177
|
+
metadata: whyMetadata,
|
|
148
178
|
});
|
|
149
179
|
|
|
150
180
|
// Heartbeat AFTER capture, so the stamp certifies "the capture loop
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { basename } from 'path';
|
|
4
4
|
import { existsSync, readFileSync } from 'fs';
|
|
5
|
-
import { AUTO_CAPTURE_TAG, captureEntity, getProjectName, isAutoCaptureEnabled, openHookDb, recordHookRun } from './_shared.js';
|
|
5
|
+
import { AUTO_CAPTURE_TAG, captureEntity, getProjectName, isAutoCaptureEnabled, openHookDb, recordHookRun, truncateTitle } from './_shared.js';
|
|
6
6
|
|
|
7
7
|
// Timeout guard: always exit within 10 seconds
|
|
8
8
|
const TIMEOUT_MS = 10000;
|
|
@@ -95,6 +95,17 @@ process.stdin.on('end', () => {
|
|
|
95
95
|
|
|
96
96
|
// Build observation content
|
|
97
97
|
const obsLines = [`Compaction reason: ${reason}`, `Tool calls: ${toolCallCount}`];
|
|
98
|
+
|
|
99
|
+
// No free-form human text exists for a pre-compact save (unlike a commit
|
|
100
|
+
// subject) — same date+project+verb heuristic as session-summary.js.
|
|
101
|
+
// This hook was the one missed by the original design (it hand-rolls the
|
|
102
|
+
// same captureEntity dance as its two siblings but was overlooked as a
|
|
103
|
+
// "write hook" until a code audit found it), so its entities were the
|
|
104
|
+
// clearest case of a raw machine key (`pre-compact-<sessionId>`) with a
|
|
105
|
+
// terse observation ("Compaction reason: manual") standing in as the
|
|
106
|
+
// display label.
|
|
107
|
+
const titleDate = new Date().toISOString().slice(0, 10);
|
|
108
|
+
const title = truncateTitle(`${titleDate} ${projectName}: ${reason} compaction (${toolCallCount} tool calls)`);
|
|
98
109
|
if (editedFiles.size > 0) {
|
|
99
110
|
obsLines.push(`Files edited: ${Array.from(editedFiles).join(', ')}`);
|
|
100
111
|
}
|
|
@@ -111,6 +122,7 @@ process.stdin.on('end', () => {
|
|
|
111
122
|
type: 'session-summary',
|
|
112
123
|
observations: obsLines,
|
|
113
124
|
tags: [AUTO_CAPTURE_TAG, 'urgency:pre-compact', `project:${projectName}`],
|
|
125
|
+
title,
|
|
114
126
|
});
|
|
115
127
|
|
|
116
128
|
// Heartbeat AFTER capture, so the stamp certifies "the capture loop
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
// Continuous Recall — PreToolUse hook for Edit/Write
|
|
4
4
|
// When editing a file, checks if MeMesh has relevant memories
|
|
5
5
|
// and injects them as context. Throttled: max 1 recall per file per session.
|
|
6
|
+
//
|
|
7
|
+
// Also the Edit/Write evaluation point for lesson guards (G1): accepted
|
|
8
|
+
// guards match against the path plus the content about to be written, and
|
|
9
|
+
// a hit injects the lesson's warning at the exact moment its mistake is
|
|
10
|
+
// about to repeat. Guards are NOT throttled — a dangerous edit is
|
|
11
|
+
// dangerous every time.
|
|
6
12
|
|
|
7
13
|
import { basename, join } from 'path';
|
|
8
14
|
import { existsSync, readFileSync } from 'fs';
|
|
@@ -15,6 +21,10 @@ import {
|
|
|
15
21
|
isTrustedForAutoContext,
|
|
16
22
|
writePrivateJson,
|
|
17
23
|
hookMatchExpression,
|
|
24
|
+
loadActiveGuards,
|
|
25
|
+
matchingGuards,
|
|
26
|
+
guardWarningLines,
|
|
27
|
+
recordGuardFires,
|
|
18
28
|
} from './_shared.js';
|
|
19
29
|
import { MemeshDatabase } from './_generated/sqlite.js';
|
|
20
30
|
|
|
@@ -43,10 +53,9 @@ process.stdin.on('end', () => {
|
|
|
43
53
|
return pass();
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// Throttle: skip if we already recalled for this file
|
|
56
|
+
// Throttle — the RECALL half runs once per file per session. The guard
|
|
57
|
+
// half is deliberately outside it, so a throttled call still opens the
|
|
58
|
+
// database for the guard pass.
|
|
50
59
|
const fileKey = filePath.toLowerCase();
|
|
51
60
|
let seenFiles = [];
|
|
52
61
|
try {
|
|
@@ -57,146 +66,175 @@ process.stdin.on('end', () => {
|
|
|
57
66
|
} catch {
|
|
58
67
|
seenFiles = [];
|
|
59
68
|
}
|
|
60
|
-
|
|
61
|
-
if (seenFiles.includes(fileKey)) {
|
|
62
|
-
return pass();
|
|
63
|
-
}
|
|
69
|
+
const throttled = seenFiles.includes(fileKey);
|
|
64
70
|
|
|
65
71
|
if (!existsSync(dbPath)) return pass();
|
|
66
72
|
|
|
73
|
+
// Get project name from cwd for project-scoped RECALL filtering — this
|
|
74
|
+
// spawns 1-2 git subprocesses, and the throttled path (every repeat
|
|
75
|
+
// edit of the same file) must not pay for it. Guards are global by
|
|
76
|
+
// design: a mistake recorded in one project is usually a mistake
|
|
77
|
+
// everywhere (the pipe-eats-exit-code shape), and the warning names
|
|
78
|
+
// its source lesson either way.
|
|
79
|
+
const projectName = throttled ? null : getProjectName(data.cwd);
|
|
80
|
+
|
|
67
81
|
// `readOnly`, not `readonly`: node:sqlite ignores the lowercase spelling
|
|
68
|
-
// and hands back a WRITABLE handle. This hook only reads
|
|
82
|
+
// and hands back a WRITABLE handle. This hook only reads; the guard
|
|
83
|
+
// fire counter opens its own writable handle for its one UPDATE.
|
|
69
84
|
const db = new MemeshDatabase(dbPath, { readOnly: true });
|
|
85
|
+
let guardMatches = [];
|
|
86
|
+
const recallLines = [];
|
|
70
87
|
try {
|
|
88
|
+
// Guard pass (G1) — matched against the path plus the content about
|
|
89
|
+
// to land, which is what the mistake would be made OF.
|
|
90
|
+
const toolName = data.tool_name === 'Write' ? 'Write' : 'Edit';
|
|
91
|
+
const guardHaystack = `${filePath}\n${toolInput.new_string ?? toolInput.content ?? ''}`;
|
|
92
|
+
guardMatches = matchingGuards(loadActiveGuards(db, toolName), toolName, guardHaystack);
|
|
71
93
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.some(c => c.name === 'status');
|
|
89
|
-
const statusFilter = hasStatus ? "AND e.status = 'active'" : '';
|
|
94
|
+
// Recall pass — throttled, project-scoped.
|
|
95
|
+
if (!throttled) {
|
|
96
|
+
// Check if entities table exists
|
|
97
|
+
// Both tables, not just `entities`. Strategy 2 below joins entities_fts,
|
|
98
|
+
// and this hook opens the database READ-ONLY without going through
|
|
99
|
+
// openHookDb, so it never creates that table. Checking only `entities`
|
|
100
|
+
// meant a structurally-absent index reached the query and failed there —
|
|
101
|
+
// which, now that the failure is no longer swallowed, would print on
|
|
102
|
+
// every single Edit.
|
|
103
|
+
const tables = new Set(
|
|
104
|
+
db.prepare(
|
|
105
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('entities','entities_fts')"
|
|
106
|
+
).all().map((r) => r.name)
|
|
107
|
+
);
|
|
108
|
+
if (tables.has('entities')) {
|
|
109
|
+
const hasFts = tables.has('entities_fts');
|
|
90
110
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// 3. FTS5 search on the basename (without extension)
|
|
95
|
-
const fileName = basename(filePath);
|
|
96
|
-
const fileNameNoExt = fileName.replace(/\.[^.]+$/, '');
|
|
111
|
+
const hasStatus = db.prepare("PRAGMA table_info(entities)").all()
|
|
112
|
+
.some(c => c.name === 'status');
|
|
113
|
+
const statusFilter = hasStatus ? "AND e.status = 'active'" : '';
|
|
97
114
|
|
|
98
|
-
|
|
115
|
+
// Search strategies:
|
|
116
|
+
// 1. Entities tagged with the file's basename (e.g., "file:auth.ts")
|
|
117
|
+
// 2. Entities with name matching the file's basename (without extension)
|
|
118
|
+
// 3. FTS5 search on the basename (without extension)
|
|
119
|
+
const fileName = basename(filePath);
|
|
120
|
+
const fileNameNoExt = fileName.replace(/\.[^.]+$/, '');
|
|
99
121
|
|
|
100
|
-
|
|
101
|
-
// CRITICAL: Filter by project to prevent cross-project memory injection
|
|
102
|
-
const projectTag = `project:${projectName}`;
|
|
103
|
-
const tagResults = db.prepare(`
|
|
104
|
-
SELECT DISTINCT e.id, e.name, e.type, e.metadata
|
|
105
|
-
FROM entities e
|
|
106
|
-
JOIN tags t1 ON t1.entity_id = e.id
|
|
107
|
-
JOIN tags t2 ON t2.entity_id = e.id
|
|
108
|
-
WHERE (t1.tag = ? OR t1.tag = ?)
|
|
109
|
-
AND t2.tag = ?
|
|
110
|
-
${statusFilter}
|
|
111
|
-
LIMIT ?
|
|
112
|
-
`).all(`file:${fileName}`, `file:${fileNameNoExt}`, projectTag, MAX_RESULTS * 3);
|
|
113
|
-
results.push(...tagResults.filter((row) => isTrustedForAutoContext(row.metadata)));
|
|
122
|
+
const results = [];
|
|
114
123
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// tokens the index actually holds. Quoting the raw basename here meant
|
|
120
|
-
// a CJK or decomposed-Unicode filename matched nothing at all against
|
|
121
|
-
// the segmented index — and the catch below made that invisible.
|
|
122
|
-
const matchExpr = hookMatchExpression(fileNameNoExt);
|
|
123
|
-
try {
|
|
124
|
-
// ORDER BY rank is load-bearing now that terms are OR-ed.
|
|
125
|
-
//
|
|
126
|
-
// The match expression used to be a single phrase, so `LIMIT` picked
|
|
127
|
-
// from a handful of rows that all genuinely contained the basename and
|
|
128
|
-
// arbitrary selection was tolerable. `hookMatchExpression` now emits
|
|
129
|
-
// `"knowledge" OR "graph"` for `knowledge-graph.ts` — necessary,
|
|
130
|
-
// because a CJK basename has to be reachable by its bigrams — which
|
|
131
|
-
// makes the match set large and unranked selection IS the result:
|
|
132
|
-
// editing that file in a project whose memories merely mention
|
|
133
|
-
// "graph" injected whatever the scan happened to reach first, where
|
|
134
|
-
// the old code correctly injected nothing. BM25 is what makes the OR
|
|
135
|
-
// safe; without it the fix trades a CJK miss for an ASCII false hit.
|
|
136
|
-
const ftsResults = matchExpr === null ? [] : db.prepare(`
|
|
124
|
+
// Strategy 1: Tag-based search (file:name or mentions of the file)
|
|
125
|
+
// CRITICAL: Filter by project to prevent cross-project memory injection
|
|
126
|
+
const projectTag = `project:${projectName}`;
|
|
127
|
+
const tagResults = db.prepare(`
|
|
137
128
|
SELECT DISTINCT e.id, e.name, e.type, e.metadata
|
|
138
129
|
FROM entities e
|
|
139
|
-
JOIN
|
|
140
|
-
JOIN tags
|
|
141
|
-
WHERE
|
|
142
|
-
AND
|
|
130
|
+
JOIN tags t1 ON t1.entity_id = e.id
|
|
131
|
+
JOIN tags t2 ON t2.entity_id = e.id
|
|
132
|
+
WHERE (t1.tag = ? OR t1.tag = ?)
|
|
133
|
+
AND t2.tag = ?
|
|
143
134
|
${statusFilter}
|
|
144
|
-
ORDER BY fts.rank, e.id DESC
|
|
145
135
|
LIMIT ?
|
|
146
|
-
`).all(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
136
|
+
`).all(`file:${fileName}`, `file:${fileNameNoExt}`, projectTag, MAX_RESULTS * 3);
|
|
137
|
+
results.push(...tagResults.filter((row) => isTrustedForAutoContext(row.metadata)));
|
|
138
|
+
|
|
139
|
+
// Strategy 2: FTS5 search on file name (if not enough results)
|
|
140
|
+
// CRITICAL: Filter by project to prevent cross-project memory injection
|
|
141
|
+
if (hasFts && results.length < MAX_RESULTS && fileNameNoExt.length >= 4) {
|
|
142
|
+
// Built by the same function core uses, so this query asks for the
|
|
143
|
+
// tokens the index actually holds. Quoting the raw basename here meant
|
|
144
|
+
// a CJK or decomposed-Unicode filename matched nothing at all against
|
|
145
|
+
// the segmented index — and the catch below made that invisible.
|
|
146
|
+
const matchExpr = hookMatchExpression(fileNameNoExt);
|
|
147
|
+
try {
|
|
148
|
+
// ORDER BY rank is load-bearing now that terms are OR-ed.
|
|
149
|
+
//
|
|
150
|
+
// The match expression used to be a single phrase, so `LIMIT` picked
|
|
151
|
+
// from a handful of rows that all genuinely contained the basename and
|
|
152
|
+
// arbitrary selection was tolerable. `hookMatchExpression` now emits
|
|
153
|
+
// `"knowledge" OR "graph"` for `knowledge-graph.ts` — necessary,
|
|
154
|
+
// because a CJK basename has to be reachable by its bigrams — which
|
|
155
|
+
// makes the match set large and unranked selection IS the result:
|
|
156
|
+
// editing that file in a project whose memories merely mention
|
|
157
|
+
// "graph" injected whatever the scan happened to reach first, where
|
|
158
|
+
// the old code correctly injected nothing. BM25 is what makes the OR
|
|
159
|
+
// safe; without it the fix trades a CJK miss for an ASCII false hit.
|
|
160
|
+
const ftsResults = matchExpr === null ? [] : db.prepare(`
|
|
161
|
+
SELECT DISTINCT e.id, e.name, e.type, e.metadata
|
|
162
|
+
FROM entities e
|
|
163
|
+
JOIN entities_fts fts ON fts.rowid = e.id
|
|
164
|
+
JOIN tags t ON t.entity_id = e.id
|
|
165
|
+
WHERE entities_fts MATCH ?
|
|
166
|
+
AND t.tag = ?
|
|
167
|
+
${statusFilter}
|
|
168
|
+
ORDER BY fts.rank, e.id DESC
|
|
169
|
+
LIMIT ?
|
|
170
|
+
`).all(matchExpr, projectTag, (MAX_RESULTS - results.length) * 3);
|
|
171
|
+
// Deduplicate
|
|
172
|
+
for (const r of ftsResults) {
|
|
173
|
+
if (!isTrustedForAutoContext(r.metadata)) continue;
|
|
174
|
+
if (!results.some(existing => existing.id === r.id)) {
|
|
175
|
+
results.push(r);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
// Never fail the user's edit over a recall miss, but do not pretend
|
|
180
|
+
// nothing happened either: a silently-skipped FTS query is how this
|
|
181
|
+
// hook injected zero memories for months without anyone noticing.
|
|
182
|
+
//
|
|
183
|
+
// Throttled, because PreToolUse fires a fresh process per Edit/Write
|
|
184
|
+
// and a persistent fault would otherwise print on every keystroke's
|
|
185
|
+
// worth of tool calls. Once per distinct message per day is enough to
|
|
186
|
+
// be noticed without becoming noise the user learns to ignore.
|
|
187
|
+
reportOnce(`fts:${err?.message || err}`, `filename search failed: ${err?.message || err}`);
|
|
152
188
|
}
|
|
153
189
|
}
|
|
154
|
-
} catch (err) {
|
|
155
|
-
// Never fail the user's edit over a recall miss, but do not pretend
|
|
156
|
-
// nothing happened either: a silently-skipped FTS query is how this
|
|
157
|
-
// hook injected zero memories for months without anyone noticing.
|
|
158
|
-
//
|
|
159
|
-
// Throttled, because PreToolUse fires a fresh process per Edit/Write
|
|
160
|
-
// and a persistent fault would otherwise print on every keystroke's
|
|
161
|
-
// worth of tool calls. Once per distinct message per day is enough to
|
|
162
|
-
// be noticed without becoming noise the user learns to ignore.
|
|
163
|
-
reportOnce(`fts:${err?.message || err}`, `filename search failed: ${err?.message || err}`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
190
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
191
|
+
if (results.length > 0) {
|
|
192
|
+
// Fetch first observation for each result
|
|
193
|
+
const getObs = db.prepare(
|
|
194
|
+
'SELECT content FROM observations WHERE entity_id = ? ORDER BY id ASC LIMIT 1'
|
|
195
|
+
);
|
|
172
196
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
197
|
+
recallLines.push(`Relevant memories for ${fileName}:`);
|
|
198
|
+
for (const r of results.slice(0, MAX_RESULTS)) {
|
|
199
|
+
const obs = getObs.get(r.id);
|
|
200
|
+
const snippet = obs ? obs.content.slice(0, 120) : '';
|
|
201
|
+
recallLines.push(snippet
|
|
202
|
+
? `• ${r.name} (${r.type}): ${snippet}`
|
|
203
|
+
: `• ${r.name} (${r.type})`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
177
207
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const snippet = obs ? obs.content.slice(0, 120) : '';
|
|
182
|
-
lines.push(snippet
|
|
183
|
-
? `• ${r.name} (${r.type}): ${snippet}`
|
|
184
|
-
: `• ${r.name} (${r.type})`
|
|
185
|
-
);
|
|
208
|
+
// Record as seen either way (avoid re-querying a no-result file)
|
|
209
|
+
recordSeen(seenFiles, fileKey);
|
|
210
|
+
}
|
|
186
211
|
}
|
|
187
|
-
|
|
188
|
-
// Record as seen
|
|
189
|
-
recordSeen(seenFiles, fileKey);
|
|
190
|
-
|
|
191
|
-
console.log(JSON.stringify({
|
|
192
|
-
hookSpecificOutput: {
|
|
193
|
-
hookEventName: 'PreToolUse',
|
|
194
|
-
additionalContext: buildReferenceContext(lines),
|
|
195
|
-
},
|
|
196
|
-
}));
|
|
197
212
|
} finally {
|
|
198
213
|
db.close();
|
|
199
214
|
}
|
|
215
|
+
|
|
216
|
+
if (guardMatches.length === 0 && recallLines.length === 0) {
|
|
217
|
+
return pass();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (guardMatches.length > 0) {
|
|
221
|
+
recordGuardFires(dbPath, guardMatches.map((g) => g.lessonId));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// One fenced block, guards first — the warning about the edit at hand
|
|
225
|
+
// outranks background recall. Both halves are memory content, so both
|
|
226
|
+
// ride the same "background data" fence.
|
|
227
|
+
const toolLabel = data.tool_name === 'Write' ? 'Write' : 'Edit';
|
|
228
|
+
const lines = guardMatches.length > 0
|
|
229
|
+
? [...guardWarningLines(guardMatches, toolLabel), ...(recallLines.length > 0 ? ['', ...recallLines] : [])]
|
|
230
|
+
: recallLines;
|
|
231
|
+
|
|
232
|
+
console.log(JSON.stringify({
|
|
233
|
+
hookSpecificOutput: {
|
|
234
|
+
hookEventName: 'PreToolUse',
|
|
235
|
+
additionalContext: buildReferenceContext(lines),
|
|
236
|
+
},
|
|
237
|
+
}));
|
|
200
238
|
} catch (err) {
|
|
201
239
|
// Never crash Claude Code, but trace — peer hooks (post-commit,
|
|
202
240
|
// pre-compact, session-summary) all stderr-trace their outer
|