linksee-memory 0.2.0 → 0.4.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 +39 -13
- package/dist/bin/import-sessions.js +10 -4
- package/dist/db/migrate.js +98 -0
- package/dist/db/schema.sql +6 -4
- package/dist/lib/normalize.d.ts +25 -0
- package/dist/lib/normalize.js +43 -0
- package/dist/lib/session-extractor.js +164 -38
- package/dist/mcp/elicitation.d.ts +24 -0
- package/dist/mcp/elicitation.js +58 -0
- package/dist/mcp/prompts.d.ts +21 -0
- package/dist/mcp/prompts.js +201 -0
- package/dist/mcp/resources.d.ts +18 -0
- package/dist/mcp/resources.js +162 -0
- package/dist/mcp/roots.d.ts +10 -0
- package/dist/mcp/roots.js +64 -0
- package/dist/mcp/sampling.d.ts +16 -0
- package/dist/mcp/sampling.js +78 -0
- package/dist/mcp/server.js +273 -70
- package/dist/skill/SKILL.md +274 -17
- package/package.json +84 -71
|
@@ -3,6 +3,54 @@
|
|
|
3
3
|
// to its intent context. A memory like "edited server.ts" becomes
|
|
4
4
|
// "edited server.ts BECAUSE the user wanted the FTS5 + LIKE merge fix".
|
|
5
5
|
import { isMetaOrNoise, isAutomatedSession, isPastedExternalContent } from './session-parser.js';
|
|
6
|
+
const ALTITUDE_PATTERNS = [
|
|
7
|
+
[/mission|ミッション|ビジョン|vision|product\s+direction|事業方針/i, 'mission'],
|
|
8
|
+
[/strategy|戦略|方針|positioning|GTM|go.to.market|revenue|pricing|ICP|ターゲット|マーケ/i, 'strategy'],
|
|
9
|
+
[/architect|設計|schema|database|DB設計|migration|API\s+design|system\s+design|layer\s+model|アーキテクチャ/i, 'architecture'],
|
|
10
|
+
];
|
|
11
|
+
function inferAltitude(text) {
|
|
12
|
+
for (const [pattern, altitude] of ALTITUDE_PATTERNS) {
|
|
13
|
+
if (pattern.test(text))
|
|
14
|
+
return altitude;
|
|
15
|
+
}
|
|
16
|
+
return 'implementation';
|
|
17
|
+
}
|
|
18
|
+
/** Extract a concise title from raw text (first sentence or up to maxLen chars) */
|
|
19
|
+
function makeTitle(text, maxLen = 80) {
|
|
20
|
+
const cleaned = text.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
21
|
+
const firstSentence = cleaned.split(/[。!?!?\n]/)[0].trim();
|
|
22
|
+
if (firstSentence.length <= maxLen)
|
|
23
|
+
return firstSentence;
|
|
24
|
+
return firstSentence.slice(0, maxLen - 3) + '...';
|
|
25
|
+
}
|
|
26
|
+
/** Extract file paths from surrounding context */
|
|
27
|
+
function extractAffectedPaths(ops) {
|
|
28
|
+
const unique = new Set(ops.map((o) => o.path));
|
|
29
|
+
return Array.from(unique).slice(0, 10);
|
|
30
|
+
}
|
|
31
|
+
function buildStructuredContent(opts) {
|
|
32
|
+
const obj = {
|
|
33
|
+
title: opts.title,
|
|
34
|
+
altitude: opts.altitude,
|
|
35
|
+
type: opts.type,
|
|
36
|
+
state: opts.state,
|
|
37
|
+
what: opts.what,
|
|
38
|
+
};
|
|
39
|
+
if (opts.why)
|
|
40
|
+
obj.why = opts.why;
|
|
41
|
+
if (opts.affects && opts.affects.length > 0)
|
|
42
|
+
obj.affects = opts.affects;
|
|
43
|
+
if (opts.next_action !== undefined)
|
|
44
|
+
obj.next_action = opts.next_action;
|
|
45
|
+
if (opts.evidence_refs && opts.evidence_refs.length > 0)
|
|
46
|
+
obj.evidence_refs = opts.evidence_refs;
|
|
47
|
+
// Merge any extra fields (session_id, git_branch, etc.)
|
|
48
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
49
|
+
if (!(k in obj) && v !== undefined)
|
|
50
|
+
obj[k] = v;
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(obj, null, 2);
|
|
53
|
+
}
|
|
6
54
|
// ============================================================
|
|
7
55
|
// Intent detection — first non-noise user message in the session.
|
|
8
56
|
// ============================================================
|
|
@@ -50,7 +98,9 @@ const CAVEAT_PATTERNS = [
|
|
|
50
98
|
// sentence terminators 。!!、 / particles ね・よ / whitespace / ください
|
|
51
99
|
// Anything else (e.g. も = concessive「〜ないでも」, 止まる・いる・ほしい etc.)
|
|
52
100
|
// is treated as descriptive and excluded.
|
|
53
|
-
|
|
101
|
+
// 心配し・気にし・遠慮し are reassurance ("don't worry / don't mind / don't hesitate")
|
|
102
|
+
// — semantically opposite to a caveat. Lookbehind excludes them.
|
|
103
|
+
/気をつけて|注意して|[!!]注意[!!]|避けて(?!いる|いない)|(?<!心配|気に|遠慮)[ぁ-ん一-龯]ないで(?=[。!!、\s]|ください|ね[^い]|よ[^う]|$)|やめて(?!おく|ほし)|禁止|ダメだ(?!った|ろうと|と思)|危険[だです]/,
|
|
54
104
|
// English: require a concrete action after avoid/don't/never — a bare
|
|
55
105
|
// "Avoiding rebuild of unchanged files" in a Vercel log is not a caveat.
|
|
56
106
|
/\b(?:don'?t|do\s+not)\s+(?:do|use|run|call|forget|try|send|share|commit|push|paste|edit)\b|\bnever\s+(?:do|use|call|share|commit|paste|run|push|edit)\b|\bavoid\s+(?:using|running|calling|committing|pushing|sharing|pasting|editing|creating|modifying)\b|\bwatch\s+out\b/i,
|
|
@@ -58,6 +108,27 @@ const CAVEAT_PATTERNS = [
|
|
|
58
108
|
function matchesAny(text, patterns) {
|
|
59
109
|
return patterns.some((p) => p.test(text));
|
|
60
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Check if a message is mostly chitchat with a decision keyword buried in it.
|
|
113
|
+
* The サイダー problem: "おお!そうだね。書斎で無糖のサイダーでした。…決めた"
|
|
114
|
+
* matches DECISION_PATTERNS because of "決めた" at the end, but the message
|
|
115
|
+
* is primarily casual conversation, not a project decision.
|
|
116
|
+
*
|
|
117
|
+
* Heuristic: if the decision keyword appears ONLY in the last 30% of a long
|
|
118
|
+
* message (>100 chars) AND the first 50 chars match chitchat patterns, skip it.
|
|
119
|
+
*/
|
|
120
|
+
const CHITCHAT_OPENERS = /^(?:おお|うん|そう(?:だね|だよね)|ありがと|はは|笑|www|OK|おー|へー|なるほど|ちなみに|そういえば|あー|えー|まぁ|まあ|ああ)/;
|
|
121
|
+
function isChitchatWithBuriedDecision(text, patterns) {
|
|
122
|
+
if (text.length < 100)
|
|
123
|
+
return false; // short messages are fine
|
|
124
|
+
if (!CHITCHAT_OPENERS.test(text.trim()))
|
|
125
|
+
return false; // doesn't open with chitchat
|
|
126
|
+
// Check if any pattern matches in the first 40% of the text
|
|
127
|
+
const earlyPortion = text.slice(0, Math.floor(text.length * 0.4));
|
|
128
|
+
if (patterns.some((p) => p.test(earlyPortion)))
|
|
129
|
+
return false; // decision is early = legitimate
|
|
130
|
+
return true; // chitchat opening + decision keyword only appears late = noise
|
|
131
|
+
}
|
|
61
132
|
// Dedupe successive file edits to the same path within N seconds —
|
|
62
133
|
// they're usually the same logical change.
|
|
63
134
|
// NOTE: this only dedupes for memory CREATION (1 implementation memory per file
|
|
@@ -86,31 +157,39 @@ export function extractSession(session, projectName) {
|
|
|
86
157
|
// 1) Goal layer — the first REAL intent (or synthetic marker for automated sessions)
|
|
87
158
|
const firstIntent = findFirstIntent(session);
|
|
88
159
|
if (firstIntent) {
|
|
160
|
+
const intentText = firstIntent.text.slice(0, 1000);
|
|
89
161
|
memories.push({
|
|
90
162
|
layer: 'goal',
|
|
91
|
-
content:
|
|
92
|
-
|
|
93
|
-
|
|
163
|
+
content: buildStructuredContent({
|
|
164
|
+
title: makeTitle(intentText),
|
|
165
|
+
altitude: inferAltitude(intentText),
|
|
166
|
+
type: 'work',
|
|
167
|
+
state: 'in_progress',
|
|
168
|
+
what: intentText,
|
|
169
|
+
why: 'Session intent — first user message',
|
|
170
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'source session' }],
|
|
94
171
|
session_id: session.session_id,
|
|
95
172
|
git_branch: session.git_branch,
|
|
96
|
-
}
|
|
97
|
-
importance: automated ? 0.3 : 0.8,
|
|
173
|
+
}),
|
|
174
|
+
importance: automated ? 0.3 : 0.8,
|
|
98
175
|
source: { session_id: session.session_id, turn_uuid: firstIntent.uuid, kind: 'first_intent' },
|
|
99
176
|
});
|
|
100
177
|
}
|
|
101
178
|
else if (automated) {
|
|
102
|
-
// Synthetic goal so the session is still discoverable
|
|
103
179
|
const match = firstRawUserText.match(/<scheduled-task\s+name="([^"]+)"/);
|
|
104
180
|
const taskName = match ? match[1] : 'unknown';
|
|
105
181
|
memories.push({
|
|
106
182
|
layer: 'goal',
|
|
107
|
-
content:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
183
|
+
content: buildStructuredContent({
|
|
184
|
+
title: `Automated: ${taskName}`,
|
|
185
|
+
altitude: 'implementation',
|
|
186
|
+
type: 'work',
|
|
187
|
+
state: 'in_progress',
|
|
188
|
+
what: `Automated scheduled task run: ${taskName}`,
|
|
189
|
+
why: 'Scheduled automation',
|
|
111
190
|
session_id: session.session_id,
|
|
112
191
|
git_branch: session.git_branch,
|
|
113
|
-
}
|
|
192
|
+
}),
|
|
114
193
|
importance: 0.2,
|
|
115
194
|
source: { session_id: session.session_id, kind: 'automated_task' },
|
|
116
195
|
});
|
|
@@ -131,11 +210,17 @@ export function extractSession(session, projectName) {
|
|
|
131
210
|
if (t.text.trim().length < 40)
|
|
132
211
|
continue;
|
|
133
212
|
clarifyCount++;
|
|
213
|
+
const msgText = t.text.slice(0, 600);
|
|
134
214
|
memories.push({
|
|
135
215
|
layer: 'context',
|
|
136
|
-
content:
|
|
137
|
-
|
|
138
|
-
|
|
216
|
+
content: buildStructuredContent({
|
|
217
|
+
title: makeTitle(msgText, 60),
|
|
218
|
+
altitude: inferAltitude(msgText),
|
|
219
|
+
type: 'note',
|
|
220
|
+
state: 'open',
|
|
221
|
+
what: msgText,
|
|
222
|
+
why: 'Clarification during session',
|
|
223
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'source session' }],
|
|
139
224
|
session_id: session.session_id,
|
|
140
225
|
}),
|
|
141
226
|
importance: 0.5,
|
|
@@ -153,17 +238,23 @@ export function extractSession(session, projectName) {
|
|
|
153
238
|
for (const [path, ops] of byPath) {
|
|
154
239
|
const first = ops[0];
|
|
155
240
|
const opsKinds = Array.from(new Set(ops.map((o) => o.operation))).join('+');
|
|
156
|
-
const
|
|
241
|
+
const userIntent = (first.preceding_user_text || '').slice(0, 400);
|
|
157
242
|
const contentSnippet = ops.map((o) => o.tool_input_preview).slice(0, 2).join(' | ').slice(0, 500);
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
243
|
+
const fileName = path.replace(/\\/g, '/').split('/').pop() || path;
|
|
244
|
+
const memoryContent = buildStructuredContent({
|
|
245
|
+
title: `${opsKinds} ${fileName} (${ops.length} ops)`,
|
|
246
|
+
altitude: 'implementation',
|
|
247
|
+
type: 'work',
|
|
248
|
+
state: 'done',
|
|
249
|
+
what: userIntent || `File operation: ${opsKinds} on ${path}`,
|
|
250
|
+
why: userIntent ? `User intent: ${makeTitle(userIntent, 120)}` : '(no explicit preceding intent)',
|
|
251
|
+
affects: [path],
|
|
252
|
+
next_action: null,
|
|
253
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'source session' }],
|
|
163
254
|
sample_change: contentSnippet,
|
|
164
|
-
|
|
255
|
+
op_count: ops.length,
|
|
165
256
|
session_id: session.session_id,
|
|
166
|
-
}
|
|
257
|
+
});
|
|
167
258
|
memories.push({
|
|
168
259
|
layer: 'implementation',
|
|
169
260
|
content: memoryContent,
|
|
@@ -195,12 +286,20 @@ export function extractSession(session, projectName) {
|
|
|
195
286
|
continue;
|
|
196
287
|
if (isPastedExternalContent(t.text))
|
|
197
288
|
continue;
|
|
198
|
-
if (matchesAny(t.text, CAVEAT_PATTERNS) && t.text.length > 20) {
|
|
289
|
+
if (matchesAny(t.text, CAVEAT_PATTERNS) && t.text.length > 20 && !isChitchatWithBuriedDecision(t.text, CAVEAT_PATTERNS)) {
|
|
290
|
+
const caveatText = t.text.slice(0, 500);
|
|
199
291
|
memories.push({
|
|
200
292
|
layer: 'caveat',
|
|
201
|
-
content:
|
|
202
|
-
|
|
203
|
-
|
|
293
|
+
content: buildStructuredContent({
|
|
294
|
+
title: makeTitle(caveatText, 70),
|
|
295
|
+
altitude: inferAltitude(caveatText),
|
|
296
|
+
type: 'learning',
|
|
297
|
+
state: 'done',
|
|
298
|
+
what: caveatText,
|
|
299
|
+
why: 'User-stated warning/prohibition — auto-extracted by caveat pattern match',
|
|
300
|
+
affects: extractAffectedPaths(session.file_ops),
|
|
301
|
+
next_action: null,
|
|
302
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'caveat source' }],
|
|
204
303
|
session_id: session.session_id,
|
|
205
304
|
}),
|
|
206
305
|
importance: 0.75,
|
|
@@ -210,6 +309,9 @@ export function extractSession(session, projectName) {
|
|
|
210
309
|
}
|
|
211
310
|
// 5) Learning layer — messages matching decision patterns
|
|
212
311
|
// Same strict filter applies.
|
|
312
|
+
// NOTE: Without LLM, we store the raw user text as `what` — this is the best
|
|
313
|
+
// heuristic extraction can do. Agent-initiated `remember()` calls should use
|
|
314
|
+
// the full structured format with agent_proposal + user_approval_scope.
|
|
213
315
|
for (const t of session.turns) {
|
|
214
316
|
if (t.role !== 'user' || isMetaOrNoise(t.text))
|
|
215
317
|
continue;
|
|
@@ -217,12 +319,20 @@ export function extractSession(session, projectName) {
|
|
|
217
319
|
continue;
|
|
218
320
|
if (isPastedExternalContent(t.text))
|
|
219
321
|
continue;
|
|
220
|
-
if (matchesAny(t.text, DECISION_PATTERNS) && t.text.length > 15) {
|
|
322
|
+
if (matchesAny(t.text, DECISION_PATTERNS) && t.text.length > 15 && !isChitchatWithBuriedDecision(t.text, DECISION_PATTERNS)) {
|
|
323
|
+
const decisionText = t.text.slice(0, 500);
|
|
221
324
|
memories.push({
|
|
222
325
|
layer: 'learning',
|
|
223
|
-
content:
|
|
224
|
-
|
|
225
|
-
|
|
326
|
+
content: buildStructuredContent({
|
|
327
|
+
title: makeTitle(decisionText, 70),
|
|
328
|
+
altitude: inferAltitude(decisionText),
|
|
329
|
+
type: 'decision',
|
|
330
|
+
state: 'decided',
|
|
331
|
+
what: decisionText,
|
|
332
|
+
why: 'Decision detected by pattern match — may need agent enrichment',
|
|
333
|
+
affects: extractAffectedPaths(session.file_ops),
|
|
334
|
+
next_action: null,
|
|
335
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'decision source' }],
|
|
226
336
|
session_id: session.session_id,
|
|
227
337
|
}),
|
|
228
338
|
importance: 0.7,
|
|
@@ -236,10 +346,15 @@ export function extractSession(session, projectName) {
|
|
|
236
346
|
if (session.errors_count > 3) {
|
|
237
347
|
memories.push({
|
|
238
348
|
layer: 'context',
|
|
239
|
-
content:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
349
|
+
content: buildStructuredContent({
|
|
350
|
+
title: `High error session (${session.errors_count} errors / ${session.turns.length} turns)`,
|
|
351
|
+
altitude: 'implementation',
|
|
352
|
+
type: 'outcome',
|
|
353
|
+
state: 'done',
|
|
354
|
+
what: `This session had ${session.errors_count} tool errors across ${session.turns.length} turns.`,
|
|
355
|
+
why: 'High error rate may indicate environmental or configuration issues worth investigating',
|
|
356
|
+
affects: extractAffectedPaths(session.file_ops),
|
|
357
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'error session' }],
|
|
243
358
|
session_id: session.session_id,
|
|
244
359
|
}),
|
|
245
360
|
importance: 0.4,
|
|
@@ -248,20 +363,31 @@ export function extractSession(session, projectName) {
|
|
|
248
363
|
}
|
|
249
364
|
// 7) Session summary — one meta-implementation memory per session for overview
|
|
250
365
|
if (memories.length > 0) {
|
|
366
|
+
const durationMin = Math.round((session.ended_at - session.started_at) / 60);
|
|
367
|
+
const firstGoalTitle = firstIntent ? makeTitle(firstIntent.text, 60) : 'automated task';
|
|
251
368
|
memories.push({
|
|
252
369
|
layer: 'implementation',
|
|
253
|
-
content:
|
|
370
|
+
content: buildStructuredContent({
|
|
371
|
+
title: `Session: ${firstGoalTitle} (${durationMin}min, ${byPath.size} files)`,
|
|
372
|
+
altitude: 'implementation',
|
|
373
|
+
type: 'outcome',
|
|
374
|
+
state: 'done',
|
|
375
|
+
what: `Session completed: ${durationMin} minutes, ${session.turn_count_user} user turns, ${byPath.size} files touched, ${session.errors_count} errors`,
|
|
376
|
+
why: 'Session overview for timeline and activity tracking',
|
|
377
|
+
affects: extractAffectedPaths(session.file_ops),
|
|
378
|
+
next_action: null,
|
|
379
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'session overview' }],
|
|
254
380
|
summary_kind: 'session_overview',
|
|
255
381
|
session_id: session.session_id,
|
|
256
382
|
started_at: new Date(session.started_at * 1000).toISOString(),
|
|
257
383
|
ended_at: new Date(session.ended_at * 1000).toISOString(),
|
|
258
|
-
duration_min:
|
|
384
|
+
duration_min: durationMin,
|
|
259
385
|
turns_user: session.turn_count_user,
|
|
260
386
|
turns_assistant: session.turn_count_assistant,
|
|
261
387
|
files_touched: byPath.size,
|
|
262
388
|
errors: session.errors_count,
|
|
263
389
|
git_branch: session.git_branch,
|
|
264
|
-
}
|
|
390
|
+
}),
|
|
265
391
|
importance: 0.4,
|
|
266
392
|
source: { session_id: session.session_id, kind: 'session_summary' },
|
|
267
393
|
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
interface ElicitResult {
|
|
3
|
+
action: 'accept' | 'decline' | 'cancel' | 'unsupported';
|
|
4
|
+
content?: Record<string, unknown>;
|
|
5
|
+
reason?: string;
|
|
6
|
+
}
|
|
7
|
+
interface ElicitParams {
|
|
8
|
+
message: string;
|
|
9
|
+
requestedSchema: {
|
|
10
|
+
type: 'object';
|
|
11
|
+
properties: Record<string, unknown>;
|
|
12
|
+
required?: string[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export declare function elicit(server: Server, p: ElicitParams): Promise<ElicitResult>;
|
|
16
|
+
export declare function confirmForget(server: Server, candidate: {
|
|
17
|
+
id: number;
|
|
18
|
+
entity: string;
|
|
19
|
+
layer: string;
|
|
20
|
+
importance: number;
|
|
21
|
+
preview: string;
|
|
22
|
+
}): Promise<boolean>;
|
|
23
|
+
export declare function confirmPin(server: Server, memoryId: number, preview: string, newImportance: number): Promise<boolean>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Elicitation block — server asks the client (and ultimately the user) a structured question.
|
|
2
|
+
//
|
|
3
|
+
// MCP semantics: server.request({method: 'elicitation/create', params: {message, requestedSchema}})
|
|
4
|
+
// The user responds via the client UI. Used by:
|
|
5
|
+
// - Stale-memory cleanup (forget candidates require confirmation)
|
|
6
|
+
// - Pin/unpin confirmation when importance crosses 0.9
|
|
7
|
+
//
|
|
8
|
+
// Clients without elicitation support fail gracefully — caller falls back to "decline = skip".
|
|
9
|
+
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
export async function elicit(server, p) {
|
|
11
|
+
try {
|
|
12
|
+
const res = await server.request({ method: 'elicitation/create', params: p }, ElicitRequestSchema);
|
|
13
|
+
return {
|
|
14
|
+
action: res?.action ?? 'decline',
|
|
15
|
+
content: res?.content,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
return { action: 'unsupported', reason: err?.message ?? String(err) };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function confirmForget(server, candidate) {
|
|
23
|
+
const res = await elicit(server, {
|
|
24
|
+
message: `Forget memory #${candidate.id} for "${candidate.entity}"?\n\nLayer: ${candidate.layer} Importance: ${candidate.importance.toFixed(2)}\nPreview: ${candidate.preview.slice(0, 200)}`,
|
|
25
|
+
requestedSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
confirm: {
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
title: 'Forget this memory',
|
|
31
|
+
description: 'Yes = delete permanently. No = keep.',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ['confirm'],
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (res.action === 'accept' && res.content && typeof res.content.confirm === 'boolean') {
|
|
38
|
+
return res.content.confirm;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
export async function confirmPin(server, memoryId, preview, newImportance) {
|
|
43
|
+
const res = await elicit(server, {
|
|
44
|
+
message: `Pin memory #${memoryId}? (importance ${newImportance.toFixed(2)})\n\nPreview: ${preview.slice(0, 200)}\n\nPinned memories survive forget-sweeps and consolidation.`,
|
|
45
|
+
requestedSchema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
confirm: { type: 'boolean', title: 'Pin this memory', description: 'Yes = pin. No = save without pinning.' },
|
|
49
|
+
},
|
|
50
|
+
required: ['confirm'],
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
if (res.action === 'accept' && res.content && typeof res.content.confirm === 'boolean') {
|
|
54
|
+
return res.content.confirm;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=elicitation.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const PROMPTS: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
arguments: {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
required: boolean;
|
|
8
|
+
}[];
|
|
9
|
+
}[];
|
|
10
|
+
interface PromptMessage {
|
|
11
|
+
role: 'user' | 'assistant';
|
|
12
|
+
content: {
|
|
13
|
+
type: 'text';
|
|
14
|
+
text: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare function getPrompt(name: string, args: Record<string, string> | undefined): {
|
|
18
|
+
description?: string;
|
|
19
|
+
messages: PromptMessage[];
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Prompts block — reusable prompt templates that agents can pull from the server.
|
|
2
|
+
//
|
|
3
|
+
// Templates:
|
|
4
|
+
// summarize-session — turn a chat transcript into structured memories (1 per layer)
|
|
5
|
+
// extract-caveats — read text and produce caveat-layer entries (pain lessons)
|
|
6
|
+
// weekly-consolidation — sleep-mode summary of the past week's memories
|
|
7
|
+
// recall-and-write — recall first, then write — anti-pattern guard
|
|
8
|
+
// entity-handoff — produce a handoff doc for an entity (name + memories + next steps)
|
|
9
|
+
//
|
|
10
|
+
// Each prompt accepts arguments and returns a list of messages the client can feed to its LLM.
|
|
11
|
+
export const PROMPTS = [
|
|
12
|
+
{
|
|
13
|
+
name: 'summarize-session',
|
|
14
|
+
description: 'Turn a chat session transcript into structured memories. Produces up to 6 memories (one per layer) capturing goal/context/emotion/implementation/caveat/learning. Use at session end.',
|
|
15
|
+
arguments: [
|
|
16
|
+
{ name: 'transcript', description: 'The session transcript text. Free-form.', required: true },
|
|
17
|
+
{ name: 'entity_hint', description: 'Optional canonical entity name to attach the memories to.', required: false },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'extract-caveats',
|
|
22
|
+
description: 'Scan a body of text (post-mortem, error log, decision doc) and propose caveat-layer memories — concise pain lessons starting with verbs ("Never", "Always", "Watch out"). Returns JSON list of caveats.',
|
|
23
|
+
arguments: [
|
|
24
|
+
{ name: 'text', description: 'Source text (post-mortem, debug session, retro). Free-form.', required: true },
|
|
25
|
+
{ name: 'entity_hint', description: 'Optional canonical entity name for the caveats.', required: false },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
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 to the consolidate tool, or to write a Friday digest.',
|
|
31
|
+
arguments: [
|
|
32
|
+
{ name: 'entity_name', description: 'The entity to consolidate.', required: true },
|
|
33
|
+
{ name: 'week_offset', description: 'Weeks-ago offset (0 = this week, 1 = last week). Default 0.', required: false },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'recall-and-write',
|
|
38
|
+
description: 'Anti-pattern guard. Before writing code, draft a doc, or making a decision: recall relevant memories first, then produce the answer with explicit citations to the recalled memory_ids. Forces "memory before action" discipline.',
|
|
39
|
+
arguments: [
|
|
40
|
+
{ name: 'task', description: 'What you are about to do (code task, decision, doc draft). One sentence.', required: true },
|
|
41
|
+
{ name: 'entity_hint', description: 'Optional entity to focus recall on.', required: false },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'entity-handoff',
|
|
46
|
+
description: 'Produce a handoff document for an entity: name, kind, key memories per layer, current open questions, and next steps. Use when transferring context to a new session, a new agent, or a new collaborator.',
|
|
47
|
+
arguments: [
|
|
48
|
+
{ name: 'entity_name', description: 'The entity to hand off.', required: true },
|
|
49
|
+
{ name: 'audience', description: 'Who receives the handoff (e.g. "new claude session", "human teammate"). Default "new claude session".', required: false },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
export function getPrompt(name, args) {
|
|
54
|
+
const a = args ?? {};
|
|
55
|
+
switch (name) {
|
|
56
|
+
case 'summarize-session': {
|
|
57
|
+
const transcript = a.transcript ?? '';
|
|
58
|
+
const entityHint = a.entity_hint ? `\n\nFocus entity: ${a.entity_hint}` : '';
|
|
59
|
+
return {
|
|
60
|
+
description: 'Summarize the session into 6-layer structured memories.',
|
|
61
|
+
messages: [
|
|
62
|
+
{
|
|
63
|
+
role: 'user',
|
|
64
|
+
content: {
|
|
65
|
+
type: 'text',
|
|
66
|
+
text: `You are an agent-memory writer. Read the session transcript below and propose memories to save. Output a JSON array; each item has {entity_name, entity_kind, layer, content, importance}.
|
|
67
|
+
|
|
68
|
+
Layers (use exactly one per memory):
|
|
69
|
+
- goal: WHY this work exists, target outcome
|
|
70
|
+
- context: WHY THIS NOW, situation, timing
|
|
71
|
+
- emotion: USER tone, feelings expressed
|
|
72
|
+
- implementation: HOW it was done, what worked, what failed
|
|
73
|
+
- caveat: PAIN lesson, "never X" / "always Y" — these are protected from forgetting
|
|
74
|
+
- learning: GROWTH, decisions made, insights
|
|
75
|
+
|
|
76
|
+
Rules:
|
|
77
|
+
- Max 6 entries (one per layer). Skip layers with nothing worth saving.
|
|
78
|
+
- Importance 0-1. Set 0.9+ to pin (use sparingly).
|
|
79
|
+
- Caveats must be 1 sentence and start with a verb.
|
|
80
|
+
- Quote nothing verbatim; summarize.
|
|
81
|
+
|
|
82
|
+
Transcript:
|
|
83
|
+
${transcript}${entityHint}`,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
case 'extract-caveats': {
|
|
90
|
+
const text = a.text ?? '';
|
|
91
|
+
const entityHint = a.entity_hint ? `\n\nFocus entity: ${a.entity_hint}` : '';
|
|
92
|
+
return {
|
|
93
|
+
description: 'Extract caveat-layer pain lessons.',
|
|
94
|
+
messages: [
|
|
95
|
+
{
|
|
96
|
+
role: 'user',
|
|
97
|
+
content: {
|
|
98
|
+
type: 'text',
|
|
99
|
+
text: `You are a caveat extractor. Read the source text below and output a JSON list of caveats. Each caveat:
|
|
100
|
+
- Starts with a verb ("Never", "Always", "Watch out", "Reject", "Confirm")
|
|
101
|
+
- Is one sentence
|
|
102
|
+
- Is concrete (a specific failure mode, not abstract advice)
|
|
103
|
+
- Captures something the reader does NOT want to relearn the hard way
|
|
104
|
+
|
|
105
|
+
Output format: [{"content": "Never X when Y, because Z", "importance": 0.7-1.0}]
|
|
106
|
+
|
|
107
|
+
Source text:
|
|
108
|
+
${text}${entityHint}`,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
case 'weekly-consolidation': {
|
|
115
|
+
const entityName = a.entity_name ?? '<entity>';
|
|
116
|
+
const weekOffset = a.week_offset ?? '0';
|
|
117
|
+
return {
|
|
118
|
+
description: `Consolidate the last week of memories for ${entityName}.`,
|
|
119
|
+
messages: [
|
|
120
|
+
{
|
|
121
|
+
role: 'user',
|
|
122
|
+
content: {
|
|
123
|
+
type: 'text',
|
|
124
|
+
text: `Consolidate the past week's memories for entity "${entityName}" (week_offset=${weekOffset}).
|
|
125
|
+
|
|
126
|
+
Step 1: Call recall(query="${entityName}", entity_name="${entityName}", max_tokens=4000) to retrieve recent memories.
|
|
127
|
+
Step 2: Read the returned memories and produce ONE learning-layer summary that captures:
|
|
128
|
+
- What we set out to do (goal trajectory)
|
|
129
|
+
- What actually happened (implementation summary)
|
|
130
|
+
- What we learned (1-3 insights)
|
|
131
|
+
- Any caveats we should never forget (preserve these — do NOT consolidate them away)
|
|
132
|
+
|
|
133
|
+
Step 3: Output a JSON object {entity_name, layer:"learning", content, importance:0.7}.
|
|
134
|
+
|
|
135
|
+
Do not write to memory directly — just return the JSON. The user will choose whether to save.`,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
case 'recall-and-write': {
|
|
142
|
+
const task = a.task ?? '<task>';
|
|
143
|
+
const entityHint = a.entity_hint ?? '';
|
|
144
|
+
const recallCmd = entityHint
|
|
145
|
+
? `recall(query="${task}", entity_name="${entityHint}", max_tokens=2000)`
|
|
146
|
+
: `recall(query="${task}", max_tokens=2000)`;
|
|
147
|
+
return {
|
|
148
|
+
description: 'Memory-before-action discipline.',
|
|
149
|
+
messages: [
|
|
150
|
+
{
|
|
151
|
+
role: 'user',
|
|
152
|
+
content: {
|
|
153
|
+
type: 'text',
|
|
154
|
+
text: `Before doing this task, recall first.
|
|
155
|
+
|
|
156
|
+
Task: ${task}
|
|
157
|
+
|
|
158
|
+
Step 1: Call ${recallCmd}.
|
|
159
|
+
Step 2: Skim the returned memories. Identify any caveats that apply.
|
|
160
|
+
Step 3: Produce your output with INLINE citations to relevant memory_ids: e.g. "Use better-sqlite3 v12+ [memory:1234] because v11 breaks on Node 24 [memory:5678]."
|
|
161
|
+
Step 4: If you found NO relevant memories, say so explicitly: "No prior memories on this — proceeding from first principles."
|
|
162
|
+
|
|
163
|
+
Goal: never solve a problem twice without checking.`,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
case 'entity-handoff': {
|
|
170
|
+
const entityName = a.entity_name ?? '<entity>';
|
|
171
|
+
const audience = a.audience ?? 'new claude session';
|
|
172
|
+
return {
|
|
173
|
+
description: `Produce a handoff document for ${entityName}.`,
|
|
174
|
+
messages: [
|
|
175
|
+
{
|
|
176
|
+
role: 'user',
|
|
177
|
+
content: {
|
|
178
|
+
type: 'text',
|
|
179
|
+
text: `Produce a handoff document for entity "${entityName}", aimed at: ${audience}.
|
|
180
|
+
|
|
181
|
+
Step 1: Call recall(query="${entityName}", entity_name="${entityName}", max_tokens=6000).
|
|
182
|
+
Step 2: Skim memories grouped by layer.
|
|
183
|
+
Step 3: Output a markdown doc with these sections:
|
|
184
|
+
- **Identity**: name, kind, canonical key (if any)
|
|
185
|
+
- **Goal** (from goal-layer memories): what this entity is for
|
|
186
|
+
- **State** (from latest implementation memories): where things stand right now
|
|
187
|
+
- **Caveats** (from caveat-layer memories): what NEVER to do, with memory_id citations
|
|
188
|
+
- **Open questions**: things the prior session left unresolved
|
|
189
|
+
- **Suggested next steps**: 3 concrete actions for the audience
|
|
190
|
+
|
|
191
|
+
Keep it under 1 page. Cite memory_ids inline like [memory:1234].`,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
default:
|
|
198
|
+
throw new Error(`unknown prompt: ${name}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
export declare const STATIC_RESOURCES: {
|
|
3
|
+
uri: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
}[];
|
|
8
|
+
export declare const RESOURCE_TEMPLATES: {
|
|
9
|
+
uriTemplate: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
mimeType: string;
|
|
13
|
+
}[];
|
|
14
|
+
export declare function readResource(db: Database.Database, uri: string): {
|
|
15
|
+
uri: string;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
text: string;
|
|
18
|
+
};
|