linksee-memory 0.16.1 → 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 +97 -183
- package/dist/bin/guard-hook.js +19 -4
- package/dist/bin/setup.js +178 -63
- package/dist/bin/share-draft.d.ts +2 -0
- package/dist/bin/share-draft.js +90 -0
- package/dist/bin/sync-session.js +10 -1
- package/dist/db/migrate.d.ts +30 -0
- package/dist/db/migrate.js +150 -9
- package/dist/lib/consolidate.d.ts +9 -0
- package/dist/lib/consolidate.js +14 -0
- 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/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/lib/share-draft.d.ts +56 -0
- package/dist/lib/share-draft.js +234 -0
- package/dist/mcp/prompts.js +1 -1
- package/dist/mcp/server.js +93 -26
- package/dist/skill/SKILL.md +6 -14
- package/package.json +4 -3
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// share_draft — build a link to KanseiLINK's reviewed draft page for an agent field note.
|
|
2
|
+
//
|
|
3
|
+
// What this does: checks a fixed set of fields, and returns a URL. Nothing else.
|
|
4
|
+
// - Linksee itself makes NO network call here. The human opens the link, reads the
|
|
5
|
+
// filled form, and only their click on that page sends anything.
|
|
6
|
+
// - No free text beyond the fixed fields. Each field has a length cap, and any field that
|
|
7
|
+
// looks like it carries a secret stops the build — we return which field and why,
|
|
8
|
+
// never a link. (The page masks again on display; we refuse earlier because the URL
|
|
9
|
+
// itself passes through the page host's request logs.)
|
|
10
|
+
// - Fields are filled by the agent from what the user wants to share. This module never
|
|
11
|
+
// reads memory to fill them.
|
|
12
|
+
//
|
|
13
|
+
// Target page: https://kansei-link.com/agents/note/new.html — it reads exactly these query
|
|
14
|
+
// keys: service, task, symptom, tried, result, conditions, kind, runtime (and trims the query
|
|
15
|
+
// to 2,000 characters).
|
|
16
|
+
export const DRAFT_PAGE = 'https://kansei-link.com/agents/note/new.html';
|
|
17
|
+
export const URL_MAX = 2000;
|
|
18
|
+
export const KINDS = ['execution', 'documentation', 'hypothesis'];
|
|
19
|
+
export const RUNTIMES = ['claude', 'codex', 'gpt', 'gemini', 'other'];
|
|
20
|
+
/** Text fields in URL order, with the page's caps (same numbers as the page's maxlength). */
|
|
21
|
+
export const TEXT_FIELDS = [
|
|
22
|
+
{ key: 'task', max: 300, required: true },
|
|
23
|
+
{ key: 'symptom', max: 600, required: true },
|
|
24
|
+
{ key: 'tried', max: 2000, required: true },
|
|
25
|
+
{ key: 'result', max: 1000, required: true },
|
|
26
|
+
{ key: 'conditions', max: 600, required: false },
|
|
27
|
+
];
|
|
28
|
+
export const ALLOWED_KEYS = ['service', ...TEXT_FIELDS.map((f) => f.key), 'kind', 'runtime'];
|
|
29
|
+
export const SERVICE_RE = /^[a-z0-9_.-]{1,160}$/;
|
|
30
|
+
export const NOTICE = [
|
|
31
|
+
'この内容は、開いた画面で確認してから送られます。送るまでどこにも出ていません。',
|
|
32
|
+
'URL の中身は KanseiLINK の通信の記録(Cloudflare)を通ります。秘密は入れないでください。',
|
|
33
|
+
];
|
|
34
|
+
export const NOTICE_EN = [
|
|
35
|
+
'Nothing is sent yet: the page opens with these fields filled, and only your click there sends them.',
|
|
36
|
+
'The URL passes through KanseiLINK\'s request logs (Cloudflare). Do not put secrets in it.',
|
|
37
|
+
];
|
|
38
|
+
// ── Secret shapes ──────────────────────────────────────────────────────────────
|
|
39
|
+
// Ported from KanseiLINK `src/utils/secret-mask.ts` (kansei-link/kansei-internal @ f39362a,
|
|
40
|
+
// file last changed in 5cdd578), copied 2026-10-30. Same TENANT_HOSTS list as the draft page
|
|
41
|
+
// (public/agents/note/new.html, which is itself a port of secret-mask.ts). Keep in sync.
|
|
42
|
+
//
|
|
43
|
+
// Differences, because here a match BLOCKS instead of masking:
|
|
44
|
+
// - Added: IP addresses, local paths (C:\, /Users/, /home/), bare tenant hosts without a scheme.
|
|
45
|
+
// - Long base64-ish blobs from 32 chars (the mask uses 40), but only when the run mixes letters
|
|
46
|
+
// and digits — so long error codes like PERMISSION_DENIED_INSUFFICIENT_SCOPES pass.
|
|
47
|
+
// - Bearer/Basic/Token needs a digit in the value (or 20+ chars), so "token authentication"
|
|
48
|
+
// in prose passes; a real credential nearly always has one.
|
|
49
|
+
// - A URL is blocked only when its host is a tenant of a TENANT_HOSTS service; other URLs
|
|
50
|
+
// (public docs) pass.
|
|
51
|
+
const TENANT_HOSTS = [
|
|
52
|
+
'slack.com', 'atlassian.net', 'kintone.com', 'cybozu.com', 'zendesk.com', 'freshdesk.com',
|
|
53
|
+
'myshopify.com', 'salesforce.com', 'my.salesforce.com', 'lightning.force.com', 'okta.com',
|
|
54
|
+
'auth0.com', 'hubspot.com', 'notion.site', 'sharepoint.com', 'onmicrosoft.com', 'zoom.us',
|
|
55
|
+
'monday.com', 'asana.com', 'intercom.io', 'chatwork.com', 'smarthr.jp', 'secure.freee.co.jp',
|
|
56
|
+
'backlog.com', 'backlog.jp', 'box.com', 'github.io', 'supabase.co', 'vercel.app', 'railway.app',
|
|
57
|
+
'herokuapp.com', 'workers.dev', 'pages.dev',
|
|
58
|
+
];
|
|
59
|
+
const GENERIC_LABELS = new Set(['www', 'api', 'app', 'docs', 'developer', 'developers', 'accounts', 'login', 'auth', 'secure', 'status', 'help', 'support', 'cdn', 'mail']);
|
|
60
|
+
function tenantOf(host) {
|
|
61
|
+
const h = host.toLowerCase().replace(/:\d+$/, '').replace(/\.$/, '');
|
|
62
|
+
for (const base of TENANT_HOSTS) {
|
|
63
|
+
if (h.endsWith('.' + base) && h.length > base.length + 1) {
|
|
64
|
+
const first = h.slice(0, h.length - base.length - 1).split('.').pop() ?? '';
|
|
65
|
+
if (GENERIC_LABELS.has(first))
|
|
66
|
+
continue; // api.slack.com is the vendor, not a tenant
|
|
67
|
+
return base;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const has = (re) => (s) => { re.lastIndex = 0; return re.test(s); };
|
|
73
|
+
const mixed = (t) => /[A-Za-z]/.test(t) && /\d/.test(t);
|
|
74
|
+
const FINDERS = [
|
|
75
|
+
{
|
|
76
|
+
kind: 'BEARER', ja: '認証ヘッダーの値(Bearer/Basic/Token …)',
|
|
77
|
+
find: (s) => {
|
|
78
|
+
const re = /\b(?:Bearer|Basic|Token)\s+([A-Za-z0-9\-._~+/=]{8,})/gi;
|
|
79
|
+
for (const m of s.matchAll(re))
|
|
80
|
+
if (/\d/.test(m[1]) || m[1].length >= 20)
|
|
81
|
+
return true;
|
|
82
|
+
return false;
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
kind: 'API_KEY', ja: 'API キー・トークンの形(sk-・ghp_・github_pat_・xox?-・AKIA・AIza など)',
|
|
87
|
+
find: has(/\b(?:sk-ant-[A-Za-z0-9\-_]{8,}|sk-[A-Za-z0-9\-_]{8,}|(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{8,}|github_pat_[A-Za-z0-9_]{8,}|xox[abprs]-[A-Za-z0-9\-]{8,}|(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{8,}|AKIA[A-Z0-9]{12,}|AIza[A-Za-z0-9\-_]{20,})/g),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
kind: 'KV_SECRET', ja: '秘密の値の書き方(token=…・password: … など)',
|
|
91
|
+
find: has(/\b(access_token|refresh_token|id_token|api[_-]?key|apikey|client_secret|secret|password|passwd|token|signature|sig)(["']?\s*[=:]\s*)(?:"([^"\r\n]+)"|'([^'\r\n]+)'|[A-Za-z0-9\-._~+/%]{6,})/gi),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
kind: 'JWT', ja: 'JWT の形(3 つの塊を . でつないだもの)',
|
|
95
|
+
find: has(/\b[A-Za-z0-9\-_]{10,}\.[A-Za-z0-9\-_]{10,}\.[A-Za-z0-9\-_]{10,}\b/g),
|
|
96
|
+
},
|
|
97
|
+
{ kind: 'EMAIL', ja: 'メールアドレス', find: has(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g) },
|
|
98
|
+
{
|
|
99
|
+
kind: 'TENANT_URL', ja: '会社ごとの URL・ホスト名(acme.slack.com など)',
|
|
100
|
+
find: (s) => {
|
|
101
|
+
for (const m of s.matchAll(/(?:\b[a-z][a-z0-9+.-]*:\/\/)?((?:[a-z0-9-]+\.)+[a-z]{2,})(?::\d+)?/gi)) {
|
|
102
|
+
if (tenantOf(m[1]))
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
kind: 'IP', ja: 'IP アドレス(localhost と書いてください)',
|
|
110
|
+
find: (s) => {
|
|
111
|
+
for (const m of s.matchAll(/(?<![\d.])(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?![\d.]*\d)/g)) {
|
|
112
|
+
if (m.slice(1, 5).every((o) => Number(o) <= 255))
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return has(/(?<![0-9a-f:])(?:(?:[0-9a-f]{1,4}:){7}[0-9a-f]{1,4}|(?:[0-9a-f]{1,4}:){1,6}:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4}|::1)(?![0-9a-f:])/gi)(s);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
kind: 'LOCAL_PATH', ja: 'PC の中の道(C:\\・/Users/・/home/)',
|
|
120
|
+
find: has(/(?<![A-Za-z])[A-Za-z]:[\\/]|\/(?:Users|home)\/|\\Users\\/g),
|
|
121
|
+
},
|
|
122
|
+
{ kind: 'UUID', ja: 'UUID(要求・テナントの id)', find: has(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi) },
|
|
123
|
+
{ kind: 'HEX', ja: '長い 16 進の塊(32 字以上)', find: has(/\b[0-9a-f]{32,}\b/gi) },
|
|
124
|
+
{
|
|
125
|
+
kind: 'BLOB', ja: '長い base64 などの塊(32 字以上・英字と数字が混ざったもの)',
|
|
126
|
+
find: (s) => {
|
|
127
|
+
for (const m of s.matchAll(/[A-Za-z0-9+=_\-]{32,}/g))
|
|
128
|
+
if (mixed(m[0]))
|
|
129
|
+
return true;
|
|
130
|
+
return false;
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
/** Which secret shapes a text contains (kinds, in check order). Empty = clean. */
|
|
135
|
+
export function findSecretShapes(text) {
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const f of FINDERS)
|
|
138
|
+
if (f.find(text))
|
|
139
|
+
out.push({ kind: f.kind, ja: f.ja });
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Check the fields and build the draft URL. Pure: no I/O, no network, no DB.
|
|
144
|
+
* Returns problems (field + reason) instead of a URL when anything is off.
|
|
145
|
+
*/
|
|
146
|
+
export function buildShareDraft(input) {
|
|
147
|
+
const problems = [];
|
|
148
|
+
const args = (input && typeof input === 'object' && !Array.isArray(input)) ? input : {};
|
|
149
|
+
for (const k of Object.keys(args)) {
|
|
150
|
+
if (!ALLOWED_KEYS.includes(k)) {
|
|
151
|
+
problems.push({ field: k, reason: `この欄は受けません。使える欄: ${ALLOWED_KEYS.join(', ')}` });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const str = (k) => {
|
|
155
|
+
const v = args[k];
|
|
156
|
+
if (v === undefined || v === null)
|
|
157
|
+
return undefined;
|
|
158
|
+
if (typeof v !== 'string') {
|
|
159
|
+
problems.push({ field: k, reason: '文字列で渡してください' });
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
return v.trim();
|
|
163
|
+
};
|
|
164
|
+
const service = str('service');
|
|
165
|
+
if (!service)
|
|
166
|
+
problems.push({ field: 'service', reason: '必須です(KanseiLINK の service_id、例: freee, slack)' });
|
|
167
|
+
else if (!SERVICE_RE.test(service))
|
|
168
|
+
problems.push({ field: 'service', reason: '形が違います。小文字・数字・_ . - だけで 160 字まで(例: freee, google-drive)' });
|
|
169
|
+
const values = {};
|
|
170
|
+
for (const f of TEXT_FIELDS) {
|
|
171
|
+
const v = str(f.key);
|
|
172
|
+
if (!v) {
|
|
173
|
+
if (f.required)
|
|
174
|
+
problems.push({ field: f.key, reason: '必須です' });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (v.length > f.max) {
|
|
178
|
+
problems.push({ field: f.key, reason: `長すぎます(${v.length} 字・上限 ${f.max} 字)` });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
values[f.key] = v;
|
|
182
|
+
}
|
|
183
|
+
const kind = str('kind');
|
|
184
|
+
if (!kind)
|
|
185
|
+
problems.push({ field: 'kind', reason: `必須です: ${KINDS.join(' / ')}(実際に動かした/文書で読んだ/試していない推測)` });
|
|
186
|
+
else if (!KINDS.includes(kind))
|
|
187
|
+
problems.push({ field: 'kind', reason: `次のどれかにしてください: ${KINDS.join(' / ')}` });
|
|
188
|
+
const runtime = str('runtime');
|
|
189
|
+
if (runtime !== undefined && runtime !== '' && !RUNTIMES.includes(runtime)) {
|
|
190
|
+
problems.push({ field: 'runtime', reason: `次のどれかにしてください: ${RUNTIMES.join(' / ')}` });
|
|
191
|
+
}
|
|
192
|
+
// Secret shapes — every field, including service.
|
|
193
|
+
const scan = [['service', service], ...Object.entries(values)];
|
|
194
|
+
for (const [field, v] of scan) {
|
|
195
|
+
if (!v)
|
|
196
|
+
continue;
|
|
197
|
+
const hits = findSecretShapes(v);
|
|
198
|
+
if (hits.length) {
|
|
199
|
+
problems.push({
|
|
200
|
+
field,
|
|
201
|
+
reason: `秘密に見える形があります: ${hits.map((h) => h.ja).join('・')}。値は書かず、種類だけを書いてください(例: 「401 invalid_token」「テナントの URL」)`,
|
|
202
|
+
kinds: hits.map((h) => h.kind),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (problems.length) {
|
|
207
|
+
return { ok: false, problems, hint: 'リンクは作っていません。上の欄を直して、もう一度呼んでください。' };
|
|
208
|
+
}
|
|
209
|
+
const q = new URLSearchParams();
|
|
210
|
+
q.set('service', service);
|
|
211
|
+
for (const f of TEXT_FIELDS)
|
|
212
|
+
if (values[f.key])
|
|
213
|
+
q.set(f.key, values[f.key]);
|
|
214
|
+
q.set('kind', kind);
|
|
215
|
+
if (runtime)
|
|
216
|
+
q.set('runtime', runtime);
|
|
217
|
+
const url = `${DRAFT_PAGE}?${q.toString()}`;
|
|
218
|
+
if (url.length > URL_MAX) {
|
|
219
|
+
const sizes = TEXT_FIELDS
|
|
220
|
+
.filter((f) => values[f.key])
|
|
221
|
+
.map((f) => ({ field: f.key, encoded: encodeURIComponent(values[f.key]).length }))
|
|
222
|
+
.sort((a, b) => b.encoded - a.encoded);
|
|
223
|
+
return {
|
|
224
|
+
ok: false,
|
|
225
|
+
problems: [{
|
|
226
|
+
field: sizes[0]?.field ?? 'tried',
|
|
227
|
+
reason: `リンクが長すぎます(${url.length} 字・上限 ${URL_MAX} 字)。短くしてください。日本語は 1 字が URL で 9 字になります。長い順: ${sizes.map((s) => `${s.field} ${s.encoded}`).join('・')}`,
|
|
228
|
+
}],
|
|
229
|
+
hint: 'リンクは作っていません。こちらでは欄を削りません。短くして、もう一度呼んでください。',
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return { ok: true, url, length: url.length, notice: NOTICE, notice_en: NOTICE_EN };
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=share-draft.js.map
|
package/dist/mcp/prompts.js
CHANGED
|
@@ -27,7 +27,7 @@ export const PROMPTS = [
|
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
name: 'weekly-consolidation',
|
|
30
|
-
description: 'Sleep-mode summary of the past week\'s memories for an entity. Produces a single learning-layer entry that captures the trajectory. Use as input
|
|
30
|
+
description: 'Sleep-mode summary of the past week\'s memories for an entity. Produces a single learning-layer entry that captures the trajectory. Use as input when distilling learnings, or to write a Friday digest.',
|
|
31
31
|
arguments: [
|
|
32
32
|
{ name: 'entity_name', description: 'The entity to consolidate.', required: true },
|
|
33
33
|
{ name: 'week_offset', description: 'Weeks-ago offset (0 = this week, 1 = last week). Default 0.', required: false },
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// linksee-memory MCP server (stdio transport).
|
|
3
|
-
// Tools: remember / recall / read_smart / drift_status / check_decision / declare_anchor / resolve_drift / flag_proposals / dream / resolve_proposal
|
|
3
|
+
// Tools: remember / recall / read_smart / drift_status / check_decision / declare_anchor / resolve_drift / flag_proposals / dream / resolve_proposal / share_draft
|
|
4
4
|
// v0.10.0 — Re-injection layer: pre-action guard (PreToolUse/SessionStart hooks) + extraction
|
|
5
5
|
// quality gate + distillation routine (dream distill_queue) + harden/soften escalation
|
|
6
6
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
-
import { openDb, runMigrations } from '../db/migrate.js';
|
|
9
|
+
import { openDb, runMigrations, DbRefusedError } from '../db/migrate.js';
|
|
10
10
|
import { computeHeat } from '../lib/heat-index.js';
|
|
11
11
|
import { decideForgetting } from '../lib/forgetting.js';
|
|
12
12
|
import { refreshMomentumForEntity } from '../lib/momentum.js';
|
|
13
|
-
import { consolidate as runConsolidate } from '../lib/consolidate.js';
|
|
13
|
+
import { consolidate as runConsolidate, autoConsolidateEnabled } from '../lib/consolidate.js';
|
|
14
14
|
import { isPastedExternalContent } from '../lib/session-parser.js';
|
|
15
15
|
import { inferAltitude, inferType, inferState } from '../lib/session-extractor.js';
|
|
16
16
|
import { normalizeEntityName } from '../lib/normalize.js';
|
|
@@ -26,6 +26,7 @@ import { getReinjectionFriction, setGateMode } from '../lib/guard.js';
|
|
|
26
26
|
import { logAnchorTouch } from '../lib/anchor-touch.js';
|
|
27
27
|
import { triageDistillQueue, distillPending } from '../lib/distill-triage.js';
|
|
28
28
|
import { whereAmI, listMapProjects } from '../lib/map-view.js';
|
|
29
|
+
import { buildShareDraft, KINDS, RUNTIMES } from '../lib/share-draft.js';
|
|
29
30
|
import { readFileSync, existsSync } from 'node:fs';
|
|
30
31
|
import { fileURLToPath } from 'node:url';
|
|
31
32
|
import { dirname, join } from 'node:path';
|
|
@@ -62,6 +63,7 @@ const SUBCOMMANDS = {
|
|
|
62
63
|
declare: 'declare-anchor.js',
|
|
63
64
|
detect: 'detect-drift.js',
|
|
64
65
|
export: 'export-report.js',
|
|
66
|
+
'share-draft': 'share-draft.js',
|
|
65
67
|
};
|
|
66
68
|
const subcommand = process.argv[2];
|
|
67
69
|
if (subcommand && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, subcommand)) {
|
|
@@ -70,8 +72,20 @@ if (subcommand && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, subcommand))
|
|
|
70
72
|
const r = spawnSync(process.execPath, [binPath, ...process.argv.slice(3)], { stdio: 'inherit' });
|
|
71
73
|
process.exit(r.status ?? (r.signal ? 1 : 0));
|
|
72
74
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
let db;
|
|
76
|
+
try {
|
|
77
|
+
db = openDb();
|
|
78
|
+
runMigrations(db);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
// A pre-June-2026 DB (schema < 6) is refused before any write; print what to do
|
|
82
|
+
// instead of a stack trace. MCP clients show the server's stderr in their logs.
|
|
83
|
+
if (e instanceof DbRefusedError) {
|
|
84
|
+
process.stderr.write(`${e.message}\n`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
75
89
|
// Distill triage runs on EVERY start, before the first request. It is one indexed scan and a
|
|
76
90
|
// handful of JSON rewrites — cheap — and 0.16.0 had tucked it inside the auto-consolidate block
|
|
77
91
|
// below, which runs at most once a week and only after a 3-second timer. So it never ran.
|
|
@@ -82,29 +96,37 @@ try {
|
|
|
82
96
|
}
|
|
83
97
|
}
|
|
84
98
|
catch { /* never block startup */ }
|
|
85
|
-
// Auto-maintenance: consolidate stale memories on startup (non-blocking)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
99
|
+
// Auto-maintenance: consolidate stale memories on startup (non-blocking).
|
|
100
|
+
//
|
|
101
|
+
// OFF by default since 0.17.1 (L-1-106 / audit F1): consolidate deletes the original rows after
|
|
102
|
+
// writing a summary that keeps only the top 3 × 200 characters, ignores thread_id, and cascades
|
|
103
|
+
// away their edges — reasons and history lost with no action from the user. It now runs only
|
|
104
|
+
// when asked: LINKSEE_AUTO_CONSOLIDATE=1 (or `--auto-consolidate` on the server command line).
|
|
105
|
+
// There is no `consolidate` tool (merged away in 0.7.0). Rows already removed by earlier versions
|
|
106
|
+
// are not restored.
|
|
107
|
+
if (autoConsolidateEnabled(process.env, process.argv.slice(2)))
|
|
108
|
+
setTimeout(() => {
|
|
89
109
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
|
|
110
|
+
let shouldRun = true;
|
|
111
|
+
try {
|
|
112
|
+
// Check if consolidations table exists before querying
|
|
113
|
+
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='consolidations'").get();
|
|
114
|
+
if (tableExists) {
|
|
115
|
+
const lastRow = db.prepare('SELECT MAX(created_at) as ts FROM consolidations').get();
|
|
116
|
+
if (lastRow?.ts && (Date.now() / 1000 - lastRow.ts) / 86400 < 7)
|
|
117
|
+
shouldRun = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
shouldRun = false; /* genuinely unexpected — skip to be safe */
|
|
122
|
+
}
|
|
123
|
+
if (shouldRun) {
|
|
124
|
+
runConsolidate(db, { scope: 'all', min_age_days: 7 });
|
|
125
|
+
process.stderr.write('[linksee-memory] auto-consolidate complete\n');
|
|
96
126
|
}
|
|
97
127
|
}
|
|
98
|
-
catch {
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
if (shouldRun) {
|
|
102
|
-
runConsolidate(db, { scope: 'all', min_age_days: 7 });
|
|
103
|
-
process.stderr.write('[linksee-memory] auto-consolidate complete\n');
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
catch { /* non-fatal */ }
|
|
107
|
-
}, 3000);
|
|
128
|
+
catch { /* non-fatal */ }
|
|
129
|
+
}, 3000);
|
|
108
130
|
const server = new Server({ name: 'linksee-memory', version: SERVER_VERSION }, {
|
|
109
131
|
capabilities: {
|
|
110
132
|
tools: {},
|
|
@@ -388,6 +410,35 @@ const TOOLS = [
|
|
|
388
410
|
required: ['candidate_id', 'verdict', 'rationale'],
|
|
389
411
|
},
|
|
390
412
|
},
|
|
413
|
+
// ── Share a fix (opt-in, link only) ──────────────────────
|
|
414
|
+
{
|
|
415
|
+
name: 'share_draft',
|
|
416
|
+
description: 'Make a LINK to a reviewed draft of a field note for other agents (KanseiLINK note page). ' +
|
|
417
|
+
'ONLY when the user says they want to share — never call it on your own.\n\n' +
|
|
418
|
+
'Use after a SaaS / MCP / API connection got stuck and was fixed (or was not). You write the fields; ' +
|
|
419
|
+
'this tool only checks them and builds the URL. Linksee sends nothing: the user opens the link, ' +
|
|
420
|
+
'reviews the filled form, and only their click on that page sends it.\n\n' +
|
|
421
|
+
'No values, only kinds: write "401 invalid_token", not the token. A field that looks like a key, ' +
|
|
422
|
+
'token, JWT, e-mail, tenant URL (acme.slack.com), IP, local path (C:\, /Users/, /home/) or a long ' +
|
|
423
|
+
'hex/base64 run stops the build and returns which field. The whole URL must fit in 2,000 characters ' +
|
|
424
|
+
'(a Japanese character takes 9) — if not, it asks you to shorten; it does not cut.\n\n' +
|
|
425
|
+
'Returns { url, notice } — show the user both. Linksee records only that a draft was made (no field text).',
|
|
426
|
+
inputSchema: {
|
|
427
|
+
type: 'object',
|
|
428
|
+
properties: {
|
|
429
|
+
service: { type: 'string', description: 'KanseiLINK service_id, lowercase (e.g. freee, slack, google-drive). ^[a-z0-9_.-]{1,160}$' },
|
|
430
|
+
task: { type: 'string', maxLength: 300, description: 'What you were trying to do (≤300 chars)' },
|
|
431
|
+
symptom: { type: 'string', maxLength: 600, description: 'The KIND of error that came back — no values (≤600 chars)' },
|
|
432
|
+
tried: { type: 'string', maxLength: 2000, description: 'The steps you tried (≤2,000 chars)' },
|
|
433
|
+
result: { type: 'string', maxLength: 1000, description: 'How it was solved, or that it was not (≤1,000 chars)' },
|
|
434
|
+
conditions: { type: 'string', maxLength: 600, description: 'Optional. Tool + version, kind of environment (≤600 chars)' },
|
|
435
|
+
kind: { type: 'string', enum: [...KINDS], description: 'execution = you ran it · documentation = you read it · hypothesis = untested guess' },
|
|
436
|
+
runtime: { type: 'string', enum: [...RUNTIMES], description: 'Optional. Which AI wrote this' },
|
|
437
|
+
},
|
|
438
|
+
required: ['service', 'task', 'symptom', 'tried', 'result', 'kind'],
|
|
439
|
+
additionalProperties: false,
|
|
440
|
+
},
|
|
441
|
+
},
|
|
391
442
|
];
|
|
392
443
|
// ============================================================
|
|
393
444
|
// Handlers
|
|
@@ -2097,6 +2148,18 @@ const LEGACY_TOOLS = {
|
|
|
2097
2148
|
dream: 'recall({ dream: true }) — recall() with no arguments already includes the counts',
|
|
2098
2149
|
resolve_proposal: "resolve_drift({ candidate_id, action: 'surface' | 'dismiss', rationale })",
|
|
2099
2150
|
};
|
|
2151
|
+
// share_draft: check fields + build the link. No network. Records only that a draft was made.
|
|
2152
|
+
function handleShareDraft(args) {
|
|
2153
|
+
const r = buildShareDraft(args);
|
|
2154
|
+
if (!r.ok)
|
|
2155
|
+
return JSON.stringify(r, null, 2);
|
|
2156
|
+
try {
|
|
2157
|
+
// events row with no entity and no payload: the fact and the time, nothing from the fields.
|
|
2158
|
+
db.prepare('INSERT INTO events (entity_id, kind, payload) VALUES (NULL, ?, NULL)').run('share_draft_created');
|
|
2159
|
+
}
|
|
2160
|
+
catch { /* the record must never break the tool */ }
|
|
2161
|
+
return JSON.stringify(r, null, 2);
|
|
2162
|
+
}
|
|
2100
2163
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2101
2164
|
const showLegacy = process.env.LINKSEE_LEGACY_TOOLS === '1';
|
|
2102
2165
|
const tools = TOOLS
|
|
@@ -2146,13 +2209,17 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2146
2209
|
case 'resolve_proposal':
|
|
2147
2210
|
text = handleResolveProposal(args);
|
|
2148
2211
|
break;
|
|
2212
|
+
// Share a fix (link only)
|
|
2213
|
+
case 'share_draft':
|
|
2214
|
+
text = handleShareDraft(args);
|
|
2215
|
+
break;
|
|
2149
2216
|
default: {
|
|
2150
2217
|
const migrations = {
|
|
2151
2218
|
update_memory: 'remember({ memory_id: <id>, content: "...", importance: 0.8 })',
|
|
2152
2219
|
forget: 'remember({ forget: true, memory_id: <id> })',
|
|
2153
2220
|
list_entities: 'recall() with no params',
|
|
2154
2221
|
recall_file: 'recall({ path: "<file_path>" })',
|
|
2155
|
-
consolidate: '
|
|
2222
|
+
consolidate: 'No longer a tool, and since 0.17.1 it does not run on server start either (it deletes the original rows). Opt in on the server with LINKSEE_AUTO_CONSOLIDATE=1 if you want the old weekly start-up run.',
|
|
2156
2223
|
};
|
|
2157
2224
|
if (name in migrations) {
|
|
2158
2225
|
throw new Error(`Tool "${name}" was merged in v0.7.0. Migration: ${migrations[name]}`);
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -372,26 +372,19 @@ Recording `prior_belief` leaves a **belief-update history**. Later, this becomes
|
|
|
372
372
|
2. ❌ Solve an error on the spot without recording — future you (or another agent) will hit the same failure
|
|
373
373
|
3. ❌ Use `Read` everywhere instead of `read_smart` (wastes tokens)
|
|
374
374
|
4. ❌ Write caveats in a flippant tone — preserve them seriously
|
|
375
|
-
5. ❌
|
|
375
|
+
5. ❌ Try to call a `consolidate` tool — there is none; consolidation is a server opt-in that deletes originals (see below)
|
|
376
376
|
6. ❌ Recall the same entity twice in one session without new context (wastes tokens)
|
|
377
377
|
7. ❌ Write `recall({ query: "what happened last time" })` — use specific keywords
|
|
378
378
|
|
|
379
379
|
---
|
|
380
380
|
|
|
381
|
-
## 🔁
|
|
381
|
+
## 🔁 Consolidation — off unless the user turns it on
|
|
382
382
|
|
|
383
|
-
|
|
383
|
+
There is no `consolidate` tool. Consolidation clusters cold, low-importance memories older than 7 days into one `learning`-layer entry and **deletes the originals**, so since 0.17.1 it never runs on its own. The user can opt in by starting the server with `LINKSEE_AUTO_CONSOLIDATE=1` (then it runs at start-up, at most weekly).
|
|
384
384
|
|
|
385
|
-
|
|
386
|
-
mcp__linksee__consolidate({
|
|
387
|
-
scope: "session",
|
|
388
|
-
min_age_days: 7
|
|
389
|
-
})
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
This clusters cold, low-importance memories older than 7 days → compresses them into a single `learning`-layer entry → deletes originals.
|
|
385
|
+
Do not suggest turning it on as routine housekeeping. If the user asks about a large DB, explain the trade-off (originals are deleted) and suggest backing up `~/.linksee-memory/` first.
|
|
393
386
|
|
|
394
|
-
**`caveat` memories and active `goal` memories are never consolidated away.**
|
|
387
|
+
**`caveat` memories and active `goal` memories are never consolidated away.**
|
|
395
388
|
|
|
396
389
|
---
|
|
397
390
|
|
|
@@ -525,7 +518,6 @@ User: "That's it for today"
|
|
|
525
518
|
})
|
|
526
519
|
|
|
527
520
|
3. Report: "Recorded N decisions, M caveats. Retrievable via recall."
|
|
528
|
-
4. Optionally suggest: consolidate({scope:"session", min_age_days: 14})
|
|
529
521
|
```
|
|
530
522
|
|
|
531
523
|
### Case F2 — Flag orphaned proposals at session end
|
|
@@ -725,7 +717,7 @@ Now you have both bodies of knowledge before starting
|
|
|
725
717
|
| Day 1 | low (little data) | Stop hook is still collecting |
|
|
726
718
|
| Week 1 | medium | thousands of memories, FTS5 kicks in |
|
|
727
719
|
| Month 1 | high | heat_score stabilizes, important memories surface |
|
|
728
|
-
| Month 3+ | strongest |
|
|
720
|
+
| Month 3+ | strongest | learnings crystallized (distilled) |
|
|
729
721
|
|
|
730
722
|
**"Gets smarter with use"** — time is on your side. Today's record is read by tomorrow's you.
|
|
731
723
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linksee-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"mcpName": "io.github.michielinksee/linksee-memory",
|
|
5
5
|
"description": "Hand a project over — to your next session, to Cursor/Codex/Gemini, to your successor — with the reasons attached. Local-first memory MCP for coding agents: decisions re-injected before the agent acts, drift detection across every project you run. One SQLite file, nothing leaves your machine.",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"linksee-memory-detect": "dist/bin/detect-drift.js",
|
|
16
16
|
"linksee-memory-guard": "dist/bin/guard-hook.js",
|
|
17
17
|
"linksee-memory-map": "dist/bin/map-import.js",
|
|
18
|
-
"linksee-memory-export": "dist/bin/export-report.js"
|
|
18
|
+
"linksee-memory-export": "dist/bin/export-report.js",
|
|
19
|
+
"linksee-memory-share-draft": "dist/bin/share-draft.js"
|
|
19
20
|
},
|
|
20
21
|
"main": "./dist/mcp/server.js",
|
|
21
22
|
"files": [
|
|
@@ -72,7 +73,7 @@
|
|
|
72
73
|
},
|
|
73
74
|
"dependencies": {
|
|
74
75
|
"@babel/parser": "^7.25.0",
|
|
75
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
76
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
76
77
|
"better-sqlite3": "^12.9.0",
|
|
77
78
|
"sqlite-vec": "^0.1.6",
|
|
78
79
|
"yaml": "^2.9.0"
|