linksee-memory 0.12.1 → 0.13.0
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 +8 -1
- package/dist/bin/setup.js +60 -47
- package/dist/lib/guard-wiring.d.ts +54 -0
- package/dist/lib/guard-wiring.js +85 -0
- package/dist/lib/guard.js +10 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,7 +207,14 @@ It is **fail-open by construction**: any parse / DB / logic error surfaces nothi
|
|
|
207
207
|
|
|
208
208
|
### Enable it
|
|
209
209
|
|
|
210
|
-
`npx -y linksee-memory setup`
|
|
210
|
+
`npx -y linksee-memory setup` wires this into `~/.claude/settings.json` (Step 4), so it is on in **every** repo — the same scope your memory already lives at. One SQLite file holds the anchors for all your projects; enforcing them per-repo meant declaring a decision once and having it enforced nowhere.
|
|
211
|
+
|
|
212
|
+
- `--project-guard` — this repo only, the old behaviour
|
|
213
|
+
- `--no-guard` — skip it
|
|
214
|
+
|
|
215
|
+
Anchors with `affects` globs fire only on matching paths; an unscoped anchor fires on its own `detect_terms` / `violation_signal`. Nothing is ever **blocked** unless you explicitly hardened it (`resolve_drift(action:'harden')`) — everything else re-injects the decision as context.
|
|
216
|
+
|
|
217
|
+
To wire it by hand instead, drop this block into `.claude/settings.json` (project root, or `~/.claude/settings.json` for every repo) — it points at the globally-installed `linksee-memory-guard` bin, so no build step is needed:
|
|
211
218
|
|
|
212
219
|
```json
|
|
213
220
|
{
|
package/dist/bin/setup.js
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
// setup: One-command setup for Linksee Memory — the "Use Linksee" installer.
|
|
3
3
|
//
|
|
4
4
|
// Usage:
|
|
5
|
-
// npx linksee-memory setup
|
|
6
|
-
// npx linksee-memory setup --yes
|
|
5
|
+
// npx linksee-memory setup (interactive setup)
|
|
6
|
+
// npx linksee-memory setup --yes (accept all defaults, no prompts)
|
|
7
7
|
// npx linksee-memory setup --dry-run
|
|
8
|
+
// npx linksee-memory setup --no-guard (skip the re-injection guard)
|
|
9
|
+
// npx linksee-memory setup --project-guard (wire the guard to THIS repo only)
|
|
8
10
|
//
|
|
9
11
|
// Does four things:
|
|
10
12
|
// 1. Registers the MCP server with Claude Code
|
|
11
13
|
// 2. Installs the SKILL.md (agent trigger phrases)
|
|
12
14
|
// 3. Configures the Stop hook (auto-capture sessions) — user-global
|
|
13
|
-
// 4.
|
|
15
|
+
// 4. Wires the re-injection guard into ~/.claude/settings.json (every project)
|
|
14
16
|
//
|
|
15
17
|
// After setup, every Claude Code session:
|
|
16
18
|
// - Auto-captures decisions, learnings, caveats to local memory
|
|
@@ -21,15 +23,30 @@
|
|
|
21
23
|
// Why: Competing memory tools (claude-mem, etc.) are one-install-and-done.
|
|
22
24
|
// Our MCP approach gives more precision, but the setup was 3 manual steps.
|
|
23
25
|
// This command eliminates that friction entirely.
|
|
26
|
+
//
|
|
27
|
+
// Why the guard is user-global (2026-09-07): memory is global — one SQLite file holding the
|
|
28
|
+
// anchors for every repo — but the guard used to be wired per project, opt-in. So the anchors
|
|
29
|
+
// existed everywhere and were enforced nowhere. The author's own machine had 42 active anchors
|
|
30
|
+
// and no PreToolUse hook in any project; the one layer no competing tool has was switched off
|
|
31
|
+
// where it was written. A founder running twenty repos should not run setup twenty times.
|
|
32
|
+
//
|
|
33
|
+
// Safe by construction: the hook is fail-open, and it can only DENY when an anchor was
|
|
34
|
+
// explicitly hardened via resolve_drift(action:'harden'). Anything else re-injects text.
|
|
35
|
+
// Anchors with `affects` globs only fire on matching paths; unscoped ones fire on their own
|
|
36
|
+
// detect_terms / violation_signal, so cross-repo noise is bounded by what you declared.
|
|
24
37
|
import { spawnSync } from 'node:child_process';
|
|
25
38
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
26
39
|
import { join, dirname } from 'node:path';
|
|
27
40
|
import { homedir } from 'node:os';
|
|
28
41
|
import { fileURLToPath } from 'node:url';
|
|
29
42
|
import { createInterface } from 'node:readline';
|
|
43
|
+
import { guardFullyWired, wireGuard, syncWiredFor } from '../lib/guard-wiring.js';
|
|
30
44
|
const args = process.argv.slice(2);
|
|
31
45
|
const dryRun = args.includes('--dry-run');
|
|
32
46
|
const autoYes = args.includes('--yes') || args.includes('-y');
|
|
47
|
+
const noGuard = args.includes('--no-guard');
|
|
48
|
+
// Opt back into the old behaviour: wire the guard to this repo instead of every repo.
|
|
49
|
+
const projectGuard = args.includes('--project-guard');
|
|
33
50
|
const showHelp = args.includes('--help') || args.includes('-h');
|
|
34
51
|
if (showHelp) {
|
|
35
52
|
console.log(`linksee-memory-setup — One-command setup for Linksee Memory
|
|
@@ -62,11 +79,9 @@ const MCP_COMMAND = `claude mcp add -s user ${SERVER_NAME} -- npx -y linksee-mem
|
|
|
62
79
|
// Subcommand form (npx -y linksee-memory <sub>) so the hooks resolve for a cold user —
|
|
63
80
|
// npx can resolve the package name, but not sibling bin names like linksee-memory-sync.
|
|
64
81
|
const HOOK_COMMAND = 'npx -y linksee-memory sync';
|
|
65
|
-
// Re-injection guard
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// form (resolved at run time) survives npx-cache eviction; a baked dist path would not.
|
|
69
|
-
const GUARD_COMMAND = 'npx -y linksee-memory guard';
|
|
82
|
+
// Re-injection guard: command + merge rules live in lib/guard-wiring.ts (shared with the tests).
|
|
83
|
+
// It points at the globally-installed bin rather than a dist path, so it ships without a build
|
|
84
|
+
// step, and the shell form is resolved at run time so it survives npx-cache eviction.
|
|
70
85
|
const PROJECT_DIR = process.cwd();
|
|
71
86
|
const PROJECT_CLAUDE_DIR = join(PROJECT_DIR, '.claude');
|
|
72
87
|
const PROJECT_SETTINGS_PATH = join(PROJECT_CLAUDE_DIR, 'settings.json');
|
|
@@ -195,7 +210,9 @@ if (existsSync(SETTINGS_PATH)) {
|
|
|
195
210
|
}
|
|
196
211
|
// Check if hook already exists
|
|
197
212
|
const stopHooks = settings?.hooks?.Stop ?? [];
|
|
198
|
-
|
|
213
|
+
// Recognise every shape the sync hook has been wired in (npx subcommand, global bin, dist
|
|
214
|
+
// path, exec form) — a narrower probe appended a second copy, see lib/guard-wiring.ts.
|
|
215
|
+
const alreadyHooked = syncWiredFor(settings, 'Stop');
|
|
199
216
|
if (alreadyHooked) {
|
|
200
217
|
console.log(` ${SKIP} Stop hook already configured`);
|
|
201
218
|
}
|
|
@@ -217,17 +234,7 @@ else {
|
|
|
217
234
|
console.log(` ${CHECK} Stop hook added → ${SETTINGS_PATH}`);
|
|
218
235
|
}
|
|
219
236
|
console.log('');
|
|
220
|
-
|
|
221
|
-
const GUARD_HOOKS = {
|
|
222
|
-
// matchers + timeouts mirror the dogfood .claude/settings.json
|
|
223
|
-
SessionStart: { matcher: 'startup|resume|compact', hooks: [{ type: 'command', command: GUARD_COMMAND, timeout: 15 }] },
|
|
224
|
-
PreToolUse: { matcher: 'Edit|Write|Bash', hooks: [{ type: 'command', command: GUARD_COMMAND, timeout: 8 }] },
|
|
225
|
-
};
|
|
226
|
-
// Idempotency probe: is OUR guard already wired for this event? (Match by bin name so a manually-added
|
|
227
|
-
// or previously-installed entry isn't duplicated, and other people's hooks are never touched.)
|
|
228
|
-
function guardWiredFor(s, ev) {
|
|
229
|
-
return (s.hooks?.[ev] ?? []).some((entry) => entry?.hooks?.some((h) => typeof h?.command === 'string' && h.command.includes('linksee-memory-guard')));
|
|
230
|
-
}
|
|
237
|
+
// ── Step 4: Wire the re-injection guard (user-global by default) ─────────
|
|
231
238
|
function askYesNo(question, defaultYes = true) {
|
|
232
239
|
return new Promise((resolve) => {
|
|
233
240
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -241,54 +248,60 @@ function askYesNo(question, defaultYes = true) {
|
|
|
241
248
|
});
|
|
242
249
|
}
|
|
243
250
|
async function configureGuard() {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
251
|
+
// Default target is the user-global settings — the same scope the MCP server is registered
|
|
252
|
+
// at, and the same scope the memory itself lives at. --project-guard keeps the old per-repo
|
|
253
|
+
// behaviour for people who want the guard in one repo only.
|
|
254
|
+
const targetPath = projectGuard ? PROJECT_SETTINGS_PATH : SETTINGS_PATH;
|
|
255
|
+
const targetDir = projectGuard ? PROJECT_CLAUDE_DIR : CLAUDE_DIR;
|
|
256
|
+
const scopeLabel = projectGuard ? 'this project' : 'all projects';
|
|
257
|
+
console.log(`${BOLD}[4/4]${RESET} Configuring re-injection guard (${scopeLabel})...`);
|
|
258
|
+
if (noGuard) {
|
|
259
|
+
console.log(` ${SKIP} Skipped (--no-guard). Enable later: npx -y linksee-memory setup`);
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
let settings = {};
|
|
263
|
+
if (existsSync(targetPath)) {
|
|
247
264
|
try {
|
|
248
265
|
// Strip a leading BOM (U+FEFF) — Windows editors (Notepad) emit UTF-8+BOM, which JSON.parse rejects.
|
|
249
|
-
const raw = readFileSync(
|
|
250
|
-
|
|
266
|
+
const raw = readFileSync(targetPath, 'utf8');
|
|
267
|
+
settings = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);
|
|
251
268
|
}
|
|
252
269
|
catch {
|
|
253
270
|
// Never clobber a file we can't parse — the user may have hand-authored it.
|
|
254
|
-
console.log(` ${FAIL} Could not parse ${
|
|
271
|
+
console.log(` ${FAIL} Could not parse ${targetPath} — left untouched`);
|
|
255
272
|
console.log(` ${DIM}Add the guard block by hand (see README → Re-injection Guard).${RESET}`);
|
|
256
273
|
return false;
|
|
257
274
|
}
|
|
258
275
|
}
|
|
259
|
-
if (
|
|
260
|
-
console.log(` ${SKIP} Guard already wired in ${
|
|
276
|
+
if (guardFullyWired(settings)) {
|
|
277
|
+
console.log(` ${SKIP} Guard already wired in ${targetPath}`);
|
|
261
278
|
return true;
|
|
262
279
|
}
|
|
263
280
|
if (dryRun) {
|
|
264
|
-
console.log(` ${DIM}[dry-run] Would merge SessionStart + PreToolUse guard hooks into ${
|
|
281
|
+
console.log(` ${DIM}[dry-run] Would merge SessionStart + PreToolUse guard hooks into ${targetPath}${RESET}`);
|
|
265
282
|
return false;
|
|
266
283
|
}
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
284
|
+
// Ask when there is someone to ask. A non-interactive run used to skip silently, which is
|
|
285
|
+
// how the guard ended up installed nowhere — setup's job is to configure, so it configures
|
|
286
|
+
// and says so loudly instead.
|
|
287
|
+
if (!autoYes && process.stdin.isTTY) {
|
|
273
288
|
console.log(` ${DIM}Re-injects your accepted decisions before Edit/Write/Bash and on session start.`);
|
|
274
|
-
console.log(` Fail-open — only an action
|
|
275
|
-
const ok = await askYesNo(` Wire it into ${
|
|
289
|
+
console.log(` Fail-open — only an action contradicting a 'hard' anchor is ever blocked.${RESET}`);
|
|
290
|
+
const ok = await askYesNo(` Wire it into ${targetPath}?`);
|
|
276
291
|
if (!ok) {
|
|
277
292
|
console.log(` ${SKIP} Skipped. Enable later via the README → Re-injection Guard.`);
|
|
278
293
|
return false;
|
|
279
294
|
}
|
|
280
295
|
}
|
|
281
|
-
// Merge, don't replace
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
296
|
+
// Merge, don't replace (see lib/guard-wiring.ts for the rules it holds to).
|
|
297
|
+
wireGuard(settings);
|
|
298
|
+
mkdirSync(targetDir, { recursive: true });
|
|
299
|
+
writeFileSync(targetPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
300
|
+
console.log(` ${CHECK} Guard wired → ${targetPath} ${DIM}(${scopeLabel})${RESET}`);
|
|
301
|
+
if (!projectGuard) {
|
|
302
|
+
console.log(` ${DIM}Active in every repo. Scope an anchor with \`affects\` globs to limit where it fires;`);
|
|
303
|
+
console.log(` --no-guard to skip, --project-guard for this repo only.${RESET}`);
|
|
288
304
|
}
|
|
289
|
-
mkdirSync(PROJECT_CLAUDE_DIR, { recursive: true });
|
|
290
|
-
writeFileSync(PROJECT_SETTINGS_PATH, JSON.stringify(project, null, 2), 'utf8');
|
|
291
|
-
console.log(` ${CHECK} Guard wired → ${PROJECT_SETTINGS_PATH}`);
|
|
292
305
|
return true;
|
|
293
306
|
}
|
|
294
307
|
const guardConfigured = await configureGuard();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type HookCommand = {
|
|
2
|
+
type: string;
|
|
3
|
+
command?: string;
|
|
4
|
+
timeout?: number;
|
|
5
|
+
args?: string[];
|
|
6
|
+
};
|
|
7
|
+
export type HookEntry = {
|
|
8
|
+
matcher?: string;
|
|
9
|
+
hooks?: HookCommand[];
|
|
10
|
+
};
|
|
11
|
+
export type ClaudeSettings = {
|
|
12
|
+
hooks?: Record<string, HookEntry[]>;
|
|
13
|
+
[k: string]: unknown;
|
|
14
|
+
};
|
|
15
|
+
export declare const GUARD_EVENTS: readonly ["SessionStart", "PreToolUse"];
|
|
16
|
+
export type GuardEvent = (typeof GUARD_EVENTS)[number];
|
|
17
|
+
/** The bin every wiring form resolves to; used as the idempotency key. */
|
|
18
|
+
export declare const GUARD_BIN = "linksee-memory-guard";
|
|
19
|
+
export declare const GUARD_COMMAND = "npx -y linksee-memory guard";
|
|
20
|
+
export declare const GUARD_HOOKS: Record<GuardEvent, HookEntry>;
|
|
21
|
+
/**
|
|
22
|
+
* Is one of OUR hooks of `kind` already wired for this event?
|
|
23
|
+
*
|
|
24
|
+
* Shared by the guard and the session-sync hook because they hit the same trap: each has been
|
|
25
|
+
* wired as an npx subcommand, as a global bin, as a dist path, and in exec form with the path
|
|
26
|
+
* in `args`. A probe that knows only one shape appends a duplicate — which is exactly what
|
|
27
|
+
* happened to the Stop hook on 2026-09-07 (`sync-session.js` did not match `linksee-memory-sync`,
|
|
28
|
+
* so setup added a second one and sessions were captured twice).
|
|
29
|
+
*/
|
|
30
|
+
export declare function linkseeHookWired(settings: ClaudeSettings, event: string, kind: 'guard' | 'sync'): boolean;
|
|
31
|
+
/** Is the session-sync (Stop) hook already wired? */
|
|
32
|
+
export declare function syncWiredFor(settings: ClaudeSettings, event?: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Is OUR guard already wired for this event?
|
|
35
|
+
*
|
|
36
|
+
* Has to recognise every shape the guard has ever been wired in, or setup duplicates it:
|
|
37
|
+
* npx -y linksee-memory guard (what setup writes)
|
|
38
|
+
* linksee-memory-guard (the global bin)
|
|
39
|
+
* node /path/to/linksee-memory/dist/bin/guard-hook.js (the old README block)
|
|
40
|
+
* { command: 'node', args: ['.../dist/bin/guard-hook.js'] } (exec form — the path is in args)
|
|
41
|
+
*
|
|
42
|
+
* The last two put the identifying part in different places, so match against command and args
|
|
43
|
+
* joined together. `guard-hook` alone is accepted because the exec form carries no package name.
|
|
44
|
+
*/
|
|
45
|
+
export declare function guardWiredFor(settings: ClaudeSettings, event: string): boolean;
|
|
46
|
+
export declare function guardFullyWired(settings: ClaudeSettings): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Add the guard to any event it does not already own. Mutates and returns `settings`, plus the
|
|
49
|
+
* events that were actually added (empty when it was already wired).
|
|
50
|
+
*/
|
|
51
|
+
export declare function wireGuard(settings: ClaudeSettings): {
|
|
52
|
+
settings: ClaudeSettings;
|
|
53
|
+
added: GuardEvent[];
|
|
54
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// guard-wiring — merge the re-injection guard's hooks into a Claude Code settings object.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from bin/setup.ts so the merge is testable without running the installer (which
|
|
4
|
+
// also registers an MCP server and copies a skill). The rules that matter here:
|
|
5
|
+
//
|
|
6
|
+
// • merge, never replace — other people's hooks in the same event must survive
|
|
7
|
+
// • idempotent — running setup twice must not produce two guard entries
|
|
8
|
+
// • recognise a hand-pasted guard from the README as already-wired (match on the bin name,
|
|
9
|
+
// not on the exact command string, which differs between `npx` and a dist path)
|
|
10
|
+
export const GUARD_EVENTS = ['SessionStart', 'PreToolUse'];
|
|
11
|
+
/** The bin every wiring form resolves to; used as the idempotency key. */
|
|
12
|
+
export const GUARD_BIN = 'linksee-memory-guard';
|
|
13
|
+
export const GUARD_COMMAND = 'npx -y linksee-memory guard';
|
|
14
|
+
export const GUARD_HOOKS = {
|
|
15
|
+
SessionStart: {
|
|
16
|
+
matcher: 'startup|resume|compact',
|
|
17
|
+
hooks: [{ type: 'command', command: GUARD_COMMAND, timeout: 15 }],
|
|
18
|
+
},
|
|
19
|
+
PreToolUse: {
|
|
20
|
+
matcher: 'Edit|Write|Bash',
|
|
21
|
+
hooks: [{ type: 'command', command: GUARD_COMMAND, timeout: 8 }],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
/** Everything a hook entry could carry an identifier in: `command`, or `args` for the exec form. */
|
|
25
|
+
function hookHaystack(h) {
|
|
26
|
+
return [h?.command, ...(h?.args ?? [])].filter((x) => typeof x === 'string').join(' ');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Is one of OUR hooks of `kind` already wired for this event?
|
|
30
|
+
*
|
|
31
|
+
* Shared by the guard and the session-sync hook because they hit the same trap: each has been
|
|
32
|
+
* wired as an npx subcommand, as a global bin, as a dist path, and in exec form with the path
|
|
33
|
+
* in `args`. A probe that knows only one shape appends a duplicate — which is exactly what
|
|
34
|
+
* happened to the Stop hook on 2026-09-07 (`sync-session.js` did not match `linksee-memory-sync`,
|
|
35
|
+
* so setup added a second one and sessions were captured twice).
|
|
36
|
+
*/
|
|
37
|
+
export function linkseeHookWired(settings, event, kind) {
|
|
38
|
+
const bare = kind === 'guard' ? 'guard-hook' : 'sync-session';
|
|
39
|
+
return (settings.hooks?.[event] ?? []).some((entry) => entry?.hooks?.some((h) => {
|
|
40
|
+
const hay = hookHaystack(h);
|
|
41
|
+
if (!hay)
|
|
42
|
+
return false;
|
|
43
|
+
return hay.includes(bare) || (hay.includes('linksee-memory') && hay.includes(kind));
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
/** Is the session-sync (Stop) hook already wired? */
|
|
47
|
+
export function syncWiredFor(settings, event = 'Stop') {
|
|
48
|
+
return linkseeHookWired(settings, event, 'sync');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Is OUR guard already wired for this event?
|
|
52
|
+
*
|
|
53
|
+
* Has to recognise every shape the guard has ever been wired in, or setup duplicates it:
|
|
54
|
+
* npx -y linksee-memory guard (what setup writes)
|
|
55
|
+
* linksee-memory-guard (the global bin)
|
|
56
|
+
* node /path/to/linksee-memory/dist/bin/guard-hook.js (the old README block)
|
|
57
|
+
* { command: 'node', args: ['.../dist/bin/guard-hook.js'] } (exec form — the path is in args)
|
|
58
|
+
*
|
|
59
|
+
* The last two put the identifying part in different places, so match against command and args
|
|
60
|
+
* joined together. `guard-hook` alone is accepted because the exec form carries no package name.
|
|
61
|
+
*/
|
|
62
|
+
export function guardWiredFor(settings, event) {
|
|
63
|
+
return linkseeHookWired(settings, event, 'guard');
|
|
64
|
+
}
|
|
65
|
+
export function guardFullyWired(settings) {
|
|
66
|
+
return GUARD_EVENTS.every((ev) => guardWiredFor(settings, ev));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Add the guard to any event it does not already own. Mutates and returns `settings`, plus the
|
|
70
|
+
* events that were actually added (empty when it was already wired).
|
|
71
|
+
*/
|
|
72
|
+
export function wireGuard(settings) {
|
|
73
|
+
const hooks = (settings.hooks ??= {});
|
|
74
|
+
const added = [];
|
|
75
|
+
for (const ev of GUARD_EVENTS) {
|
|
76
|
+
if (!Array.isArray(hooks[ev]))
|
|
77
|
+
hooks[ev] = [];
|
|
78
|
+
if (!guardWiredFor(settings, ev)) {
|
|
79
|
+
hooks[ev].push(GUARD_HOOKS[ev]);
|
|
80
|
+
added.push(ev);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { settings, added };
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=guard-wiring.js.map
|
package/dist/lib/guard.js
CHANGED
|
@@ -97,9 +97,16 @@ export function matchAction(db, act) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
-
// Scope
|
|
101
|
-
//
|
|
102
|
-
|
|
100
|
+
// Scope. `affects` says WHERE a decision applies; `violation_signal` says WHAT is forbidden.
|
|
101
|
+
// An explicit signal hit is the stronger evidence, so it brings the anchor into scope on its
|
|
102
|
+
// own — otherwise a path-scoped anchor is blind to `Bash`, which carries no file path at all.
|
|
103
|
+
// That blindness was measured on a real machine: 21 of 42 active anchors declared forbidden
|
|
104
|
+
// strings and could never fire on a Bash command — including "ALTER TABLE memories DROP" on
|
|
105
|
+
// the anchor that exists to prevent exactly that. Bash is where the destructive things run.
|
|
106
|
+
//
|
|
107
|
+
// (matchViolation already guards the obvious false positives: word boundaries, a negation
|
|
108
|
+
// window, and citation-without-call — a naive substring test produced ~90% noise.)
|
|
109
|
+
const inScope = sigHit != null || (hasScope ? pathHit : termHit);
|
|
103
110
|
if (!inScope)
|
|
104
111
|
continue;
|
|
105
112
|
out.push({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linksee-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"mcpName": "io.github.michielinksee/linksee-memory",
|
|
5
5
|
"description": "Local-first agent memory MCP — cross-agent brain with drift detection, 6-layer structured memory + token-saving file diff cache",
|
|
6
6
|
"type": "module",
|