linksee-memory 0.17.0 → 0.17.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/README.md +958 -1081
- package/dist/bin/guard-hook.js +19 -4
- package/dist/bin/setup.js +191 -76
- package/dist/bin/share-draft.js +28 -28
- package/dist/bin/sync-session.js +10 -1
- package/dist/db/migrate.d.ts +30 -0
- package/dist/db/migrate.js +178 -37
- package/dist/lib/consolidate.d.ts +9 -0
- package/dist/lib/consolidate.js +14 -0
- package/dist/lib/drift-anchors.js +3 -3
- package/dist/lib/drift-detection.js +20 -20
- package/dist/lib/drift-view.js +16 -16
- package/dist/lib/guard-wiring.d.ts +47 -1
- package/dist/lib/guard-wiring.js +114 -11
- package/dist/lib/hook-runtime.d.ts +41 -0
- package/dist/lib/hook-runtime.js +197 -0
- package/dist/lib/map-import.js +17 -17
- package/dist/lib/native-hint.d.ts +5 -0
- package/dist/lib/native-hint.js +42 -0
- package/dist/lib/settings-file.d.ts +23 -0
- package/dist/lib/settings-file.js +137 -0
- package/dist/mcp/prompts.js +59 -59
- package/dist/mcp/resources.js +7 -7
- package/dist/mcp/sampling.js +20 -20
- package/dist/mcp/server.js +180 -160
- package/dist/skill/SKILL.md +6 -14
- package/package.json +107 -107
package/dist/bin/guard-hook.js
CHANGED
|
@@ -14,8 +14,14 @@
|
|
|
14
14
|
// "hooks": [ { "type": "command", "command": "node",
|
|
15
15
|
// "args": ["${CLAUDE_PROJECT_DIR}/dist/bin/guard-hook.js"], "timeout": 8 } ] } ] } }
|
|
16
16
|
import { pathToFileURL } from 'node:url';
|
|
17
|
-
import {
|
|
18
|
-
|
|
17
|
+
import { NATIVE_HINT, isNativeLoadError, hookLog } from '../lib/native-hint.js';
|
|
18
|
+
/** Fail open (exit 0). Native-load failures say so once on stderr; anything else goes to hook.log only. */
|
|
19
|
+
function bail(where, e) {
|
|
20
|
+
if (isNativeLoadError(e))
|
|
21
|
+
process.stderr.write(`${NATIVE_HINT}\n`);
|
|
22
|
+
hookLog(`guard: ${where}: ${String(e?.message ?? e).split('\n')[0]}`);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
19
25
|
function readStdin() {
|
|
20
26
|
return new Promise((resolve) => {
|
|
21
27
|
const chunks = [];
|
|
@@ -37,14 +43,23 @@ async function main() {
|
|
|
37
43
|
}
|
|
38
44
|
if (!ev || typeof ev !== 'object')
|
|
39
45
|
process.exit(0);
|
|
46
|
+
let openDb, runMigrations;
|
|
47
|
+
let gateAction, buildBootDigest;
|
|
48
|
+
try {
|
|
49
|
+
({ openDb, runMigrations } = await import('../db/migrate.js'));
|
|
50
|
+
({ gateAction, buildBootDigest } = await import('../lib/guard.js'));
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
bail('import', e);
|
|
54
|
+
}
|
|
40
55
|
let db;
|
|
41
56
|
try {
|
|
42
57
|
db = openDb();
|
|
43
58
|
db.pragma('busy_timeout = 2000'); // MCP server may hold a write lock; wait briefly, else fail-open
|
|
44
59
|
runMigrations(db); // ensures injection_log exists even when run standalone
|
|
45
60
|
}
|
|
46
|
-
catch {
|
|
47
|
-
|
|
61
|
+
catch (e) {
|
|
62
|
+
bail('open', e); // can't open DB → never block
|
|
48
63
|
}
|
|
49
64
|
try {
|
|
50
65
|
if (ev.hook_event_name === 'PreToolUse') {
|
package/dist/bin/setup.js
CHANGED
|
@@ -36,11 +36,14 @@
|
|
|
36
36
|
// detect_terms / violation_signal, so cross-repo noise is bounded by what you declared.
|
|
37
37
|
import { spawnSync } from 'node:child_process';
|
|
38
38
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
39
|
-
import { join, dirname } from 'node:path';
|
|
39
|
+
import { join, dirname, sep } from 'node:path';
|
|
40
40
|
import { homedir } from 'node:os';
|
|
41
41
|
import { fileURLToPath } from 'node:url';
|
|
42
42
|
import { createInterface } from 'node:readline';
|
|
43
|
-
import
|
|
43
|
+
import Database from 'better-sqlite3';
|
|
44
|
+
import { guardFullyWired, wireGuard, syncWiredFor, rewireLegacyHooks, unmanagedLinkseeHooks } from '../lib/guard-wiring.js';
|
|
45
|
+
import { resolveHookRuntime, formatBytes } from '../lib/hook-runtime.js';
|
|
46
|
+
import { readSettingsFile, stripBom, writeSettingsFile } from '../lib/settings-file.js';
|
|
44
47
|
const args = process.argv.slice(2);
|
|
45
48
|
const dryRun = args.includes('--dry-run');
|
|
46
49
|
const autoYes = args.includes('--yes') || args.includes('-y');
|
|
@@ -49,20 +52,25 @@ const noGuard = args.includes('--no-guard');
|
|
|
49
52
|
const projectGuard = args.includes('--project-guard');
|
|
50
53
|
const showHelp = args.includes('--help') || args.includes('-h');
|
|
51
54
|
if (showHelp) {
|
|
52
|
-
console.log(`linksee-memory-setup — One-command setup for Linksee Memory
|
|
53
|
-
|
|
54
|
-
Usage:
|
|
55
|
-
npx linksee-memory setup Interactive setup
|
|
56
|
-
npx linksee-memory setup --yes Accept all defaults, no prompts
|
|
57
|
-
npx linksee-memory setup --dry-run Show what would happen
|
|
58
|
-
|
|
59
|
-
What it does:
|
|
60
|
-
1. Registers linksee-memory MCP server with Claude Code
|
|
61
|
-
2. Installs SKILL.md (teaches the agent when to recall/remember)
|
|
62
|
-
3. Configures Stop hook (auto-captures every session)
|
|
63
|
-
4.
|
|
64
|
-
|
|
65
|
-
|
|
55
|
+
console.log(`linksee-memory-setup — One-command setup for Linksee Memory
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
npx linksee-memory setup Interactive setup
|
|
59
|
+
npx linksee-memory setup --yes Accept all defaults, no prompts
|
|
60
|
+
npx linksee-memory setup --dry-run Show what would happen
|
|
61
|
+
|
|
62
|
+
What it does:
|
|
63
|
+
1. Registers linksee-memory MCP server with Claude Code
|
|
64
|
+
2. Installs SKILL.md (teaches the agent when to recall/remember)
|
|
65
|
+
3. Configures Stop hook (auto-captures every session)
|
|
66
|
+
4. Wires the re-injection guard into ~/.claude/settings.json (every repo)
|
|
67
|
+
--project-guard: this repo only · --no-guard: skip
|
|
68
|
+
|
|
69
|
+
Writes ~/.claude/settings.json (one settings.json.bak-<timestamp> kept before changing it),
|
|
70
|
+
~/.claude/skills/linksee-memory/SKILL.md, and — when run through npx — a pinned copy under
|
|
71
|
+
~/.linksee-memory/runtime/<version>/ that the hooks call with node directly.
|
|
72
|
+
|
|
73
|
+
After setup, just chat with Claude Code normally.
|
|
66
74
|
Add "Use Linksee" to any prompt to trigger memory recall.`);
|
|
67
75
|
process.exit(0);
|
|
68
76
|
}
|
|
@@ -76,12 +84,21 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
76
84
|
const SKILL_SRC = join(dirname(__filename), '..', 'skill', 'SKILL.md');
|
|
77
85
|
const SERVER_NAME = 'linksee';
|
|
78
86
|
const MCP_COMMAND = `claude mcp add -s user ${SERVER_NAME} -- npx -y linksee-memory`;
|
|
79
|
-
//
|
|
80
|
-
// npx
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
87
|
+
// Hook commands (0.17.1, L-1-106): `"<node>" "<package>/dist/bin/<bin>.js"` — node called directly,
|
|
88
|
+
// not `npx -y linksee-memory <sub>` (6–9 s per call vs the 8 s PreToolUse timeout). When setup runs
|
|
89
|
+
// from npx's temporary cache, lib/hook-runtime.ts installs this version under
|
|
90
|
+
// <memory dir>/runtime/<version> (npm install, once) and the hooks point there; if that fails it
|
|
91
|
+
// keeps the npx form and says so.
|
|
92
|
+
// Merge / replace rules live in lib/guard-wiring.ts (shared with the tests).
|
|
93
|
+
const PACKAGE_ROOT = join(dirname(__filename), '..', '..');
|
|
94
|
+
const PACKAGE_VERSION = (() => {
|
|
95
|
+
try {
|
|
96
|
+
return String(JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return 'unknown';
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
85
102
|
const PROJECT_DIR = process.cwd();
|
|
86
103
|
const PROJECT_CLAUDE_DIR = join(PROJECT_DIR, '.claude');
|
|
87
104
|
const PROJECT_SETTINGS_PATH = join(PROJECT_CLAUDE_DIR, 'settings.json');
|
|
@@ -107,7 +124,7 @@ try {
|
|
|
107
124
|
if (!existsSync(confFile))
|
|
108
125
|
continue;
|
|
109
126
|
try {
|
|
110
|
-
const conf = JSON.parse(readFileSync(confFile, 'utf8'));
|
|
127
|
+
const conf = JSON.parse(stripBom(readFileSync(confFile, 'utf8')));
|
|
111
128
|
if (conf?.mcpServers?.[SERVER_NAME]) {
|
|
112
129
|
mcpAlreadyRegistered = true;
|
|
113
130
|
break;
|
|
@@ -198,40 +215,85 @@ else {
|
|
|
198
215
|
console.log('');
|
|
199
216
|
// ── Step 3: Configure Stop hook ──────────────────────────
|
|
200
217
|
console.log(`${BOLD}[3/4]${RESET} Configuring auto-capture hook...`);
|
|
201
|
-
|
|
202
|
-
|
|
218
|
+
const runtime = resolveHookRuntime({
|
|
219
|
+
packageRoot: PACKAGE_ROOT,
|
|
220
|
+
nodePath: process.execPath,
|
|
221
|
+
memoryDir: TELEMETRY_DIR,
|
|
222
|
+
version: PACKAGE_VERSION,
|
|
223
|
+
dryRun,
|
|
224
|
+
// For testing an unpublished build: what to npm-install instead of linksee-memory@<version>.
|
|
225
|
+
spec: process.env.LINKSEE_RUNTIME_SPEC || undefined,
|
|
226
|
+
});
|
|
227
|
+
const HOOK_COMMANDS = runtime.commands;
|
|
228
|
+
const HOOK_COMMAND = HOOK_COMMANDS.sync;
|
|
229
|
+
if (runtime.mode === 'npx') {
|
|
230
|
+
console.log(` ${SKIP} Hooks use npx (slower, ~6–9 s per call): ${runtime.note ?? ''}`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.log(` ${DIM}Hooks call node directly: ${runtime.root}${runtime.note ? ` (${runtime.note})` : ''}${RESET}`);
|
|
234
|
+
}
|
|
235
|
+
// Hooks that run our scripts but were wired by hand (wrappers, unquoted paths, exec form) are
|
|
236
|
+
// never rewritten; list them at the end so the user can update them if they want to.
|
|
237
|
+
function handWiredNotice(path) {
|
|
238
|
+
const r = readSettingsFile(path);
|
|
239
|
+
if (!r.ok)
|
|
240
|
+
return;
|
|
241
|
+
for (const c of unmanagedLinkseeHooks(r.settings))
|
|
242
|
+
handWired.add(`${c} (${path})`);
|
|
243
|
+
}
|
|
244
|
+
const handWired = new Set();
|
|
245
|
+
/** writeSettingsFile that reports instead of throwing (a failed write leaves the file as it was). */
|
|
246
|
+
function safeWrite(path, settings) {
|
|
203
247
|
try {
|
|
204
|
-
|
|
248
|
+
return writeSettingsFile(path, settings);
|
|
205
249
|
}
|
|
206
|
-
catch {
|
|
207
|
-
console.log(` ${FAIL} Could not
|
|
208
|
-
|
|
250
|
+
catch (e) {
|
|
251
|
+
console.log(` ${FAIL} Could not write ${path} (${e?.message ?? e}) — left as it was`);
|
|
252
|
+
return null;
|
|
209
253
|
}
|
|
210
254
|
}
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
console.log(` ${SKIP} Stop hook already configured`);
|
|
218
|
-
}
|
|
219
|
-
else if (dryRun) {
|
|
220
|
-
console.log(` ${DIM}[dry-run] Would add Stop hook to ${SETTINGS_PATH}${RESET}`);
|
|
255
|
+
// Same protection as the guard step below (lib/settings-file.ts): BOM stripped, an unparseable
|
|
256
|
+
// file is left untouched, one .bak before the first write, temp file + rename.
|
|
257
|
+
const stopRead = readSettingsFile(SETTINGS_PATH);
|
|
258
|
+
if (!stopRead.ok) {
|
|
259
|
+
console.log(` ${FAIL} Could not parse ${SETTINGS_PATH} (${stopRead.reason}) — left untouched`);
|
|
260
|
+
console.log(` ${DIM}Fix the file, or add the Stop hook by hand: ${HOOK_COMMAND}${RESET}`);
|
|
221
261
|
}
|
|
222
262
|
else {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
263
|
+
const settings = stopRead.settings;
|
|
264
|
+
// Swap the Stop hook forms setup wrote (the exact 0.17.0 npx string, or our marked direct form).
|
|
265
|
+
// Only the sync kind here: the guard hooks are step 4's, after its --no-guard check.
|
|
266
|
+
const replaced = rewireLegacyHooks(settings, HOOK_COMMANDS, ['sync']).replaced;
|
|
267
|
+
// Recognise every shape the sync hook has been wired in (npx subcommand, global bin, dist
|
|
268
|
+
// path, exec form) — a narrower probe appended a second copy, see lib/guard-wiring.ts.
|
|
269
|
+
const alreadyHooked = syncWiredFor(settings, 'Stop');
|
|
270
|
+
if (alreadyHooked && replaced.length === 0) {
|
|
271
|
+
console.log(` ${SKIP} Stop hook already configured`);
|
|
272
|
+
}
|
|
273
|
+
else if (dryRun) {
|
|
274
|
+
console.log(` ${DIM}[dry-run] Would ${alreadyHooked ? 'switch the npx hooks to direct node calls in' : 'add Stop hook to'} ${SETTINGS_PATH}${RESET}`);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
if (!alreadyHooked) {
|
|
278
|
+
if (!settings.hooks)
|
|
279
|
+
settings.hooks = {};
|
|
280
|
+
if (!settings.hooks.Stop)
|
|
281
|
+
settings.hooks.Stop = [];
|
|
282
|
+
settings.hooks.Stop.push({
|
|
283
|
+
matcher: '',
|
|
284
|
+
hooks: [{ type: 'command', command: HOOK_COMMAND }],
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const w = safeWrite(SETTINGS_PATH, settings);
|
|
288
|
+
if (w) {
|
|
289
|
+
if (replaced.length)
|
|
290
|
+
console.log(` ${CHECK} Repointed ${replaced.length} hook(s) to ${HOOK_COMMANDS.sync.startsWith('npx') ? 'npx' : 'a direct node call'} → ${SETTINGS_PATH}`);
|
|
291
|
+
if (!alreadyHooked)
|
|
292
|
+
console.log(` ${CHECK} Stop hook added → ${SETTINGS_PATH}`);
|
|
293
|
+
if (w.backup)
|
|
294
|
+
console.log(` ${DIM}Previous file kept at ${w.backup}${RESET}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
235
297
|
}
|
|
236
298
|
console.log('');
|
|
237
299
|
// ── Step 4: Wire the re-injection guard (user-global by default) ─────────
|
|
@@ -259,24 +321,29 @@ async function configureGuard() {
|
|
|
259
321
|
console.log(` ${SKIP} Skipped (--no-guard). Enable later: npx -y linksee-memory setup`);
|
|
260
322
|
return false;
|
|
261
323
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
catch {
|
|
270
|
-
// Never clobber a file we can't parse — the user may have hand-authored it.
|
|
271
|
-
console.log(` ${FAIL} Could not parse ${targetPath} — left untouched`);
|
|
272
|
-
console.log(` ${DIM}Add the guard block by hand (see README → Re-injection Guard).${RESET}`);
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
324
|
+
// BOM stripped; never clobber a file we can't parse — the user may have hand-authored it.
|
|
325
|
+
const read = readSettingsFile(targetPath);
|
|
326
|
+
if (!read.ok) {
|
|
327
|
+
console.log(` ${FAIL} Could not parse ${targetPath} (${read.reason}) — left untouched`);
|
|
328
|
+
console.log(` ${DIM}Add the guard block by hand (see README → Re-injection Guard).${RESET}`);
|
|
329
|
+
return false;
|
|
275
330
|
}
|
|
276
|
-
|
|
331
|
+
const settings = read.settings;
|
|
332
|
+
// Known npx forms → direct node call (exact matches only; see lib/guard-wiring.ts).
|
|
333
|
+
const { replaced } = rewireLegacyHooks(settings, HOOK_COMMANDS, ['guard']);
|
|
334
|
+
if (guardFullyWired(settings) && replaced.length === 0) {
|
|
277
335
|
console.log(` ${SKIP} Guard already wired in ${targetPath}`);
|
|
278
336
|
return true;
|
|
279
337
|
}
|
|
338
|
+
if (guardFullyWired(settings) && !dryRun) {
|
|
339
|
+
const w = safeWrite(targetPath, settings);
|
|
340
|
+
if (!w)
|
|
341
|
+
return false;
|
|
342
|
+
console.log(` ${CHECK} Repointed ${replaced.length} guard hook(s) → ${targetPath}`);
|
|
343
|
+
if (w.backup)
|
|
344
|
+
console.log(` ${DIM}Previous file kept at ${w.backup}${RESET}`);
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
280
347
|
if (dryRun) {
|
|
281
348
|
console.log(` ${DIM}[dry-run] Would merge SessionStart + PreToolUse guard hooks into ${targetPath}${RESET}`);
|
|
282
349
|
return false;
|
|
@@ -294,10 +361,13 @@ async function configureGuard() {
|
|
|
294
361
|
}
|
|
295
362
|
}
|
|
296
363
|
// Merge, don't replace (see lib/guard-wiring.ts for the rules it holds to).
|
|
297
|
-
wireGuard(settings);
|
|
298
|
-
|
|
299
|
-
|
|
364
|
+
wireGuard(settings, HOOK_COMMANDS.guard);
|
|
365
|
+
const w = safeWrite(targetPath, settings);
|
|
366
|
+
if (!w)
|
|
367
|
+
return false;
|
|
300
368
|
console.log(` ${CHECK} Guard wired → ${targetPath} ${DIM}(${scopeLabel})${RESET}`);
|
|
369
|
+
if (w.backup)
|
|
370
|
+
console.log(` ${DIM}Previous file kept at ${w.backup}${RESET}`);
|
|
301
371
|
if (!projectGuard) {
|
|
302
372
|
console.log(` ${DIM}Active in every repo. Scope an anchor with \`affects\` globs to limit where it fires;`);
|
|
303
373
|
console.log(` --no-guard to skip, --project-guard for this repo only.${RESET}`);
|
|
@@ -307,6 +377,25 @@ async function configureGuard() {
|
|
|
307
377
|
const guardConfigured = await configureGuard();
|
|
308
378
|
console.log('');
|
|
309
379
|
// ── Telemetry consent (opt-in, anonymous, off unless you agree) ──────────
|
|
380
|
+
/** True when there is no memory yet (or it cannot be read) — used only to pick the first-try text. */
|
|
381
|
+
function memoryIsEmpty() {
|
|
382
|
+
const dbPath = join(TELEMETRY_DIR, 'memory.db');
|
|
383
|
+
if (!existsSync(dbPath))
|
|
384
|
+
return true;
|
|
385
|
+
try {
|
|
386
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
387
|
+
try {
|
|
388
|
+
const row = db.prepare('SELECT COUNT(*) AS c FROM memories').get();
|
|
389
|
+
return !row || row.c === 0;
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
db.close();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
310
399
|
async function configureTelemetryConsent() {
|
|
311
400
|
console.log(`${BOLD}Anonymous usage stats${RESET} ${DIM}(optional)${RESET}`);
|
|
312
401
|
const env = (process.env.LINKSEE_TELEMETRY || '').toLowerCase().trim();
|
|
@@ -355,19 +444,45 @@ console.log('');
|
|
|
355
444
|
console.log(`${BOLD}Setup complete!${RESET}`);
|
|
356
445
|
console.log('');
|
|
357
446
|
console.log('How it works:');
|
|
358
|
-
console.log(` ${DIM}• Every session is auto-captured (decisions, caveats, learnings)${RESET}`);
|
|
447
|
+
console.log(` ${DIM}• Every Claude Code session is auto-captured (decisions, caveats, learnings)${RESET}`);
|
|
359
448
|
console.log(` ${DIM}• Agent auto-recalls past context when starting a task${RESET}`);
|
|
360
449
|
console.log(` ${DIM}• Memory is local-first (your memory never leaves your machine)${RESET}`);
|
|
361
|
-
console.log(` ${DIM}•
|
|
450
|
+
console.log(` ${DIM}• The memory is shared over MCP by 5 clients (Claude Code, Cursor, Windsurf, Codex, Gemini CLI);${RESET}`);
|
|
451
|
+
console.log(` ${DIM} auto-capture and the guard run in Claude Code only. Codex: codex mcp add linksee -- npx -y linksee-memory${RESET}`);
|
|
362
452
|
if (guardConfigured) {
|
|
363
|
-
console.log(` ${DIM}• Re-injection guard re-surfaces
|
|
453
|
+
console.log(` ${DIM}• Re-injection guard re-surfaces your accepted decisions before edits (${projectGuard ? 'this project' : 'every repo'})${RESET}`);
|
|
364
454
|
}
|
|
365
455
|
console.log('');
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
console.log(` ${
|
|
370
|
-
console.log(` ${
|
|
456
|
+
// 0.17.1 never deletes pinned runtimes (Codex fourth review of #15: any automatic delete risked
|
|
457
|
+
// removing one a hook still uses). Say what they cost and how to remove old ones by hand.
|
|
458
|
+
if (runtime.mode === 'pinned' && runtime.runtimeBase && !dryRun) {
|
|
459
|
+
console.log(` ${DIM}• Hooks run from ${runtime.runtimeBase}${sep}<version> (${runtime.bytes !== undefined ? formatBytes(runtime.bytes) : 'about 35 MB'} each). Older versions are not removed automatically;${RESET}`);
|
|
460
|
+
console.log(` ${DIM} 0.17.2 adds \`linksee-memory runtime prune\`. Until then you can delete old ones by hand: ${runtime.runtimeBase}${RESET}`);
|
|
461
|
+
console.log('');
|
|
462
|
+
}
|
|
463
|
+
handWiredNotice(SETTINGS_PATH);
|
|
464
|
+
if (projectGuard)
|
|
465
|
+
handWiredNotice(PROJECT_SETTINGS_PATH);
|
|
466
|
+
if (handWired.size) {
|
|
467
|
+
console.log('Left as you wired them (not written by setup, so not changed):');
|
|
468
|
+
for (const c of handWired)
|
|
469
|
+
console.log(` ${DIM}• ${c}${RESET}`);
|
|
470
|
+
console.log(` ${DIM}setup writes "<node>" "<package>/dist/bin/<script>.js" --linksee-managed; switch to that form if you want setup to manage them.${RESET}`);
|
|
471
|
+
console.log('');
|
|
472
|
+
}
|
|
473
|
+
if (memoryIsEmpty()) {
|
|
474
|
+
// A fresh install has nothing to recall, so "how did we solve this before?" always comes back
|
|
475
|
+
// empty — the first thing a new user tries would fail. Offer a round trip that cannot.
|
|
476
|
+
console.log('Try it (your memory is empty, so start by giving it something):');
|
|
477
|
+
console.log(` 1. Say: ${BOLD}"Remember: in this repo we decided to use X, because Y."${RESET}`);
|
|
478
|
+
console.log(` 2. Open a ${BOLD}new${RESET} session and ask: ${BOLD}"What did we decide about X?"${RESET}`);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
console.log('Test by asking:');
|
|
482
|
+
console.log(` ${BOLD}"How did we solve this before?"${RESET}`);
|
|
483
|
+
console.log(` ${BOLD}"Same error again"${RESET}`);
|
|
484
|
+
console.log(` ${BOLD}「前にこの問題どう解決したっけ」${RESET}`);
|
|
485
|
+
}
|
|
371
486
|
console.log('');
|
|
372
487
|
console.log(`Or add ${BOLD}"Use Linksee"${RESET} to any prompt to trigger memory recall.`);
|
|
373
488
|
console.log('');
|
package/dist/bin/share-draft.js
CHANGED
|
@@ -7,34 +7,34 @@
|
|
|
7
7
|
// npx -y linksee-memory share-draft --service freee --task "..." --symptom "..." \
|
|
8
8
|
// --tried "..." --result "..." --kind execution [--conditions "..."] [--runtime claude] [--json]
|
|
9
9
|
import { buildShareDraft, ALLOWED_KEYS, KINDS, RUNTIMES, NOTICE, NOTICE_EN, DRAFT_PAGE, URL_MAX } from '../lib/share-draft.js';
|
|
10
|
-
const HELP = `linksee-memory share-draft — make a link to a reviewed draft of a fix, for other agents
|
|
11
|
-
|
|
12
|
-
Linksee sends nothing. This prints a URL to ${DRAFT_PAGE}
|
|
13
|
-
The page opens with the fields filled; you read them there, and only your click sends.
|
|
14
|
-
If you never run this (or the MCP tool share_draft), nothing happens.
|
|
15
|
-
|
|
16
|
-
Usage:
|
|
17
|
-
npx -y linksee-memory share-draft --service <id> --task <text> --symptom <text> \\
|
|
18
|
-
--tried <text> --result <text> --kind <${KINDS.join('|')}> \\
|
|
19
|
-
[--conditions <text>] [--runtime <${RUNTIMES.join('|')}>] [--json]
|
|
20
|
-
|
|
21
|
-
Fields (nothing else is accepted):
|
|
22
|
-
--service KanseiLINK service_id, lowercase: ^[a-z0-9_.-]{1,160}$ (required)
|
|
23
|
-
--task what you were trying to do, ≤300 chars (required)
|
|
24
|
-
--symptom the KIND of error, no values, ≤600 chars (required)
|
|
25
|
-
--tried steps you tried, ≤2,000 chars (required)
|
|
26
|
-
--result how it was solved / that it was not, ≤1,000 chars (required)
|
|
27
|
-
--conditions tool + version, kind of environment, ≤600 chars
|
|
28
|
-
--kind execution | documentation | hypothesis (required)
|
|
29
|
-
--runtime claude | codex | gpt | gemini | other
|
|
30
|
-
|
|
31
|
-
Stops (no link) when a field looks like a token / API key / JWT / Bearer value, an e-mail,
|
|
32
|
-
a tenant URL (acme.slack.com …), an IP address, a local path (C:\\, /Users/, /home/), or a
|
|
33
|
-
long hex / base64 run (32+), and says which field. The whole URL must be ≤${URL_MAX} chars
|
|
34
|
-
(one Japanese character takes 9) — it asks you to shorten; it never cuts.
|
|
35
|
-
|
|
36
|
-
${NOTICE[0]}
|
|
37
|
-
${NOTICE[1]}
|
|
10
|
+
const HELP = `linksee-memory share-draft — make a link to a reviewed draft of a fix, for other agents
|
|
11
|
+
|
|
12
|
+
Linksee sends nothing. This prints a URL to ${DRAFT_PAGE}
|
|
13
|
+
The page opens with the fields filled; you read them there, and only your click sends.
|
|
14
|
+
If you never run this (or the MCP tool share_draft), nothing happens.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
npx -y linksee-memory share-draft --service <id> --task <text> --symptom <text> \\
|
|
18
|
+
--tried <text> --result <text> --kind <${KINDS.join('|')}> \\
|
|
19
|
+
[--conditions <text>] [--runtime <${RUNTIMES.join('|')}>] [--json]
|
|
20
|
+
|
|
21
|
+
Fields (nothing else is accepted):
|
|
22
|
+
--service KanseiLINK service_id, lowercase: ^[a-z0-9_.-]{1,160}$ (required)
|
|
23
|
+
--task what you were trying to do, ≤300 chars (required)
|
|
24
|
+
--symptom the KIND of error, no values, ≤600 chars (required)
|
|
25
|
+
--tried steps you tried, ≤2,000 chars (required)
|
|
26
|
+
--result how it was solved / that it was not, ≤1,000 chars (required)
|
|
27
|
+
--conditions tool + version, kind of environment, ≤600 chars
|
|
28
|
+
--kind execution | documentation | hypothesis (required)
|
|
29
|
+
--runtime claude | codex | gpt | gemini | other
|
|
30
|
+
|
|
31
|
+
Stops (no link) when a field looks like a token / API key / JWT / Bearer value, an e-mail,
|
|
32
|
+
a tenant URL (acme.slack.com …), an IP address, a local path (C:\\, /Users/, /home/), or a
|
|
33
|
+
long hex / base64 run (32+), and says which field. The whole URL must be ≤${URL_MAX} chars
|
|
34
|
+
(one Japanese character takes 9) — it asks you to shorten; it never cuts.
|
|
35
|
+
|
|
36
|
+
${NOTICE[0]}
|
|
37
|
+
${NOTICE[1]}
|
|
38
38
|
`;
|
|
39
39
|
function parse(argv) {
|
|
40
40
|
const fields = {};
|
package/dist/bin/sync-session.js
CHANGED
|
@@ -15,7 +15,9 @@ import { join, dirname } from 'node:path';
|
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
import { isTelemetryEnabled, buildPayload, sendTelemetry } from '../lib/telemetry.js';
|
|
18
|
-
import
|
|
18
|
+
import { NATIVE_HINT, isNativeLoadError } from '../lib/native-hint.js';
|
|
19
|
+
// better-sqlite3 is imported dynamically below, so a broken native addon goes through the same
|
|
20
|
+
// one-line hint as everything else instead of failing at module load (Codex third review of #15).
|
|
19
21
|
const LOG_DIR = process.env.LINKSEE_MEMORY_DIR ?? join(homedir(), '.linksee-memory');
|
|
20
22
|
const LOG_FILE = join(LOG_DIR, 'hook.log');
|
|
21
23
|
const LOG_MAX_BYTES = 1024 * 1024; // 1 MB → rotate
|
|
@@ -86,6 +88,10 @@ async function main() {
|
|
|
86
88
|
}
|
|
87
89
|
else if (r.status !== 0) {
|
|
88
90
|
log(`importer exited ${r.status} (session=${sessionId}, ${elapsed}ms): ${(r.stderr || '').slice(0, 500)}`);
|
|
91
|
+
// The native addon loads at `new Database()` inside the importer; after a Node major upgrade
|
|
92
|
+
// that is where it fails. Say so once — still exit 0, the Stop hook never blocks.
|
|
93
|
+
if (isNativeLoadError(r.stderr || ''))
|
|
94
|
+
process.stderr.write(`${NATIVE_HINT}\n`);
|
|
89
95
|
process.exit(0);
|
|
90
96
|
}
|
|
91
97
|
const out = (r.stdout || '').trim().split('\n').slice(-1)[0] || '';
|
|
@@ -114,6 +120,7 @@ async function main() {
|
|
|
114
120
|
const dbDir = process.env.LINKSEE_MEMORY_DIR ?? join(homedir(), '.linksee-memory');
|
|
115
121
|
const dbPath = join(dbDir, 'memory.db');
|
|
116
122
|
if (existsSync(dbPath)) {
|
|
123
|
+
const { default: Database } = await import('better-sqlite3');
|
|
117
124
|
const db = new Database(dbPath, { readonly: true });
|
|
118
125
|
try {
|
|
119
126
|
const payload = buildPayload(db, sessionId, { mcpServersInUse: mcpServers });
|
|
@@ -132,6 +139,8 @@ async function main() {
|
|
|
132
139
|
}
|
|
133
140
|
catch (e) {
|
|
134
141
|
log(`telemetry: error (non-fatal): ${e?.message ?? e}`);
|
|
142
|
+
if (isNativeLoadError(e))
|
|
143
|
+
process.stderr.write(`${NATIVE_HINT}\n`);
|
|
135
144
|
}
|
|
136
145
|
}
|
|
137
146
|
process.exit(0);
|
package/dist/db/migrate.d.ts
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
export declare function getDbPath(): string;
|
|
3
3
|
export declare function openDb(): Database.Database;
|
|
4
|
+
/** Oldest schema this version migrates in place. Older files are refused before any write. */
|
|
5
|
+
export declare const MIN_SUPPORTED_SCHEMA = 6;
|
|
6
|
+
/** Base class: openDb() refused this file without writing to it. The message says what to do. */
|
|
7
|
+
export declare class DbRefusedError extends Error {
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Thrown for a schema older than MIN_SUPPORTED_SCHEMA (a DB last opened by a 2026-05-or-earlier
|
|
11
|
+
* version). Raised by openDb() from a read-only connection, before any writable connection exists,
|
|
12
|
+
* and again by runMigrations() as a second line.
|
|
13
|
+
*
|
|
14
|
+
* No old version is recommended as a stepping stone: 0.8.0–0.17.0 stop on `no such table:
|
|
15
|
+
* drift_anchors` after partly altering the file, and 0.7.2 has no subcommands — `npx
|
|
16
|
+
* linksee-memory@0.7.2 <anything>` starts its MCP server, whose start-up consolidate can delete
|
|
17
|
+
* original rows (Codex review of PR #15). A migration command for these files is 0.17.2 work.
|
|
18
|
+
*/
|
|
19
|
+
export declare class LegacySchemaError extends DbRefusedError {
|
|
20
|
+
readonly schemaVersion: number;
|
|
21
|
+
constructor(schemaVersion: number, dbPath: string);
|
|
22
|
+
}
|
|
23
|
+
/** The read-only check could not run (busy, locked, unreadable): refused rather than guessed. */
|
|
24
|
+
export declare class PrecheckError extends DbRefusedError {
|
|
25
|
+
readonly reason: string;
|
|
26
|
+
constructor(dbPath: string, reason: string);
|
|
27
|
+
}
|
|
28
|
+
/** A rollback journal left by an interrupted write: it cannot be read without being rolled back. */
|
|
29
|
+
export declare class HotJournalError extends DbRefusedError {
|
|
30
|
+
constructor(dbPath: string);
|
|
31
|
+
}
|
|
32
|
+
/** Read meta.schema_version without writing (0 when the table or row is absent). */
|
|
33
|
+
export declare function readSchemaVersion(db: Database.Database): number;
|
|
4
34
|
export declare function runMigrations(db: Database.Database): void;
|