@pcircle/memesh 4.4.0 → 4.5.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +1 -1
- package/README.de.md +32 -12
- package/README.es.md +13 -12
- package/README.fr.md +31 -11
- package/README.ja.md +13 -12
- package/README.ko.md +14 -13
- package/README.md +41 -16
- package/README.pt.md +30 -10
- package/README.th.md +16 -9
- package/README.vi.md +32 -12
- package/README.zh-CN.md +14 -13
- package/README.zh-TW.md +14 -13
- package/dashboard/dist/index.html +10 -10
- package/dist/cli/view-live.js +1 -1
- package/dist/cli/view.d.ts.map +1 -1
- package/dist/cli/view.js +2 -2
- package/dist/cli/view.js.map +1 -1
- package/dist/core/analytics.d.ts +7 -3
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js +8 -8
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/auto-tagger.d.ts.map +1 -1
- package/dist/core/auto-tagger.js.map +1 -1
- package/dist/core/config.d.ts +5 -3
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +30 -14
- package/dist/core/config.js.map +1 -1
- package/dist/core/demo.d.ts +2 -2
- package/dist/core/demo.d.ts.map +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 -1
- package/dist/core/digest-validator.js.map +1 -1
- package/dist/core/doctor.d.ts +3 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +255 -101
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +35 -7
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +394 -41
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/embedder.d.ts +3 -5
- package/dist/core/embedder.d.ts.map +1 -1
- package/dist/core/embedder.js +26 -95
- package/dist/core/embedder.js.map +1 -1
- package/dist/core/extractor.d.ts.map +1 -1
- package/dist/core/extractor.js +2 -1
- package/dist/core/extractor.js.map +1 -1
- package/dist/core/failure-analyzer.d.ts.map +1 -1
- package/dist/core/failure-analyzer.js +2 -1
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/graph.d.ts +4 -4
- package/dist/core/graph.d.ts.map +1 -1
- package/dist/core/graph.js.map +1 -1
- package/dist/core/install-hooks.d.ts +1 -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/kg-backfill.d.ts +4 -4
- package/dist/core/kg-backfill.d.ts.map +1 -1
- package/dist/core/kg-backfill.js +1 -1
- package/dist/core/kg-backfill.js.map +1 -1
- package/dist/core/lesson-engine.d.ts +1 -0
- package/dist/core/lesson-engine.d.ts.map +1 -1
- package/dist/core/lesson-engine.js +1 -0
- package/dist/core/lesson-engine.js.map +1 -1
- package/dist/core/lifecycle.d.ts +4 -4
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +1 -1
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/llm-client.d.ts.map +1 -1
- package/dist/core/llm-client.js +3 -6
- package/dist/core/llm-client.js.map +1 -1
- package/dist/core/llm-telemetry.d.ts +4 -4
- package/dist/core/llm-telemetry.d.ts.map +1 -1
- package/dist/core/llm-telemetry.js +1 -1
- package/dist/core/llm-telemetry.js.map +1 -1
- package/dist/core/llm-validator.d.ts +1 -0
- package/dist/core/llm-validator.d.ts.map +1 -1
- package/dist/core/llm-validator.js +33 -10
- package/dist/core/llm-validator.js.map +1 -1
- package/dist/core/memory-tool.d.ts.map +1 -1
- package/dist/core/memory-tool.js +4 -0
- package/dist/core/memory-tool.js.map +1 -1
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +23 -8
- package/dist/core/operations.js.map +1 -1
- package/dist/core/output-language.d.ts +6 -0
- package/dist/core/output-language.d.ts.map +1 -0
- package/dist/core/output-language.js +25 -0
- package/dist/core/output-language.js.map +1 -0
- package/dist/core/paths.d.ts +3 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +67 -1
- package/dist/core/paths.js.map +1 -1
- package/dist/core/patterns.d.ts +2 -3
- package/dist/core/patterns.d.ts.map +1 -1
- package/dist/core/patterns.js +1 -5
- package/dist/core/patterns.js.map +1 -1
- package/dist/core/project-tags.d.ts +3 -3
- package/dist/core/project-tags.d.ts.map +1 -1
- package/dist/core/project-tags.js.map +1 -1
- package/dist/core/projects.d.ts +2 -2
- package/dist/core/projects.d.ts.map +1 -1
- package/dist/core/projects.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +0 -34
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +45 -4
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/stats.d.ts +2 -2
- package/dist/core/stats.d.ts.map +1 -1
- package/dist/core/stats.js.map +1 -1
- package/dist/core/transcript-extractor.d.ts +88 -0
- package/dist/core/transcript-extractor.d.ts.map +1 -0
- package/dist/core/transcript-extractor.js +417 -0
- package/dist/core/transcript-extractor.js.map +1 -0
- package/dist/core/transcript-source.d.ts +21 -0
- package/dist/core/transcript-source.d.ts.map +1 -0
- package/dist/core/transcript-source.js +142 -0
- package/dist/core/transcript-source.js.map +1 -0
- package/dist/core/types.d.ts +17 -7
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -1
- package/dist/db.d.ts +5 -5
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +165 -17
- package/dist/db.js.map +1 -1
- package/dist/knowledge-graph.d.ts +3 -2
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +19 -10
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/mcp/server.js +2 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/skills-manifest.json +28 -33
- package/dist/storage/conflicts.d.ts +3 -3
- package/dist/storage/conflicts.d.ts.map +1 -1
- package/dist/storage/conflicts.js.map +1 -1
- package/dist/storage/fts-index.d.ts +4 -4
- package/dist/storage/fts-index.d.ts.map +1 -1
- package/dist/storage/fts-index.js.map +1 -1
- package/dist/storage/sqlite.d.ts +20 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +64 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/storage/vector-index.d.ts +3 -0
- package/dist/storage/vector-index.d.ts.map +1 -0
- package/dist/storage/vector-index.js +7 -0
- package/dist/storage/vector-index.js.map +1 -0
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +278 -91
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/retired-routes.d.ts.map +1 -1
- package/dist/transports/http/retired-routes.js +1 -0
- package/dist/transports/http/retired-routes.js.map +1 -1
- package/dist/transports/http/server.d.ts +7 -0
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +165 -79
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +4 -96
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +26 -59
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +10 -30
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +5 -29
- package/dist/transports/schemas.js.map +1 -1
- package/hooks/hooks.json +0 -10
- package/package.json +6 -19
- package/scripts/hooks/_generated/core-paths.js +67 -1
- package/scripts/hooks/_generated/sqlite.js +71 -0
- package/scripts/hooks/_shared.js +286 -195
- package/scripts/hooks/post-commit.js +52 -10
- package/scripts/hooks/pre-compact.js +10 -7
- package/scripts/hooks/pre-edit-recall.js +4 -8
- package/scripts/hooks/session-start.js +95 -47
- package/scripts/hooks/session-summary.js +120 -52
- package/scripts/hooks/user-prompt-intent.js +3 -2
- package/skills/memesh/SKILL.md +3 -2
- package/dist/core/skill-usage-log.d.ts +0 -11
- package/dist/core/skill-usage-log.d.ts.map +0 -1
- package/dist/core/skill-usage-log.js +0 -125
- package/dist/core/skill-usage-log.js.map +0 -1
- package/dist/core/verifier.d.ts +0 -40
- package/dist/core/verifier.d.ts.map +0 -1
- package/dist/core/verifier.js +0 -206
- package/dist/core/verifier.js.map +0 -1
- package/dist/mcp/launcher.d.ts +0 -3
- package/dist/mcp/launcher.d.ts.map +0 -1
- package/dist/mcp/launcher.js +0 -37
- package/dist/mcp/launcher.js.map +0 -1
- package/scripts/hooks/pre-bash-orchestration-nudge.js +0 -155
- package/scripts/postinstall-rebuild.mjs +0 -41
- package/skills/agentic-orchestration/SKILL.md +0 -399
package/scripts/hooks/_shared.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
|
-
import {
|
|
4
|
+
import { MemeshDatabase } from './_generated/sqlite.js';
|
|
5
5
|
import { dirname, join } from 'path';
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
7
7
|
|
|
@@ -23,7 +23,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
|
|
|
23
23
|
// did, but the copy is byte-locked to core and CI-gated (`git diff` on rebuild +
|
|
24
24
|
// `tests/hooks/mirror-parity.test.ts`), making drift structurally impossible.
|
|
25
25
|
//
|
|
26
|
-
// Re-exported here so all
|
|
26
|
+
// Re-exported here so all 6 hooks keep importing these names from `_shared.js`
|
|
27
27
|
// unchanged.
|
|
28
28
|
import {
|
|
29
29
|
memeshDir,
|
|
@@ -126,28 +126,17 @@ export function readHookConfig(_env = process.env) {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/**
|
|
130
|
-
* Resolve the agentic-orchestration opt-in flag.
|
|
131
|
-
* Precedence: env > config > default(false).
|
|
132
|
-
* Env semantics preserved: only `=== '1'` enables (avoids accidental
|
|
133
|
-
* truthy unlock from a stray env value).
|
|
134
|
-
*
|
|
135
|
-
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
136
|
-
* @returns {boolean}
|
|
137
|
-
*/
|
|
138
|
-
export function isAgenticOrchestrationEnabled(env = process.env) {
|
|
139
|
-
const envVal = env.MEMESH_ENABLE_AGENTIC_ORCHESTRATION;
|
|
140
|
-
if (envVal !== undefined) return envVal === '1';
|
|
141
|
-
const cfg = readHookConfig(env);
|
|
142
|
-
return cfg.enableAgenticOrchestration === true;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
129
|
/**
|
|
146
130
|
* Resolve the auto-capture flag.
|
|
147
131
|
* Precedence: env > config > default(true).
|
|
148
132
|
* Env semantics preserved: explicit `=== 'false'` disables; any other
|
|
149
133
|
* value (including undefined) leaves it on or defers to config.
|
|
150
134
|
*
|
|
135
|
+
* src/core/doctor.ts duplicates this precedence (isAutoCaptureOff — the
|
|
136
|
+
* TS/hook-JS bundle boundary forbids sharing code). A semantic change here
|
|
137
|
+
* MUST be mirrored there, or doctor starts reasoning about a disabled state
|
|
138
|
+
* the hooks don't agree on.
|
|
139
|
+
*
|
|
151
140
|
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
152
141
|
* @returns {boolean}
|
|
153
142
|
*/
|
|
@@ -180,6 +169,19 @@ export function resolveSessionLimit(env = process.env) {
|
|
|
180
169
|
return 10;
|
|
181
170
|
}
|
|
182
171
|
|
|
172
|
+
/**
|
|
173
|
+
* The tag every capture hook attaches to what it writes.
|
|
174
|
+
*
|
|
175
|
+
* `memesh doctor`'s hook-activity row counts THIS to answer "is the
|
|
176
|
+
* auto-capture loop alive" — it used to answer from entity type, and one of
|
|
177
|
+
* those types is what `memesh learn` writes by hand. The constant lives in
|
|
178
|
+
* `src/core/types.ts` for the TypeScript side; the hooks are plain .js loaded
|
|
179
|
+
* by Claude Code and cannot import it, so this is the one mirror.
|
|
180
|
+
* `tests/auto-capture-provenance.test.ts` fails if the two ever disagree, or
|
|
181
|
+
* if a capture hook stops writing it.
|
|
182
|
+
*/
|
|
183
|
+
export const AUTO_CAPTURE_TAG = 'source:auto-capture';
|
|
184
|
+
|
|
183
185
|
const VALID_AUTO_UPDATE_POLICIES = new Set(['off', 'patch', 'minor', 'major']);
|
|
184
186
|
|
|
185
187
|
/**
|
|
@@ -251,13 +253,11 @@ CREATE TABLE IF NOT EXISTS tags (
|
|
|
251
253
|
|
|
252
254
|
CREATE INDEX IF NOT EXISTS idx_tags_entity ON tags(entity_id);
|
|
253
255
|
CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);
|
|
254
|
-
DELETE
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
);
|
|
260
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_entity_tag_unique ON tags(entity_id, tag);
|
|
256
|
+
-- The tags dedup DELETE + idx_tags_entity_tag_unique creation live in
|
|
257
|
+
-- ensureTagsUniqueIndex(), AFTER this exec — the DELETE is a one-time
|
|
258
|
+
-- migration, and a DML statement in this string made every open start a
|
|
259
|
+
-- write transaction even when it deleted nothing (same reader-breaking
|
|
260
|
+
-- pattern as the hook_runs_since note below).
|
|
261
261
|
CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_id);
|
|
262
262
|
CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id);
|
|
263
263
|
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id);
|
|
@@ -276,6 +276,44 @@ CREATE TABLE IF NOT EXISTS memesh_metadata (
|
|
|
276
276
|
key TEXT PRIMARY KEY,
|
|
277
277
|
value TEXT NOT NULL
|
|
278
278
|
);
|
|
279
|
+
|
|
280
|
+
-- Proof that a capture hook actually RAN. Nothing else in this schema can
|
|
281
|
+
-- give it: every other signal is "a row was written", and "the hook ran and
|
|
282
|
+
-- found nothing worth saving" is the healthy case that produces no row at
|
|
283
|
+
-- all. So a quiet day and a dead capture loop were byte-identical in the
|
|
284
|
+
-- database, and the one doctor message that had to cover both cried wolf on
|
|
285
|
+
-- the first and stayed silent on the second.
|
|
286
|
+
--
|
|
287
|
+
-- Written by the three hooks that hold a read-write handle (Stop, PreCompact,
|
|
288
|
+
-- PostToolUse), each calling recordHookRun() at its own SUCCESSFUL exit —
|
|
289
|
+
-- after capture, not at open. Stamping at open certified the wrong thing: a
|
|
290
|
+
-- hook that opened the database and then died mid-capture looked alive for a
|
|
291
|
+
-- day. "Successful" is precise: a completed capture, or a well-formed
|
|
292
|
+
-- payload the hook correctly decided not to capture (dedup, low-signal
|
|
293
|
+
-- session). A malformed payload (schema-flip shapes) and a write that did
|
|
294
|
+
-- not land both leave no stamp — either would make the heartbeat mask the
|
|
295
|
+
-- exact dropout it exists to expose. The recall-side hooks open read-only
|
|
296
|
+
-- and deliberately do not appear here: their liveness answers a different
|
|
297
|
+
-- question, and giving them a write handle would put a lock acquisition on
|
|
298
|
+
-- the SessionStart hot path.
|
|
299
|
+
--
|
|
300
|
+
-- One row per hook, upserted. It does not grow.
|
|
301
|
+
CREATE TABLE IF NOT EXISTS hook_runs (
|
|
302
|
+
hook TEXT PRIMARY KEY,
|
|
303
|
+
last_run_at TIMESTAMP NOT NULL,
|
|
304
|
+
run_count INTEGER NOT NULL DEFAULT 0
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
-- The 'hook_runs_since' metadata key (when this database first became able
|
|
308
|
+
-- to record hook runs) is stamped by ensureHookRunsSince() AFTER this exec,
|
|
309
|
+
-- NOT here. It used to be an INSERT OR IGNORE in this string, and that made
|
|
310
|
+
-- every open — including opens that only ever read — start a write
|
|
311
|
+
-- transaction: an INSERT statement takes the WAL writer lock even when
|
|
312
|
+
-- OR IGNORE ends up changing nothing. Two states regressed from "reads work"
|
|
313
|
+
-- to "open throws": a peer holding the writer lock past busy_timeout, and a
|
|
314
|
+
-- read-only database FILE (measured: "attempt to write a readonly database"
|
|
315
|
+
-- killed recall along with capture). The helper SELECTs first and writes
|
|
316
|
+
-- only when the key is genuinely absent — once per database lifetime.
|
|
279
317
|
`;
|
|
280
318
|
|
|
281
319
|
// FTS5 virtual table — separate so hooks that don't need it stay lean.
|
|
@@ -304,171 +342,19 @@ CREATE VIRTUAL TABLE IF NOT EXISTS fts_vocab USING fts5vocab(entities_fts, 'row'
|
|
|
304
342
|
* @param {boolean} [opts.fts=false] - Also create the FTS5 virtual table.
|
|
305
343
|
* @returns {{ db: any, dbPath: string }}
|
|
306
344
|
*/
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
// `
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
return process.env.VITEST === 'true' || process.env.NODE_ENV === 'test';
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
let _cachedDatabaseCtor;
|
|
322
|
-
export function tryRequireBetterSqlite() {
|
|
323
|
-
// Test-only seam: force the "native module unavailable" branch so
|
|
324
|
-
// tests can exercise the silent-skip path that plugin-marketplace
|
|
325
|
-
// cache installs hit. Gated to test environments so an accidental
|
|
326
|
-
// shell export cannot disable memesh on a real user's machine.
|
|
327
|
-
if (_inTestEnv() && process.env.MEMESH_TEST_FORCE_MISSING_NATIVE === '1') return null;
|
|
328
|
-
if (_cachedDatabaseCtor !== undefined) return _cachedDatabaseCtor;
|
|
329
|
-
try {
|
|
330
|
-
const Database = require('better-sqlite3');
|
|
331
|
-
// Second test-only seam: simulate the exact plugin-marketplace cache
|
|
332
|
-
// failure mode where require() succeeds (JS wrapper present) but the
|
|
333
|
-
// native .node is missing, so the first construction throws. Same
|
|
334
|
-
// test-env gate as the seam above.
|
|
335
|
-
if (_inTestEnv() && process.env.MEMESH_TEST_FORCE_BINDING_LOAD_FAIL === '1') {
|
|
336
|
-
throw new Error('Could not locate the bindings file. (test forced)');
|
|
337
|
-
}
|
|
338
|
-
const probe = new Database(':memory:');
|
|
339
|
-
probe.close();
|
|
340
|
-
_cachedDatabaseCtor = Database;
|
|
341
|
-
} catch (err) {
|
|
342
|
-
// Stderr-trace-then-silent: an empty catch would collapse plugin-
|
|
343
|
-
// marketplace's "no .node binding" case together with ABI mismatches,
|
|
344
|
-
// disk full, fd exhaustion, OOM, and tampered native modules — all
|
|
345
|
-
// distinct causes with distinct fixes. Following the project's hook
|
|
346
|
-
// pattern (e.g. session-summary.js, post-commit.js), we surface a
|
|
347
|
-
// single line on stderr (NOT stdout — Claude Code's hook contract
|
|
348
|
-
// requires stdout stays a single JSON document or empty) and let
|
|
349
|
-
// each caller continue its silent-skip behavior. The stderr trace
|
|
350
|
-
// is visible to anyone running `memesh doctor` or inspecting hook
|
|
351
|
-
// exit logs; it does NOT reach the Claude Code conversation.
|
|
352
|
-
try {
|
|
353
|
-
const code = err && typeof err === 'object' && 'code' in err ? err.code : '';
|
|
354
|
-
const msg = (err && typeof err === 'object' && 'message' in err ? err.message : String(err)) || 'unknown';
|
|
355
|
-
process.stderr.write(`[memesh hook] better-sqlite3 probe failed: ${code} ${msg}\n`);
|
|
356
|
-
} catch {
|
|
357
|
-
// stderr write itself failed (closed pipe, etc.) — give up silently.
|
|
358
|
-
}
|
|
359
|
-
// Self-heal for the plugin-marketplace silent-dropout class of bug.
|
|
360
|
-
// When Claude Code's `/plugin install` runs `npm install --ignore-scripts`
|
|
361
|
-
// (security default), better-sqlite3's `install` script never fetches
|
|
362
|
-
// / builds the native binding. Result: `require()` returns a JS
|
|
363
|
-
// wrapper but `new Database()` throws "Could not locate the bindings
|
|
364
|
-
// file" — and every hook silently exits without writing entities.
|
|
365
|
-
// Without this self-heal, the user has no signal that auto-capture
|
|
366
|
-
// is broken; the DB just stays empty forever.
|
|
367
|
-
//
|
|
368
|
-
// Strategy: spawn a detached `npm rebuild better-sqlite3` in the
|
|
369
|
-
// package root so the *next* hook invocation succeeds. Cap to one
|
|
370
|
-
// attempt per hour per package root via an exclusive-create marker
|
|
371
|
-
// so a crash-loop can't drive a rebuild storm. Skipped under test
|
|
372
|
-
// env (tests deliberately exercise the failure path).
|
|
373
|
-
if (!_inTestEnv()) {
|
|
374
|
-
_attemptBetterSqliteRebuild();
|
|
375
|
-
}
|
|
376
|
-
_cachedDatabaseCtor = null;
|
|
377
|
-
}
|
|
378
|
-
return _cachedDatabaseCtor;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function _attemptBetterSqliteRebuild() {
|
|
382
|
-
try {
|
|
383
|
-
// Package root = parent of the scripts/hooks/ directory that contains
|
|
384
|
-
// this file. That's where memesh's own `package.json` lives.
|
|
385
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
386
|
-
const pkgRoot = dirname(dirname(here));
|
|
387
|
-
if (!existsSync(join(pkgRoot, 'package.json'))) return;
|
|
388
|
-
// Resolve better-sqlite3's actual install location via Node's normal
|
|
389
|
-
// resolution algorithm, which follows hoisting (the consumer's
|
|
390
|
-
// top-level node_modules holds the package when memesh is installed
|
|
391
|
-
// as a dependency). Looking at `<pkgRoot>/node_modules/better-sqlite3`
|
|
392
|
-
// directly would false-negative on every hoisted install.
|
|
393
|
-
let bsqliteDir;
|
|
394
|
-
try {
|
|
395
|
-
// `require.resolve` returns the path to `lib/index.js` inside the
|
|
396
|
-
// package. Walk up to the package directory.
|
|
397
|
-
const entry = require.resolve('better-sqlite3', { paths: [pkgRoot] });
|
|
398
|
-
// `<bsqliteDir>/lib/index.js` → walk back twice to the package root.
|
|
399
|
-
bsqliteDir = dirname(dirname(entry));
|
|
400
|
-
} catch {
|
|
401
|
-
// Genuinely not installed anywhere on the resolution path. `npm
|
|
402
|
-
// rebuild` cannot help; the user needs a full install.
|
|
403
|
-
try {
|
|
404
|
-
process.stderr.write(
|
|
405
|
-
`[memesh hook] better-sqlite3 is not installed (Node could not resolve from ${pkgRoot}). `
|
|
406
|
-
+ `Run: cd to the project that depends on @pcircle/memesh and run \`npm install\`.\n`,
|
|
407
|
-
);
|
|
408
|
-
} catch {}
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
// The hoisted install location's package root — npm rebuild needs to
|
|
412
|
-
// be run from a project that owns this node_modules tree. Walking
|
|
413
|
-
// up to the nearest directory that has its own package.json gives
|
|
414
|
-
// us the right cwd.
|
|
415
|
-
let rebuildCwd = dirname(bsqliteDir);
|
|
416
|
-
while (rebuildCwd !== dirname(rebuildCwd)) {
|
|
417
|
-
if (existsSync(join(rebuildCwd, 'package.json')) && !rebuildCwd.endsWith('node_modules')) break;
|
|
418
|
-
rebuildCwd = dirname(rebuildCwd);
|
|
419
|
-
}
|
|
420
|
-
const memesh = join(homedir(), '.memesh');
|
|
421
|
-
try { mkdirSync(memesh, { recursive: true, mode: 0o700 }); } catch {}
|
|
422
|
-
const markerPath = join(memesh, 'last-rebuild-attempt.lock');
|
|
423
|
-
// Atomic one-shot claim via O_EXCL. Once the marker exists, every
|
|
424
|
-
// future hook bails — no stale-cleanup-then-recreate dance, which
|
|
425
|
-
// would open a TOCTOU window (stat → unlink → open is racy: a peer
|
|
426
|
-
// can insert between any two steps and the result is either a
|
|
427
|
-
// double-spawn of `npm rebuild` or one peer's fresh marker being
|
|
428
|
-
// stomped by another peer's stale-cleanup).
|
|
429
|
-
//
|
|
430
|
-
// Trade-off: if the rebuild fails, the marker blocks retries until
|
|
431
|
-
// the user removes it manually. That's acceptable because the
|
|
432
|
-
// stderr breadcrumb below tells the user the exact manual command,
|
|
433
|
-
// and `memesh doctor` will also surface the failure. A retry-loop
|
|
434
|
-
// here would either re-introduce the race or burn CPU on a broken
|
|
435
|
-
// npm config.
|
|
436
|
-
try {
|
|
437
|
-
const fd = openSync(markerPath, 'wx', 0o600);
|
|
438
|
-
try { writeFileSync(fd, String(Date.now())); } finally { closeSync(fd); }
|
|
439
|
-
} catch (err) {
|
|
440
|
-
if (err && err.code === 'EEXIST') return; // peer / prior attempt owns it
|
|
441
|
-
return; // any other write failure — bail silently
|
|
442
|
-
}
|
|
443
|
-
process.stderr.write(
|
|
444
|
-
`[memesh hook] Attempting to rebuild better-sqlite3 in background — `
|
|
445
|
-
+ `next session should capture normally. (rebuildCwd: ${rebuildCwd})\n`
|
|
446
|
-
+ `[memesh hook] To retry later, manually: rm "${markerPath}" && `
|
|
447
|
-
+ `cd "${rebuildCwd}" && npm rebuild better-sqlite3\n`,
|
|
448
|
-
);
|
|
449
|
-
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
450
|
-
const child = spawn(npm, ['rebuild', 'better-sqlite3'], {
|
|
451
|
-
cwd: rebuildCwd,
|
|
452
|
-
detached: true,
|
|
453
|
-
stdio: 'ignore',
|
|
454
|
-
windowsHide: true,
|
|
455
|
-
});
|
|
456
|
-
// 'error' is emitted asynchronously (e.g. npm not on PATH). Without a
|
|
457
|
-
// listener it becomes an uncaught exception that the outer sync
|
|
458
|
-
// try/catch cannot catch — and a hook crash here would turn a silent
|
|
459
|
-
// dropout into a louder broken-hook story. Swallow it: self-heal is
|
|
460
|
-
// best-effort by design, and the binding probe already left a stderr
|
|
461
|
-
// breadcrumb explaining the manual fix.
|
|
462
|
-
child.on('error', () => {});
|
|
463
|
-
child.unref();
|
|
464
|
-
} catch {
|
|
465
|
-
// Best-effort — never let self-heal failures crash the hook.
|
|
466
|
-
}
|
|
467
|
-
}
|
|
345
|
+
// No native-binding probe, and nothing to self-heal.
|
|
346
|
+
//
|
|
347
|
+
// This is where ~160 lines used to live: a cached require() of
|
|
348
|
+
// better-sqlite3, an in-memory construction to force the deferred bindings
|
|
349
|
+
// load, two test seams to simulate the failure, and a detached
|
|
350
|
+
// `npm rebuild` with an O_EXCL marker so a crash-loop could not storm it.
|
|
351
|
+
// All of it existed because better-sqlite3 ships a compiled binary that
|
|
352
|
+
// `npm install --ignore-scripts` never builds — and Claude Code's
|
|
353
|
+
// `/plugin install` uses exactly that flag, so every hook silently did
|
|
354
|
+
// nothing. node:sqlite is part of the runtime: there is no binary to
|
|
355
|
+
// miss, so the failure mode and its whole recovery apparatus are gone.
|
|
468
356
|
|
|
469
357
|
export function openHookDb(env = process.env, opts = {}) {
|
|
470
|
-
const Database = tryRequireBetterSqlite();
|
|
471
|
-
if (!Database) return null;
|
|
472
358
|
|
|
473
359
|
// Path helpers read process.env directly (no-arg). The `env` parameter
|
|
474
360
|
// is kept on this signature for backward compatibility with callers
|
|
@@ -479,10 +365,58 @@ export function openHookDb(env = process.env, opts = {}) {
|
|
|
479
365
|
const dbDir = env.MEMESH_DB_PATH ? dirname(env.MEMESH_DB_PATH) : memeshDir();
|
|
480
366
|
if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
|
|
481
367
|
|
|
482
|
-
|
|
368
|
+
// `allowExtension` matches src/db.ts: it only permits a later
|
|
369
|
+
// `enableLoadExtension(true)`, and session-summary.js needs one to load
|
|
370
|
+
// sqlite-vec through this handle. The switch itself stays off.
|
|
371
|
+
const db = new MemeshDatabase(dbPath, { allowExtension: true });
|
|
483
372
|
db.pragma('journal_mode = WAL');
|
|
484
373
|
db.pragma('foreign_keys = ON');
|
|
374
|
+
// Bringing the schema current is a WRITE, and "cannot migrate" must not
|
|
375
|
+
// mean "cannot open": a database file that is read-only but behind on
|
|
376
|
+
// schema (a pre-upgrade backup, a permissions accident) dies on the
|
|
377
|
+
// CREATE TABLE any release adds. If the FILE refuses writes, open it for
|
|
378
|
+
// what it can still do — reads; capture writes fail individually at
|
|
379
|
+
// their own guarded call sites. Any other error still throws. Mirrors
|
|
380
|
+
// initialiseDatabase() in src/db.ts — keep the two in lockstep.
|
|
381
|
+
try {
|
|
382
|
+
migrateHookDbToCurrent(db, opts);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
if (!/readonly database|SQLITE_READONLY/i.test(err?.message || '')) throw err;
|
|
385
|
+
try {
|
|
386
|
+
process.stderr.write(
|
|
387
|
+
'MeMesh: the database file is read-only, so schema migration was skipped — ' +
|
|
388
|
+
'opened for reads only. Capture and migrations resume when the file is writable.\n',
|
|
389
|
+
);
|
|
390
|
+
} catch { /* stderr gone */ }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
// No heartbeat here. This helper used to stamp `hook_runs` as soon as the
|
|
395
|
+
// handle was usable, and that stamped-then-crashed hooks into looking
|
|
396
|
+
// alive: a hook that opened the database and then died in its own capture
|
|
397
|
+
// logic — the failure class this table exists to expose — read as PASS in
|
|
398
|
+
// `memesh doctor` for the next 24 hours. Each capture hook now calls
|
|
399
|
+
// recordHookRun() itself at every SUCCESSFUL exit (including "ran, nothing
|
|
400
|
+
// worth saving"), so a mid-capture throw leaves no stamp.
|
|
401
|
+
|
|
402
|
+
return { db, dbPath };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Everything that makes a hook-opened handle CURRENT: schema, FTS,
|
|
407
|
+
* one-time migrations and the segmentation rebuild. Split from
|
|
408
|
+
* openHookDb() so the read-only-file tolerance there has a single
|
|
409
|
+
* boundary to wrap — every statement in here may write, and none of
|
|
410
|
+
* them is load-bearing for reading what the database already holds.
|
|
411
|
+
* Mirrors migrateToCurrentSchema() in src/db.ts — keep in lockstep.
|
|
412
|
+
*
|
|
413
|
+
* @param {import('./_generated/sqlite.js').MemeshDatabase} db
|
|
414
|
+
* @param {{fts?: boolean}} opts
|
|
415
|
+
*/
|
|
416
|
+
function migrateHookDbToCurrent(db, opts) {
|
|
485
417
|
db.exec(SCHEMA_SQL);
|
|
418
|
+
ensureTagsUniqueIndex(db);
|
|
419
|
+
ensureHookRunsSince(db);
|
|
486
420
|
if (opts.fts) db.exec(FTS_SQL);
|
|
487
421
|
|
|
488
422
|
// Apply the full migration chain — keep in lockstep with src/db.ts.
|
|
@@ -554,8 +488,160 @@ export function openHookDb(env = process.env, opts = {}) {
|
|
|
554
488
|
// delete failed, the stale row survived alongside the new one, and the user
|
|
555
489
|
// saw "database disk image is malformed" on hook stderr.
|
|
556
490
|
if (opts.fts) ensureHookFtsSegmentation(db);
|
|
491
|
+
}
|
|
557
492
|
|
|
558
|
-
|
|
493
|
+
/**
|
|
494
|
+
* One-time tags dedup + unique-index creation, guarded so it never writes
|
|
495
|
+
* once the index exists. Mirror of ensureTagsUniqueIndex() in src/db.ts —
|
|
496
|
+
* hooks cannot import from dist/. Keep the two in lockstep.
|
|
497
|
+
*
|
|
498
|
+
* @param {import('./_generated/sqlite.js').MemeshDatabase} db
|
|
499
|
+
*/
|
|
500
|
+
export function ensureTagsUniqueIndex(db) {
|
|
501
|
+
try {
|
|
502
|
+
const present = db
|
|
503
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_tags_entity_tag_unique'")
|
|
504
|
+
.get();
|
|
505
|
+
if (present) return;
|
|
506
|
+
// One IMMEDIATE transaction, not two autocommit statements: exec runs
|
|
507
|
+
// each statement in its own transaction, so a crash or a busy peer
|
|
508
|
+
// between them could commit the DELETE with the index never created —
|
|
509
|
+
// and a concurrent pre-index writer could re-insert a duplicate pair in
|
|
510
|
+
// that window, failing the CREATE with a constraint error. BEGIN
|
|
511
|
+
// IMMEDIATE holds the write lock across both, so the dedup and the
|
|
512
|
+
// index land together or not at all (CREATE INDEX is transactional in
|
|
513
|
+
// SQLite).
|
|
514
|
+
db.exec(
|
|
515
|
+
'BEGIN IMMEDIATE; ' +
|
|
516
|
+
'DELETE FROM tags WHERE id NOT IN (SELECT MIN(id) FROM tags GROUP BY entity_id, tag); ' +
|
|
517
|
+
'CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_entity_tag_unique ON tags(entity_id, tag); ' +
|
|
518
|
+
'COMMIT;',
|
|
519
|
+
);
|
|
520
|
+
} catch (err) {
|
|
521
|
+
try { db.exec('ROLLBACK'); } catch { /* no transaction open */ }
|
|
522
|
+
try {
|
|
523
|
+
process.stderr.write(
|
|
524
|
+
`MeMesh: could not create the tags unique index (${err?.message ?? err}). ` +
|
|
525
|
+
`Reads are unaffected; the next open retries the dedup and index together.\n`,
|
|
526
|
+
);
|
|
527
|
+
} catch { /* stderr gone; nothing left to say */ }
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Stamp 'hook_runs_since' once per database lifetime — read-first, so an
|
|
533
|
+
* open that only ever reads stays a reader.
|
|
534
|
+
*
|
|
535
|
+
* Mirror of ensureHookRunsSince() in src/db.ts — hooks cannot import from
|
|
536
|
+
* dist/. Keep the two in lockstep; the full rationale (the INSERT used to
|
|
537
|
+
* live inside SCHEMA_SQL and made read-only database files unopenable)
|
|
538
|
+
* lives on the src copy.
|
|
539
|
+
*
|
|
540
|
+
* @param {import('./_generated/sqlite.js').MemeshDatabase} db
|
|
541
|
+
*/
|
|
542
|
+
export function ensureHookRunsSince(db) {
|
|
543
|
+
try {
|
|
544
|
+
const row = db
|
|
545
|
+
.prepare("SELECT value FROM memesh_metadata WHERE key = 'hook_runs_since'")
|
|
546
|
+
.get();
|
|
547
|
+
if (row) {
|
|
548
|
+
// A marker that exists but cannot be read as a past UTC timestamp
|
|
549
|
+
// (corrupt text, a rolled-over pseudo-date, a wrong clock stamping the
|
|
550
|
+
// future) would grant doctor's "tracking just started" grace FOREVER —
|
|
551
|
+
// a fail-open. Heal it HERE, because this is a write path that runs on
|
|
552
|
+
// every real open; doctor is a reader (reachable via GET /v1/doctor)
|
|
553
|
+
// and must not repair the database it inspects. Same parse rules as
|
|
554
|
+
// doctor's hoursSince: anchored, UTC, round-tripped.
|
|
555
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/.exec(row.value ?? '');
|
|
556
|
+
if (m) {
|
|
557
|
+
const then = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
|
|
558
|
+
const d = new Date(then);
|
|
559
|
+
const intact = Number.isFinite(then)
|
|
560
|
+
&& d.getUTCFullYear() === +m[1] && d.getUTCMonth() === +m[2] - 1 && d.getUTCDate() === +m[3]
|
|
561
|
+
&& d.getUTCHours() === +m[4] && d.getUTCMinutes() === +m[5] && d.getUTCSeconds() === +m[6];
|
|
562
|
+
if (intact && then <= Date.now() + 5 * 60 * 1000) return;
|
|
563
|
+
}
|
|
564
|
+
db
|
|
565
|
+
.prepare("UPDATE memesh_metadata SET value = datetime('now') WHERE key = 'hook_runs_since'")
|
|
566
|
+
.run();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
db
|
|
570
|
+
.prepare("INSERT OR IGNORE INTO memesh_metadata (key, value) VALUES ('hook_runs_since', datetime('now'))")
|
|
571
|
+
.run();
|
|
572
|
+
} catch (err) {
|
|
573
|
+
try {
|
|
574
|
+
process.stderr.write(
|
|
575
|
+
`MeMesh: could not stamp hook_runs_since (${err?.message ?? err}). ` +
|
|
576
|
+
`Reads are unaffected; doctor's hook-activity tracking starts once the database is writable.\n`,
|
|
577
|
+
);
|
|
578
|
+
} catch { /* stderr gone; nothing left to say */ }
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Stamp a hook's heartbeat from a path that has no database handle open.
|
|
584
|
+
*
|
|
585
|
+
* session-summary's low-signal bails (non-agentic session, vanished
|
|
586
|
+
* transcript, fewer than three tool calls) decide "nothing worth saving"
|
|
587
|
+
* BEFORE opening the database — and a correct nothing-to-do decision is a
|
|
588
|
+
* successful run that must stamp, or a user whose sessions are consistently
|
|
589
|
+
* short reads as "capture has stopped" in doctor within a day: the exact
|
|
590
|
+
* crying-wolf this table exists to end. Stop fires once per session, so one
|
|
591
|
+
* extra open+close here is noise.
|
|
592
|
+
*
|
|
593
|
+
* Never throws: the heartbeat is diagnostics, and the bail it decorates was
|
|
594
|
+
* already a successful exit.
|
|
595
|
+
*
|
|
596
|
+
* @param {NodeJS.ProcessEnv} env
|
|
597
|
+
* @param {string} hook
|
|
598
|
+
*/
|
|
599
|
+
export function stampHookRunOnly(env, hook) {
|
|
600
|
+
try {
|
|
601
|
+
const { db } = openHookDb(env);
|
|
602
|
+
try { recordHookRun(db, hook); } finally { db.close(); }
|
|
603
|
+
} catch (err) {
|
|
604
|
+
try {
|
|
605
|
+
process.stderr.write(
|
|
606
|
+
`MeMesh: could not stamp the ${hook} heartbeat on a no-capture exit (${err?.message ?? err}).\n`,
|
|
607
|
+
);
|
|
608
|
+
} catch { /* stderr gone */ }
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Record that `hook` ran, right now.
|
|
614
|
+
*
|
|
615
|
+
* This is the only evidence in the system that a hook EXECUTED, as opposed to
|
|
616
|
+
* a hook having captured something. Doctor could previously only count
|
|
617
|
+
* auto-captured entities, which conflates the healthy "ran, nothing worth
|
|
618
|
+
* saving" with the fatal "never ran" — see the `hook_runs` comment in
|
|
619
|
+
* SCHEMA_SQL.
|
|
620
|
+
*
|
|
621
|
+
* A failure here must never take the hook down with it: the heartbeat is
|
|
622
|
+
* diagnostics, and a session that captured its work but could not stamp the
|
|
623
|
+
* row is far better than one that threw. But it is not swallowed either — it
|
|
624
|
+
* writes to stderr, because a heartbeat that silently stops recording would
|
|
625
|
+
* recreate the exact blind spot it exists to close.
|
|
626
|
+
*/
|
|
627
|
+
export function recordHookRun(db, hook) {
|
|
628
|
+
try {
|
|
629
|
+
db.prepare(
|
|
630
|
+
`INSERT INTO hook_runs (hook, last_run_at, run_count)
|
|
631
|
+
VALUES (?, datetime('now'), 1)
|
|
632
|
+
ON CONFLICT(hook) DO UPDATE SET
|
|
633
|
+
last_run_at = datetime('now'),
|
|
634
|
+
run_count = run_count + 1`,
|
|
635
|
+
).run(hook);
|
|
636
|
+
} catch (err) {
|
|
637
|
+
try {
|
|
638
|
+
process.stderr.write(
|
|
639
|
+
`MeMesh: could not record that the ${hook} hook ran (${err?.message ?? err}). ` +
|
|
640
|
+
`Capture itself is unaffected, but 'memesh doctor' will under-report ` +
|
|
641
|
+
`hook liveness until this succeeds.\n`,
|
|
642
|
+
);
|
|
643
|
+
} catch { /* stderr itself is gone; there is nowhere left to report */ }
|
|
644
|
+
}
|
|
559
645
|
}
|
|
560
646
|
|
|
561
647
|
/**
|
|
@@ -625,8 +711,8 @@ function ensureHookFtsSegmentation(db) {
|
|
|
625
711
|
|
|
626
712
|
db.exec("INSERT INTO entities_fts (entities_fts) VALUES('delete-all')");
|
|
627
713
|
|
|
628
|
-
// Paged, not `.iterate()`:
|
|
629
|
-
//
|
|
714
|
+
// Paged, not `.iterate()`: writing while an iterator is open on the
|
|
715
|
+
// same connection is not something to rely on, and writing as we read is
|
|
630
716
|
// the point. Keyset pagination on e.id bounds memory to one page.
|
|
631
717
|
const page = db.prepare(
|
|
632
718
|
`SELECT e.id, e.name, COALESCE(group_concat(o.content, ' '), '') AS obs
|
|
@@ -711,14 +797,19 @@ function ensureHookFtsSegmentation(db) {
|
|
|
711
797
|
* deliberately NOT done here: hooks are cheap always-on capture, and those are
|
|
712
798
|
* the heavier, user-initiated `remember` concerns (core owns them).
|
|
713
799
|
*
|
|
714
|
-
* @param {import('
|
|
800
|
+
* @param {import('./_generated/sqlite.js').MemeshDatabase} db - an open hook DB handle
|
|
715
801
|
* @param {{name: string, type: string, observations?: string[], tags?: string[]}} entity
|
|
716
802
|
* @returns {{ id: number, isNew: boolean } | null} null if the row could not be resolved
|
|
717
803
|
*/
|
|
718
804
|
export function captureEntity(db, { name, type, observations = [], tags = [] }) {
|
|
805
|
+
// source_host provenance: these hooks only ever run under Claude Code (they
|
|
806
|
+
// are wired into ~/.claude/settings.json), so a hook-captured entity is by
|
|
807
|
+
// definition a claude-code capture. Stamped only on the INSERT — an OR
|
|
808
|
+
// IGNORE re-capture of an existing entity must not overwrite provenance an
|
|
809
|
+
// earlier writer (possibly another host, via MCP) already recorded.
|
|
719
810
|
const insertResult = db
|
|
720
|
-
.prepare('INSERT OR IGNORE INTO entities (name, type) VALUES (?, ?)')
|
|
721
|
-
.run(name, type);
|
|
811
|
+
.prepare('INSERT OR IGNORE INTO entities (name, type, metadata) VALUES (?, ?, ?)')
|
|
812
|
+
.run(name, type, JSON.stringify({ provenance: { source_host: 'claude-code' } }));
|
|
722
813
|
const isNew = insertResult.changes > 0;
|
|
723
814
|
const row = db.prepare('SELECT id FROM entities WHERE name = ?').get(name);
|
|
724
815
|
if (!row) return null;
|