linksee-memory 0.7.2 → 0.10.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ // linksee-memory-declare — declare / list / retire drift anchors (v8).
3
+ //
4
+ // The explicit write path for drift observability. Anchors declared here are clean
5
+ // by construction (declare-don't-mine): a human typed them. The bulk seeding script
6
+ // (curate the existing candidate pool) reuses curateAnchorFromMemory() from the lib.
7
+ //
8
+ // Usage:
9
+ // linksee-memory-declare list [--status active|retired] [--kind prohibition|decision|constraint]
10
+ // linksee-memory-declare add --kind <k> --statement "<text>" [--rationale "<text>"]
11
+ // [--affects "glob1,glob2"] [--terms "t1,t2"] [--violation "v1,v2"] [--tier human|explicit]
12
+ // linksee-memory-declare retire --id <n>
13
+ import { openDb, runMigrations } from '../db/migrate.js';
14
+ import { declareAnchor, listAnchors, retireAnchor, setNodeFields, getCurrentTruth, getAlertPolicy, setAlertPolicy, } from '../lib/drift-anchors.js';
15
+ function parseFlags(argv) {
16
+ const out = {};
17
+ for (let i = 0; i < argv.length; i++) {
18
+ const a = argv[i];
19
+ if (a.startsWith('--')) {
20
+ const key = a.slice(2);
21
+ const next = argv[i + 1];
22
+ if (next === undefined || next.startsWith('--')) {
23
+ out[key] = 'true';
24
+ }
25
+ else {
26
+ out[key] = next;
27
+ i++;
28
+ }
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+ function splitList(v) {
34
+ if (!v)
35
+ return [];
36
+ return v
37
+ .split(',')
38
+ .map((s) => s.trim())
39
+ .filter(Boolean);
40
+ }
41
+ function print(obj) {
42
+ process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
43
+ }
44
+ function main() {
45
+ const [, , sub, ...rest] = process.argv;
46
+ const flags = parseFlags(rest);
47
+ const db = openDb();
48
+ runMigrations(db); // ensure drift tables exist when run standalone
49
+ try {
50
+ switch (sub) {
51
+ case 'add': {
52
+ const anchor = declareAnchor(db, {
53
+ kind: flags.kind,
54
+ statement: flags.statement ?? '',
55
+ rationale: flags.rationale,
56
+ affects: splitList(flags.affects),
57
+ detect_terms: splitList(flags.terms),
58
+ violation_signal: splitList(flags.violation),
59
+ tier: flags.tier || undefined,
60
+ });
61
+ // v9 ProjectCoreNode fields (minimal input — only what's given):
62
+ // --node-type --domain --mode --confidence --cadence --stale-days --applies --not-applies
63
+ const nf = {};
64
+ if (flags['node-type'])
65
+ nf.node_type = flags['node-type'];
66
+ if (flags.domain)
67
+ nf.domain = flags.domain;
68
+ if (flags.mode)
69
+ nf.decision_mode = flags.mode;
70
+ if (flags.confidence)
71
+ nf.confidence = Number(flags.confidence);
72
+ if (flags.cadence || flags['stale-days']) {
73
+ const cp = { enabled: true };
74
+ if (flags.cadence)
75
+ cp.cadence_days = Number(flags.cadence);
76
+ if (flags['stale-days'])
77
+ cp.stale_threshold_days = Number(flags['stale-days']);
78
+ nf.card_policy = cp;
79
+ }
80
+ if (flags.applies || flags['not-applies']) {
81
+ const vs = {};
82
+ if (flags.applies)
83
+ vs.applies_to = splitList(flags.applies);
84
+ if (flags['not-applies'])
85
+ vs.does_not_apply_to = splitList(flags['not-applies']);
86
+ nf.validity_scope = vs;
87
+ }
88
+ if (Object.keys(nf).length)
89
+ setNodeFields(db, anchor.id, nf);
90
+ print({ ok: true, declared: anchor, node_fields: Object.keys(nf).length ? nf : null });
91
+ break;
92
+ }
93
+ case 'retire': {
94
+ const id = Number(flags.id);
95
+ if (!Number.isFinite(id))
96
+ throw new Error('--id <n> required');
97
+ const ok = retireAnchor(db, id);
98
+ print({ ok, retired: ok ? id : null });
99
+ break;
100
+ }
101
+ case 'list':
102
+ case undefined: {
103
+ const anchors = listAnchors(db, {
104
+ status: flags.status,
105
+ kind: flags.kind,
106
+ });
107
+ print({ ok: true, count: anchors.length, anchors });
108
+ break;
109
+ }
110
+ case 'truth': {
111
+ // ⑧ read_smart-style scoped read: active Current-Truth slice only.
112
+ const nodes = getCurrentTruth(db, { domain: flags.domain, decision_mode: flags.mode });
113
+ print({ ok: true, scope: { domain: flags.domain ?? 'all', decision_mode: flags.mode ?? 'all' }, count: nodes.length, current_truth: nodes });
114
+ break;
115
+ }
116
+ case 'policy': {
117
+ const p = {};
118
+ if (flags['max-cards-per-day'])
119
+ p.max_cards_per_day = Number(flags['max-cards-per-day']);
120
+ if (flags['max-soft-per-week'])
121
+ p.max_soft_cards_per_week = Number(flags['max-soft-per-week']);
122
+ if (flags['min-soft-confidence'])
123
+ p.min_confidence_for_soft_card = Number(flags['min-soft-confidence']);
124
+ if (flags['two-sided'])
125
+ p.require_two_sided_evidence = flags['two-sided'] !== 'false';
126
+ const policy = Object.keys(p).length ? setAlertPolicy(db, p) : getAlertPolicy(db);
127
+ print({ ok: true, alert_policy: policy });
128
+ break;
129
+ }
130
+ default:
131
+ throw new Error(`unknown subcommand "${sub}" — use: add | list | retire | truth | policy`);
132
+ }
133
+ }
134
+ catch (err) {
135
+ print({ ok: false, error: err?.message ?? String(err) });
136
+ db.close();
137
+ process.exit(1);
138
+ }
139
+ db.close();
140
+ }
141
+ if (import.meta.url === `file://${process.argv[1]}` ||
142
+ process.argv[1]?.endsWith('declare-anchor.ts') ||
143
+ process.argv[1]?.endsWith('declare-anchor.js')) {
144
+ main();
145
+ }
146
+ //# sourceMappingURL=declare-anchor.js.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ // linksee-memory-detect — run the drift detector (照合: declared intent vs. actual reality).
3
+ //
4
+ // The manual-trigger entry point for drift detection (the cadence question). Mirrors
5
+ // declare-anchor.ts; purely additive — touches only drift_edges (the feature's own table) and
6
+ // ONLY when --persist is passed. SAFE BY DEFAULT: a bare run is a dry-run (reads only, no
7
+ // writes), so you can preview drift before committing it to the view. Pass --persist to write
8
+ // the edges that /drift renders. No embedding layer: matching is lexical/glob/trigram-FTS only.
9
+ //
10
+ // Usage:
11
+ // linksee-memory-detect # dry-run — preview drift, writes NOTHING (default)
12
+ // linksee-memory-detect --persist # write contradicts/absent edges into drift_edges
13
+ // linksee-memory-detect --stale-days 30 # override absence staleness gate (default 14)
14
+ // linksee-memory-detect --threshold 0.5 # override emit threshold (default 0.5)
15
+ import { openDb, runMigrations } from '../db/migrate.js';
16
+ import { detectDrift, detectFileViolations } from '../lib/drift-detection.js';
17
+ function parseFlags(argv) {
18
+ const out = {};
19
+ for (let i = 0; i < argv.length; i++) {
20
+ const a = argv[i];
21
+ if (a.startsWith('--')) {
22
+ const key = a.slice(2);
23
+ const next = argv[i + 1];
24
+ if (next === undefined || next.startsWith('--')) {
25
+ out[key] = 'true';
26
+ }
27
+ else {
28
+ out[key] = next;
29
+ i++;
30
+ }
31
+ }
32
+ }
33
+ return out;
34
+ }
35
+ function print(obj) {
36
+ process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
37
+ }
38
+ function main() {
39
+ const [, , ...rest] = process.argv;
40
+ const flags = parseFlags(rest);
41
+ const persist = flags.persist === 'true';
42
+ const staleDays = flags['stale-days'] !== undefined ? Number(flags['stale-days']) : undefined;
43
+ const emitThreshold = flags.threshold !== undefined ? Number(flags.threshold) : undefined;
44
+ if (staleDays !== undefined && !Number.isFinite(staleDays))
45
+ throw new Error('--stale-days <n> must be a number');
46
+ if (emitThreshold !== undefined && !Number.isFinite(emitThreshold))
47
+ throw new Error('--threshold <n> must be a number');
48
+ const db = openDb();
49
+ runMigrations(db); // ensure drift tables exist when run standalone
50
+ try {
51
+ const res = detectDrift(db, { dryRun: !persist, staleDays, emitThreshold });
52
+ const fres = detectFileViolations(db, { dryRun: !persist, emitThreshold });
53
+ print({
54
+ ok: true,
55
+ mode: persist ? 'PERSISTED' : 'DRY RUN (no writes — pass --persist to write)',
56
+ persisted: res.persisted,
57
+ anchorsScanned: res.anchorsScanned,
58
+ editsScanned: res.editsScanned,
59
+ // v1 — edit-snippet scan (what a captured edit's snippet contained)
60
+ editSnippetScan: {
61
+ contradicts: res.contradicts,
62
+ absent: res.absent,
63
+ edgesEmitted: res.edgesEmitted,
64
+ byAnchorHits: res.byAnchor.filter((b) => b.contradicts > 0 || b.absent > 0),
65
+ samples: res.samples.slice(0, 6),
66
+ },
67
+ // v2 — current-file scan (live file:line against violation_signal)
68
+ currentFileScan: {
69
+ filesScanned: fres.filesScanned,
70
+ anchorsCapped: fres.anchorsCapped,
71
+ contradicts: fres.contradicts,
72
+ edgesEmitted: fres.edgesEmitted,
73
+ byAnchorHits: fres.byAnchor,
74
+ samples: fres.samples.slice(0, 14),
75
+ },
76
+ totalEdgesEmitted: res.edgesEmitted + fres.edgesEmitted,
77
+ });
78
+ }
79
+ catch (err) {
80
+ print({ ok: false, error: err?.message ?? String(err) });
81
+ db.close();
82
+ process.exit(1);
83
+ }
84
+ db.close();
85
+ }
86
+ if (import.meta.url === `file://${process.argv[1]}` ||
87
+ process.argv[1]?.endsWith('detect-drift.ts') ||
88
+ process.argv[1]?.endsWith('detect-drift.js')) {
89
+ main();
90
+ }
91
+ //# sourceMappingURL=detect-drift.js.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ // linksee-memory-guard — Claude Code hook adapter for the re-injection layer.
3
+ //
4
+ // Reads a hook event JSON from stdin and emits hook-control JSON to stdout:
5
+ // • PreToolUse (matcher Edit|Write|Bash) → gateAction → block (hard) / soft-inject (warn|inform)
6
+ // • SessionStart (startup|resume|compact) → buildBootDigest → additionalContext
7
+ //
8
+ // FAIL-OPEN by construction: any parse/DB/logic error → NO output, exit 0 → the tool/session proceeds
9
+ // unblocked. The ONLY thing that ever blocks is an explicit `gate_mode:'hard'` contradiction. This is
10
+ // intentional — a guard that breaks the user's workflow on its own bug is a footgun.
11
+ //
12
+ // Wire it (project or ~/.claude/settings.json), exec form so Windows .cmd shims are bypassed:
13
+ // { "hooks": { "PreToolUse": [ { "matcher": "Edit|Write|Bash",
14
+ // "hooks": [ { "type": "command", "command": "node",
15
+ // "args": ["${CLAUDE_PROJECT_DIR}/dist/bin/guard-hook.js"], "timeout": 8 } ] } ] } }
16
+ import { pathToFileURL } from 'node:url';
17
+ import { openDb, runMigrations } from '../db/migrate.js';
18
+ import { gateAction, buildBootDigest } from '../lib/guard.js';
19
+ function readStdin() {
20
+ return new Promise((resolve) => {
21
+ const chunks = [];
22
+ process.stdin.on('data', (c) => chunks.push(c));
23
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
24
+ process.stdin.on('error', () => resolve(''));
25
+ });
26
+ }
27
+ function emit(obj) {
28
+ process.stdout.write(JSON.stringify(obj));
29
+ }
30
+ async function main() {
31
+ let ev;
32
+ try {
33
+ ev = JSON.parse(await readStdin());
34
+ }
35
+ catch {
36
+ process.exit(0); // unparseable stdin → fail-open
37
+ }
38
+ if (!ev || typeof ev !== 'object')
39
+ process.exit(0);
40
+ let db;
41
+ try {
42
+ db = openDb();
43
+ db.pragma('busy_timeout = 2000'); // MCP server may hold a write lock; wait briefly, else fail-open
44
+ runMigrations(db); // ensures injection_log exists even when run standalone
45
+ }
46
+ catch {
47
+ process.exit(0); // can't open DB → never block
48
+ }
49
+ try {
50
+ if (ev.hook_event_name === 'PreToolUse') {
51
+ const ti = (ev.tool_input ?? {});
52
+ const r = gateAction(db, {
53
+ tool: ev.tool_name,
54
+ file_path: ti.file_path,
55
+ command: ti.command,
56
+ content: ti.content,
57
+ diff: ti.new_string ?? ti.diff, // Edit passes new_string; some tools pass diff
58
+ }, { sessionId: ev.session_id });
59
+ if (r.gate === 'block') {
60
+ emit({
61
+ hookSpecificOutput: {
62
+ hookEventName: 'PreToolUse',
63
+ permissionDecision: 'deny',
64
+ permissionDecisionReason: r.reinject,
65
+ },
66
+ });
67
+ }
68
+ else if (r.gate === 'warn' || r.gate === 'inform') {
69
+ emit({
70
+ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: r.reinject },
71
+ });
72
+ }
73
+ // 'allow' → emit nothing
74
+ }
75
+ else if (ev.hook_event_name === 'SessionStart') {
76
+ const d = buildBootDigest(db);
77
+ if (d.text) {
78
+ emit({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: d.text } });
79
+ }
80
+ }
81
+ }
82
+ catch {
83
+ /* fail-open: surface nothing rather than risk blocking on a guard bug */
84
+ }
85
+ try {
86
+ db.close();
87
+ }
88
+ catch {
89
+ /* ignore */
90
+ }
91
+ process.exit(0);
92
+ }
93
+ const invoked = !!process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
94
+ if (invoked)
95
+ main();
96
+ //# sourceMappingURL=guard-hook.js.map
@@ -16,10 +16,10 @@ import { extractSession } from '../lib/session-extractor.js';
16
16
  import { normalizeEntityName } from '../lib/normalize.js';
17
17
  const CLAUDE_PROJECTS = join(homedir(), '.claude', 'projects');
18
18
  function usage() {
19
- console.log(`Usage:
20
- node dist/bin/import-sessions.js [--dry-run] [--all | <projectDir> [<projectDir> ...]]
21
- --all : scan every project under ~/.claude/projects/*
22
- --dry-run : parse + extract but do not write to DB
19
+ console.log(`Usage:
20
+ node dist/bin/import-sessions.js [--dry-run] [--all | <projectDir> [<projectDir> ...]]
21
+ --all : scan every project under ~/.claude/projects/*
22
+ --dry-run : parse + extract but do not write to DB
23
23
  projectDir : absolute path to a project dir (must contain *.jsonl files)`);
24
24
  }
25
25
  function collectJsonlFiles(projectDir) {
@@ -42,13 +42,33 @@ function collectJsonlFiles(projectDir) {
42
42
  }
43
43
  // Idempotent wipe: remove all rows tied to a given session_id BEFORE re-inserting.
44
44
  // Uses LIKE matching on the JSON-encoded source field for memories/events.
45
+ // DISTILLED memories survive the wipe: a session stays ACTIVE while its raw extractions
46
+ // get distilled (dream → remember(memory_id) rewrite), and the next Stop-hook re-import
47
+ // must not destroy that human/agent-curated rewrite and resurrect the raw utterance.
45
48
  function wipeSession(db, sessionId) {
46
49
  const sidNeedle = `%"session_id":"${sessionId}"%`;
47
50
  const editDel = db.prepare('DELETE FROM session_file_edits WHERE session_id = ?').run(sessionId);
48
- const memDel = db.prepare('DELETE FROM memories WHERE source LIKE ?').run(sidNeedle);
51
+ const memDel = db.prepare(`DELETE FROM memories WHERE source LIKE ?
52
+ AND (NOT json_valid(content) OR COALESCE(json_extract(content, '$.distilled'), 0) != 1)`).run(sidNeedle);
49
53
  const evtDel = db.prepare('DELETE FROM events WHERE payload LIKE ?').run(sidNeedle);
50
54
  return { memories: memDel.changes, edits: editDel.changes, events: evtDel.changes };
51
55
  }
56
+ // A re-extracted memory must NOT be re-inserted if its source turn already has a surviving
57
+ // distilled rewrite (matched by turn_uuid — the stable key shared by raw and rewrite).
58
+ function makeDistilledTurnSet(db, sessionId) {
59
+ const rows = db.prepare(`SELECT source FROM memories
60
+ WHERE source LIKE ? AND json_valid(content) AND json_extract(content, '$.distilled') = 1`).all(`%"session_id":"${sessionId}"%`);
61
+ const set = new Set();
62
+ for (const r of rows) {
63
+ try {
64
+ const uuid = JSON.parse(r.source)?.turn_uuid;
65
+ if (uuid)
66
+ set.add(String(uuid));
67
+ }
68
+ catch { /* ignore malformed source */ }
69
+ }
70
+ return set;
71
+ }
52
72
  async function main() {
53
73
  const args = process.argv.slice(2);
54
74
  if (args.includes('-h') || args.includes('--help')) {
@@ -86,7 +106,10 @@ async function main() {
86
106
  if (!db)
87
107
  return;
88
108
  // Idempotent: wipe any prior data for THIS session before re-inserting
109
+ // (distilled rewrites survive — see wipeSession), then skip re-extracting
110
+ // any turn that already has a distilled rewrite.
89
111
  const wiped = wipeSession(db, result.session_id);
112
+ const distilledTurns = makeDistilledTurnSet(db, result.session_id);
90
113
  // Resolve project entity (canonical_key = project:<name>, so same project across
91
114
  // different sessions/cwds collapses to one entity)
92
115
  let projectEntityId;
@@ -102,14 +125,19 @@ async function main() {
102
125
  const ins = db.prepare('INSERT INTO entities (kind, name, normalized_name, canonical_key) VALUES (?, ?, ?, ?)').run('project', projectName, normalized, canonicalKey);
103
126
  projectEntityId = Number(ins.lastInsertRowid);
104
127
  }
105
- const insMem = db.prepare('INSERT INTO memories (entity_id, layer, content, importance, source, thread_id) VALUES (?, ?, ?, ?, ?, ?)');
128
+ // created_at = the source turn's true timestamp. The Stop hook re-imports the growing
129
+ // transcript every turn (wipe+reinsert), and DEFAULT unixepoch() restamped days-old
130
+ // content as "now" — inflating every time-windowed view ("today" showed last week).
131
+ const insMem = db.prepare('INSERT INTO memories (entity_id, layer, content, importance, source, thread_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)');
106
132
  const insEdit = db.prepare(`INSERT INTO session_file_edits (session_id, memory_id, file_path, operation, turn_uuid, context_snippet, occurred_at) VALUES (?, ?, ?, ?, ?, ?, ?)`);
107
133
  const insEvt = db.prepare('INSERT INTO events (entity_id, kind, payload, occurred_at) VALUES (?, ?, ?, ?)');
108
134
  let inserted = { memories: 0, edits: 0 };
109
135
  db.transaction(() => {
110
136
  const memContentToId = new Map();
111
137
  for (const m of result.memories) {
112
- const res = insMem.run(projectEntityId, m.layer, m.content, m.importance, JSON.stringify(m.source), m.thread_id ?? null);
138
+ if (m.source.turn_uuid && distilledTurns.has(m.source.turn_uuid))
139
+ continue; // distilled rewrite wins
140
+ const res = insMem.run(projectEntityId, m.layer, m.content, m.importance, JSON.stringify(m.source), m.thread_id ?? null, m.occurred_at ?? Math.floor(Date.now() / 1000));
113
141
  memContentToId.set(m.content, Number(res.lastInsertRowid));
114
142
  inserted.memories++;
115
143
  }
@@ -221,17 +249,22 @@ async function main() {
221
249
  const ins = db.prepare('INSERT INTO entities (kind, name, normalized_name, canonical_key) VALUES (?, ?, ?, ?)').run('project', projectName, normalized, canonicalKey);
222
250
  projectEntityId = Number(ins.lastInsertRowid);
223
251
  }
224
- // Idempotent: wipe any prior data for THIS session before re-inserting (Phase B)
252
+ // Idempotent: wipe any prior data for THIS session before re-inserting (Phase B).
253
+ // Distilled rewrites survive the wipe and suppress re-insertion of their raw turn.
225
254
  wipeSession(db, result.session_id);
226
- // Insert memories + file_edits in a single transaction per session
227
- const insMem = db.prepare('INSERT INTO memories (entity_id, layer, content, importance, source, thread_id) VALUES (?, ?, ?, ?, ?, ?)');
255
+ const distilledTurns = makeDistilledTurnSet(db, result.session_id);
256
+ // Insert memories + file_edits in a single transaction per session.
257
+ // created_at = source-turn timestamp (see single-file mode above for why).
258
+ const insMem = db.prepare('INSERT INTO memories (entity_id, layer, content, importance, source, thread_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)');
228
259
  const insEdit = db.prepare(`INSERT INTO session_file_edits (session_id, memory_id, file_path, operation, turn_uuid, context_snippet, occurred_at) VALUES (?, ?, ?, ?, ?, ?, ?)`);
229
260
  const insEvt = db.prepare('INSERT INTO events (entity_id, kind, payload, occurred_at) VALUES (?, ?, ?, ?)');
230
261
  const tx = db.transaction(() => {
231
262
  const memContentToId = new Map();
232
263
  for (const m of result.memories) {
264
+ if (m.source.turn_uuid && distilledTurns.has(m.source.turn_uuid))
265
+ continue; // distilled rewrite wins
233
266
  const srcJson = JSON.stringify(m.source);
234
- const res = insMem.run(projectEntityId, m.layer, m.content, m.importance, srcJson, m.thread_id ?? null);
267
+ const res = insMem.run(projectEntityId, m.layer, m.content, m.importance, srcJson, m.thread_id ?? null, m.occurred_at ?? Math.floor(Date.now() / 1000));
235
268
  memContentToId.set(m.content, Number(res.lastInsertRowid));
236
269
  agg.memories_inserted++;
237
270
  }
@@ -21,20 +21,20 @@ const force = args.includes('--force') || args.includes('-f');
21
21
  const dryRun = args.includes('--dry-run');
22
22
  const showHelp = args.includes('--help') || args.includes('-h');
23
23
  if (showHelp) {
24
- console.log(`linksee-memory-install-skill
25
-
26
- Install the linksee-memory Claude Code skill into ~/.claude/skills/linksee-memory/.
27
-
28
- Options:
29
- --force, -f Overwrite an existing skill file
30
- --dry-run Show what would happen without writing
31
- --help, -h This message
32
-
33
- After installation, ensure the MCP server is registered in Claude Code:
34
- claude mcp add -s user linksee -- npx -y linksee-memory
35
-
36
- The skill expects tool names of the form mcp__linksee__*. If you register the
37
- server under a different name (e.g. "linksee-memory"), edit the skill file
24
+ console.log(`linksee-memory-install-skill
25
+
26
+ Install the linksee-memory Claude Code skill into ~/.claude/skills/linksee-memory/.
27
+
28
+ Options:
29
+ --force, -f Overwrite an existing skill file
30
+ --dry-run Show what would happen without writing
31
+ --help, -h This message
32
+
33
+ After installation, ensure the MCP server is registered in Claude Code:
34
+ claude mcp add -s user linksee -- npx -y linksee-memory
35
+
36
+ The skill expects tool names of the form mcp__linksee__*. If you register the
37
+ server under a different name (e.g. "linksee-memory"), edit the skill file
38
38
  afterwards.`);
39
39
  process.exit(0);
40
40
  }