pi-automem-core 0.2.0 → 0.2.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 +8 -167
- package/package.json +7 -39
- package/LICENSE +0 -21
- package/examples/config.advanced.json +0 -59
- package/examples/config.minimal.json +0 -10
- package/prompts/automem-guidelines.md +0 -42
- package/skills/SKILL.md +0 -41
- package/src/commands/recall.ts +0 -44
- package/src/commands/status.ts +0 -40
- package/src/config.ts +0 -251
- package/src/context-injector.ts +0 -42
- package/src/index.ts +0 -158
- package/src/mcp-client.ts +0 -361
- package/src/project-detect.ts +0 -94
- package/src/recall.ts +0 -254
- package/src/secret-scan.ts +0 -34
- package/src/tools/memory-tools.ts +0 -307
- package/src/tools/relationship-tools.ts +0 -114
- package/src/write-policy.ts +0 -142
package/src/secret-scan.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export interface SecretFinding {
|
|
2
|
-
kind: string;
|
|
3
|
-
match: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const SECRET_PATTERNS: Array<{ kind: string; regex: RegExp }> = [
|
|
7
|
-
{ kind: "private-key", regex: /-----BEGIN\s+(?:RSA\s+|EC\s+|OPENSSH\s+)?PRIVATE\s+KEY-----/i },
|
|
8
|
-
{ kind: "bearer-token", regex: /\bBearer\s+[A-Za-z0-9._~+\/-]+=*/i },
|
|
9
|
-
{ kind: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/ },
|
|
10
|
-
{ kind: "openai-key", regex: /\bsk-[A-Za-z0-9_-]{20,}\b/ },
|
|
11
|
-
{ kind: "aws-access-key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
12
|
-
{ kind: "secret-assignment", regex: /\b(?:api[_-]?key|token|secret|password|passwd|credential)\b\s*[:=]\s*['\"]?[^\s'\"]{20,}/i },
|
|
13
|
-
{ kind: "connection-string", regex: /\b(?:postgres|mysql|mongodb|redis):\/\/[^\s]+/i },
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
export function scanForSecrets(text: string): SecretFinding[] {
|
|
17
|
-
const findings: SecretFinding[] = [];
|
|
18
|
-
for (const pattern of SECRET_PATTERNS) {
|
|
19
|
-
const match = text.match(pattern.regex);
|
|
20
|
-
if (match && match[0]) {
|
|
21
|
-
findings.push({ kind: pattern.kind, match: redact(match[0]) });
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return findings;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function hasSecrets(text: string): boolean {
|
|
28
|
-
return scanForSecrets(text).length > 0;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function redact(value: string): string {
|
|
32
|
-
if (value.length <= 12) return "[redacted]";
|
|
33
|
-
return value.slice(0, 6) + "…" + value.slice(-4);
|
|
34
|
-
}
|
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Type } from "typebox";
|
|
3
|
-
import { loadConfig, type MemoryType } from "../config";
|
|
4
|
-
import { automemRecall, automemStore, automemUpdate, setAutoMemMcpServerName } from "../mcp-client";
|
|
5
|
-
import { evaluateWritePolicy, formatCandidate, type MemoryCandidate } from "../write-policy";
|
|
6
|
-
import { parseSearchResults } from "../recall";
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Shared parameter schemas (plain Type.Object — no Type.Intersect)
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
const CandidateParams = Type.Object({
|
|
13
|
-
content: Type.String({ description: "Compact memory text. Target 150-300 chars; hard max from config, default 2000." }),
|
|
14
|
-
type: Type.String({ description: "Memory type: Decision, Pattern, Preference, Style, Habit, Insight, or Context" }),
|
|
15
|
-
tags: Type.Optional(Type.Array(Type.String(), { description: "Tags such as source:pi, project:<slug>, preference, decision" })),
|
|
16
|
-
importance: Type.Optional(Type.Number({ description: "Importance 0-1. Use 0.85+ for durable decisions/preferences/corrections." })),
|
|
17
|
-
confidence: Type.Optional(Type.Number({ description: "Classification confidence 0-1. Default 0.9." })),
|
|
18
|
-
category: Type.Optional(Type.String({ description: "Write-policy category, e.g. technical-decision, agent-pattern, bug-fix, private" })),
|
|
19
|
-
source: Type.Optional(Type.String({ description: "Memory source label. Default from config." })),
|
|
20
|
-
metadata: Type.Optional(Type.Any({ description: "Optional JSON metadata" })),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const CommitParams = Type.Object({
|
|
24
|
-
content: Type.String({ description: "Compact memory text. Target 150-300 chars; hard max from config, default 2000." }),
|
|
25
|
-
type: Type.String({ description: "Memory type: Decision, Pattern, Preference, Style, Habit, Insight, or Context" }),
|
|
26
|
-
tags: Type.Optional(Type.Array(Type.String(), { description: "Tags such as source:pi, project:<slug>, preference, decision" })),
|
|
27
|
-
importance: Type.Optional(Type.Number({ description: "Importance 0-1. Use 0.85+ for durable decisions/preferences/corrections." })),
|
|
28
|
-
confidence: Type.Optional(Type.Number({ description: "Classification confidence 0-1. Default 0.9." })),
|
|
29
|
-
category: Type.Optional(Type.String({ description: "Write-policy category, e.g. technical-decision, agent-pattern, bug-fix, private" })),
|
|
30
|
-
source: Type.Optional(Type.String({ description: "Memory source label. Default from config." })),
|
|
31
|
-
metadata: Type.Optional(Type.Any({ description: "Optional JSON metadata" })),
|
|
32
|
-
approvedByUser: Type.Optional(Type.Boolean({ description: "Set true only after explicit user approval for this exact memory candidate." })),
|
|
33
|
-
dedupeQuery: Type.Optional(Type.String({ description: "Optional query for similar-memory recall before storing. Defaults to content." })),
|
|
34
|
-
updateMemoryId: Type.Optional(Type.String({ description: "If set, update this existing memory instead of storing a new one. Use when dedupe found a close match." })),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const UpdateParams = Type.Object({
|
|
38
|
-
memoryId: Type.String({ description: "ID of the existing AutoMem memory to update." }),
|
|
39
|
-
content: Type.Optional(Type.String({ description: "New memory content to replace the existing content." })),
|
|
40
|
-
type: Type.Optional(Type.String({ description: "Updated memory type: Decision, Pattern, Preference, Style, Habit, Insight, or Context" })),
|
|
41
|
-
tags: Type.Optional(Type.Array(Type.String(), { description: "Updated tags (replaces existing tags on the memory)." })),
|
|
42
|
-
importance: Type.Optional(Type.Number({ description: "Updated importance 0-1." })),
|
|
43
|
-
confidence: Type.Optional(Type.Number({ description: "Updated confidence 0-1." })),
|
|
44
|
-
metadata: Type.Optional(Type.Any({ description: "Updated metadata (merged with existing)." })),
|
|
45
|
-
approvedByUser: Type.Optional(Type.Boolean({ description: "Set true only after explicit user approval." })),
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
// Registration
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
|
|
52
|
-
export function registerMemoryTools(pi: ExtensionAPI) {
|
|
53
|
-
// ── automem_propose_memory ──────────────────────────────────────────────
|
|
54
|
-
pi.registerTool({
|
|
55
|
-
name: "automem_propose_memory",
|
|
56
|
-
label: "AutoMem Propose Memory",
|
|
57
|
-
description: "Validate and preview a durable memory candidate without writing it. Runs policy, secret scan, and similar-memory recall.",
|
|
58
|
-
promptSnippet: "Use before storing durable memories. It does not write; it proposes and checks relevance/safety.",
|
|
59
|
-
parameters: CandidateParams,
|
|
60
|
-
async execute(_toolCallId, params) {
|
|
61
|
-
const config = loadConfig();
|
|
62
|
-
setAutoMemMcpServerName(config.mcpServerName);
|
|
63
|
-
const candidate = toCandidate(params);
|
|
64
|
-
const decision = evaluateWritePolicy(candidate, config);
|
|
65
|
-
const { text: similarText, matches: similarMatches } = await recallSimilarWithMatches(
|
|
66
|
-
decision.normalized.content,
|
|
67
|
-
decision.normalized.tags,
|
|
68
|
-
config,
|
|
69
|
-
).catch(() => ({ text: "Similar recall failed.", matches: [] }));
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
content: [{ type: "text" as const, text: formatProposal(decision.action, decision.reasons, decision.normalized, similarText, similarMatches) }],
|
|
73
|
-
details: { action: decision.action, reasons: decision.reasons, findings: decision.findings, candidate: decision.normalized, similarMatches },
|
|
74
|
-
isError: decision.action === "block",
|
|
75
|
-
};
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// ── automem_commit_memory ───────────────────────────────────────────────
|
|
80
|
-
pi.registerTool({
|
|
81
|
-
name: "automem_commit_memory",
|
|
82
|
-
label: "AutoMem Commit Memory",
|
|
83
|
-
description: "Store a policy-approved durable memory in AutoMem. If dedupe finds a close match, returns DUPLICATE_DETECTED with the matching memory ID — use updateMemoryId to update instead of creating a duplicate.",
|
|
84
|
-
promptSnippet: "Use only after automem_propose_memory and explicit approval, unless policy returns safe-auto. If DUPLICATE_DETECTED, consider calling again with updateMemoryId instead.",
|
|
85
|
-
parameters: CommitParams,
|
|
86
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx: any) {
|
|
87
|
-
const config = loadConfig();
|
|
88
|
-
setAutoMemMcpServerName(config.mcpServerName);
|
|
89
|
-
const candidate = toCandidate(params);
|
|
90
|
-
const decision = evaluateWritePolicy(candidate, config);
|
|
91
|
-
|
|
92
|
-
if (decision.action === "block") {
|
|
93
|
-
return {
|
|
94
|
-
content: [{ type: "text" as const, text: "Blocked by AutoMem write policy.\n" + decision.reasons.map((r: string) => "- " + r).join("\n") }],
|
|
95
|
-
details: { action: decision.action, reasons: decision.reasons, findings: decision.findings },
|
|
96
|
-
isError: true,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const needsConfirmation = decision.action !== "auto";
|
|
101
|
-
if (needsConfirmation && !params.approvedByUser) {
|
|
102
|
-
if (ctx && ctx.ui && typeof ctx.ui.confirm === "function") {
|
|
103
|
-
const ok = await ctx.ui.confirm("Store AutoMem memory?", formatCandidate(decision.normalized));
|
|
104
|
-
if (!ok) {
|
|
105
|
-
return { content: [{ type: "text" as const, text: "AutoMem memory write cancelled." }], details: { cancelled: true } };
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
return {
|
|
109
|
-
content: [{ type: "text" as const, text: "Confirmation required before storing this memory. Re-run with approvedByUser=true only after explicit user approval." }],
|
|
110
|
-
details: { action: decision.action, reasons: decision.reasons, candidate: decision.normalized },
|
|
111
|
-
isError: true,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── UPDATE path ──────────────────────────────────────────────────────
|
|
117
|
-
if (params.updateMemoryId) {
|
|
118
|
-
const result = await automemUpdate(params.updateMemoryId, {
|
|
119
|
-
content: decision.normalized.content,
|
|
120
|
-
type: decision.normalized.type,
|
|
121
|
-
tags: decision.normalized.tags,
|
|
122
|
-
importance: decision.normalized.importance,
|
|
123
|
-
confidence: decision.normalized.confidence,
|
|
124
|
-
metadata: { ...(decision.normalized.metadata || {}), write_policy_action: decision.action, updated_via: "automem_commit_memory" },
|
|
125
|
-
});
|
|
126
|
-
const text = result.content?.[0]?.text || "Memory updated.";
|
|
127
|
-
return {
|
|
128
|
-
content: [{ type: "text" as const, text: "Updated existing AutoMem memory " + params.updateMemoryId + ".\n\n" + text }],
|
|
129
|
-
details: { updated: true, memoryId: params.updateMemoryId, candidate: decision.normalized },
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ── DEDUPE CHECK ─────────────────────────────────────────────────────
|
|
134
|
-
// Skip dedupe if dedupeQuery is explicitly set to empty string
|
|
135
|
-
const skipDedupe = typeof params.dedupeQuery === "string" && params.dedupeQuery.trim() === "";
|
|
136
|
-
const dedupeQuery = skipDedupe ? null : (params.dedupeQuery || decision.normalized.content);
|
|
137
|
-
const { text: similarText, matches: similarMatches } = dedupeQuery
|
|
138
|
-
? await recallSimilarWithMatches(dedupeQuery, decision.normalized.tags, config).catch(() => ({ text: "", matches: [] }))
|
|
139
|
-
: { text: "", matches: [] };
|
|
140
|
-
|
|
141
|
-
// Surface a duplicate warning if a close match exists
|
|
142
|
-
if (!skipDedupe && similarMatches.length > 0 && similarMatches[0].id) {
|
|
143
|
-
const top = similarMatches[0];
|
|
144
|
-
return {
|
|
145
|
-
content: [{
|
|
146
|
-
type: "text" as const,
|
|
147
|
-
text: [
|
|
148
|
-
"DUPLICATE_DETECTED — a similar memory already exists.",
|
|
149
|
-
"",
|
|
150
|
-
"Existing memory (ID: " + top.id + "):",
|
|
151
|
-
" " + top.content,
|
|
152
|
-
"",
|
|
153
|
-
"Your candidate:",
|
|
154
|
-
" " + decision.normalized.content,
|
|
155
|
-
"",
|
|
156
|
-
"Options:",
|
|
157
|
-
" 1. Update the existing memory: call automem_commit_memory again with updateMemoryId=\"" + top.id + "\"",
|
|
158
|
-
" 2. Store anyway (new memory): call automem_commit_memory with dedupeQuery=\"\" to skip dedupe",
|
|
159
|
-
" 3. Cancel: do nothing if this is not worth storing separately",
|
|
160
|
-
].join("\n"),
|
|
161
|
-
}],
|
|
162
|
-
details: { duplicateDetected: true, existingMemoryId: top.id, existingContent: top.content, candidate: decision.normalized, allSimilar: similarMatches },
|
|
163
|
-
isError: false,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ── STORE path ───────────────────────────────────────────────────────
|
|
168
|
-
const result = await automemStore(
|
|
169
|
-
decision.normalized.content,
|
|
170
|
-
decision.normalized.type,
|
|
171
|
-
decision.normalized.tags,
|
|
172
|
-
{
|
|
173
|
-
source: decision.normalized.source,
|
|
174
|
-
confidence: decision.normalized.confidence,
|
|
175
|
-
importance: decision.normalized.importance,
|
|
176
|
-
metadata: { ...(decision.normalized.metadata || {}), write_policy_action: decision.action },
|
|
177
|
-
},
|
|
178
|
-
);
|
|
179
|
-
const text = result.content?.[0]?.text || "Memory stored.";
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
content: [{ type: "text" as const, text: "Stored AutoMem memory.\n\n" + text }],
|
|
183
|
-
details: { result, candidate: decision.normalized, similarPreview: similarText.slice(0, 500) },
|
|
184
|
-
};
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// ── automem_update_memory ───────────────────────────────────────────────
|
|
189
|
-
pi.registerTool({
|
|
190
|
-
name: "automem_update_memory",
|
|
191
|
-
label: "AutoMem Update Memory",
|
|
192
|
-
description: "Update an existing AutoMem memory by ID. Use when dedupe found a close match and the existing memory needs correction or enrichment rather than a new duplicate being stored.",
|
|
193
|
-
promptSnippet: "Use after automem_commit_memory returns DUPLICATE_DETECTED, or when correcting a known memory. Requires the existing memory ID.",
|
|
194
|
-
parameters: UpdateParams,
|
|
195
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx: any) {
|
|
196
|
-
if (!params.memoryId) {
|
|
197
|
-
return {
|
|
198
|
-
content: [{ type: "text" as const, text: "memoryId is required for automem_update_memory." }],
|
|
199
|
-
isError: true,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (!params.approvedByUser) {
|
|
204
|
-
if (ctx && ctx.ui && typeof ctx.ui.confirm === "function") {
|
|
205
|
-
const preview = [
|
|
206
|
-
"Memory ID: " + params.memoryId,
|
|
207
|
-
params.content ? "New content: " + params.content : null,
|
|
208
|
-
params.type ? "Type: " + params.type : null,
|
|
209
|
-
params.tags ? "Tags: " + params.tags.join(", ") : null,
|
|
210
|
-
].filter(Boolean).join("\n");
|
|
211
|
-
const ok = await ctx.ui.confirm("Update AutoMem memory?", preview);
|
|
212
|
-
if (!ok) {
|
|
213
|
-
return { content: [{ type: "text" as const, text: "AutoMem memory update cancelled." }], details: { cancelled: true } };
|
|
214
|
-
}
|
|
215
|
-
} else {
|
|
216
|
-
return {
|
|
217
|
-
content: [{ type: "text" as const, text: "Confirmation required before updating this memory. Re-run with approvedByUser=true only after explicit user approval." }],
|
|
218
|
-
isError: true,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const result = await automemUpdate(params.memoryId, {
|
|
224
|
-
content: params.content,
|
|
225
|
-
type: params.type,
|
|
226
|
-
tags: Array.isArray(params.tags) ? params.tags : undefined,
|
|
227
|
-
importance: params.importance,
|
|
228
|
-
confidence: params.confidence,
|
|
229
|
-
metadata: params.metadata ? { ...params.metadata, updated_via: "automem_update_memory" } : { updated_via: "automem_update_memory" },
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
const text = result.content?.[0]?.text || "Memory updated.";
|
|
233
|
-
return {
|
|
234
|
-
content: [{ type: "text" as const, text: "Updated AutoMem memory " + params.memoryId + ".\n\n" + text }],
|
|
235
|
-
details: { result, memoryId: params.memoryId },
|
|
236
|
-
};
|
|
237
|
-
},
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// ---------------------------------------------------------------------------
|
|
242
|
-
// Helpers
|
|
243
|
-
// ---------------------------------------------------------------------------
|
|
244
|
-
|
|
245
|
-
function toCandidate(params: any): MemoryCandidate {
|
|
246
|
-
return {
|
|
247
|
-
content: params.content,
|
|
248
|
-
type: params.type as MemoryType,
|
|
249
|
-
tags: Array.isArray(params.tags) ? params.tags : [],
|
|
250
|
-
importance: params.importance,
|
|
251
|
-
confidence: params.confidence,
|
|
252
|
-
source: params.source,
|
|
253
|
-
category: params.category,
|
|
254
|
-
metadata: params.metadata,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
interface SimilarResult {
|
|
259
|
-
text: string;
|
|
260
|
-
matches: Array<{ id: string; content: string; score?: number }>;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function recallSimilarWithMatches(query: string, tags: string[], config: any): Promise<SimilarResult> {
|
|
264
|
-
if (config.writePolicy.dedupeBeforeWrite === false) {
|
|
265
|
-
return { text: "Dedupe recall disabled.", matches: [] };
|
|
266
|
-
}
|
|
267
|
-
const result = await automemRecall(query, {
|
|
268
|
-
limit: Number((config.writePolicy as any).dedupeLimit || 3),
|
|
269
|
-
tags,
|
|
270
|
-
tagMode: "any",
|
|
271
|
-
contextTypes: ["Decision", "Pattern", "Preference", "Style", "Habit", "Insight", "Context"],
|
|
272
|
-
expandRelations: false,
|
|
273
|
-
expandEntities: false,
|
|
274
|
-
});
|
|
275
|
-
const text = result.content?.[0]?.text || "No similar memories found.";
|
|
276
|
-
const parsed = parseSearchResults(text);
|
|
277
|
-
const matches = parsed
|
|
278
|
-
.filter(m => m.id)
|
|
279
|
-
.map(m => ({ id: m.id, content: m.content, score: m.score }));
|
|
280
|
-
return { text, matches };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function formatProposal(
|
|
284
|
-
action: string,
|
|
285
|
-
reasons: string[],
|
|
286
|
-
candidate: MemoryCandidate,
|
|
287
|
-
similarText: string,
|
|
288
|
-
similarMatches: Array<{ id: string; content: string; score?: number }>,
|
|
289
|
-
): string {
|
|
290
|
-
const dupeWarning = similarMatches.length > 0 && similarMatches[0].id
|
|
291
|
-
? "\n⚠️ Possible duplicate detected (ID: " + similarMatches[0].id + "):\n " + similarMatches[0].content
|
|
292
|
-
: "";
|
|
293
|
-
|
|
294
|
-
return [
|
|
295
|
-
"# AutoMem memory proposal",
|
|
296
|
-
"",
|
|
297
|
-
"Recommended action: " + action,
|
|
298
|
-
reasons.length ? "\nReasons:\n" + reasons.map((r: string) => "- " + r).join("\n") : "",
|
|
299
|
-
dupeWarning,
|
|
300
|
-
"",
|
|
301
|
-
"## Candidate",
|
|
302
|
-
formatCandidate(candidate),
|
|
303
|
-
"",
|
|
304
|
-
"## Similar-memory check",
|
|
305
|
-
similarText.slice(0, 2000),
|
|
306
|
-
].filter(Boolean).join("\n");
|
|
307
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Type } from "typebox";
|
|
3
|
-
import { loadConfig, type MemoryType } from "../config";
|
|
4
|
-
import { automemStore, automemAssociate, setAutoMemMcpServerName } from "../mcp-client";
|
|
5
|
-
import { evaluateWritePolicy, type MemoryCandidate } from "../write-policy";
|
|
6
|
-
|
|
7
|
-
const LinkParams = Type.Object({
|
|
8
|
-
memoryId1: Type.String({ description: "ID of the first memory." }),
|
|
9
|
-
memoryId2: Type.String({ description: "ID of the second memory." }),
|
|
10
|
-
relationship: Type.String({ description: "Relationship type: RELATES_TO, LEADS_TO, OCCURRED_BEFORE, PREFERS_OVER, EXEMPLIFIES, CONTRADICTS, REINFORCES, INVALIDATED_BY, EVOLVED_INTO, DERIVED_FROM, PART_OF" }),
|
|
11
|
-
strength: Type.Optional(Type.Number({ description: "Relationship weight 0–1. Default 0.5." })),
|
|
12
|
-
approvedByUser: Type.Boolean({ description: "Must be true to execute. Prevents accidental linking." }),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const CorrectParams = Type.Object({
|
|
16
|
-
memoryId: Type.String({ description: "ID of the memory being corrected." }),
|
|
17
|
-
correction: Type.String({ description: "New correct content to store." }),
|
|
18
|
-
type: Type.Optional(Type.String({ description: "Memory type for the new memory. Default: Context." })),
|
|
19
|
-
tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for the new memory." })),
|
|
20
|
-
importance: Type.Optional(Type.Number({ description: "Importance 0–1 for the new memory." })),
|
|
21
|
-
relationship: Type.Optional(Type.String({ description: "EVOLVED_INTO or CONTRADICTS. Default: EVOLVED_INTO." })),
|
|
22
|
-
approvedByUser: Type.Boolean({ description: "Must be true to execute." }),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
export function registerRelationshipTools(pi: ExtensionAPI) {
|
|
26
|
-
pi.registerTool({
|
|
27
|
-
name: "automem_link_memories",
|
|
28
|
-
label: "AutoMem Link Memories",
|
|
29
|
-
description: "Create a typed relationship between two existing AutoMem memories.",
|
|
30
|
-
promptSnippet: "Use after identifying that two memories are related. Requires both memory IDs and the relationship type.",
|
|
31
|
-
parameters: LinkParams,
|
|
32
|
-
async execute(_toolCallId: string, params: any) {
|
|
33
|
-
const config = loadConfig();
|
|
34
|
-
setAutoMemMcpServerName(config.mcpServerName);
|
|
35
|
-
|
|
36
|
-
if (!params.approvedByUser) {
|
|
37
|
-
return {
|
|
38
|
-
content: [{ type: "text" as const, text: "Confirmation required before linking memories. Re-run with approvedByUser=true only after explicit user approval.\n\nWould link:\n " + params.memoryId1 + " → " + params.relationship + " → " + params.memoryId2 }],
|
|
39
|
-
isError: true,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const strength = typeof params.strength === "number" ? params.strength : 0.5;
|
|
44
|
-
const result = await automemAssociate(params.memoryId1, params.memoryId2, params.relationship, strength);
|
|
45
|
-
const text = result.content?.[0]?.text || "Association created.";
|
|
46
|
-
return {
|
|
47
|
-
content: [{ type: "text" as const, text: "Linked " + params.memoryId1 + " → " + params.relationship + " → " + params.memoryId2 + " (strength: " + strength + ").\n\n" + text }],
|
|
48
|
-
details: { memoryId1: params.memoryId1, memoryId2: params.memoryId2, relationship: params.relationship, strength },
|
|
49
|
-
};
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
pi.registerTool({
|
|
54
|
-
name: "automem_correct_memory",
|
|
55
|
-
label: "AutoMem Correct Memory",
|
|
56
|
-
description: "Store a correction to an existing memory and link old → new with a provenance relationship. Preserves history; use automem_update_memory for simple in-place edits.",
|
|
57
|
-
promptSnippet: "Use when a memory was wrong or outdated and you want to preserve history. Stores new content as a separate memory, then links old → EVOLVED_INTO/CONTRADICTS → new.",
|
|
58
|
-
parameters: CorrectParams,
|
|
59
|
-
async execute(_toolCallId: string, params: any) {
|
|
60
|
-
const config = loadConfig();
|
|
61
|
-
setAutoMemMcpServerName(config.mcpServerName);
|
|
62
|
-
|
|
63
|
-
const candidate: MemoryCandidate = {
|
|
64
|
-
content: params.correction,
|
|
65
|
-
type: (params.type || "Context") as MemoryType,
|
|
66
|
-
tags: Array.isArray(params.tags) ? params.tags : [],
|
|
67
|
-
importance: params.importance,
|
|
68
|
-
};
|
|
69
|
-
const decision = evaluateWritePolicy(candidate, config);
|
|
70
|
-
if (decision.action === "block") {
|
|
71
|
-
return {
|
|
72
|
-
content: [{ type: "text" as const, text: "Blocked by AutoMem write policy.\n" + decision.reasons.map((r: string) => "- " + r).join("\n") }],
|
|
73
|
-
details: { action: decision.action, reasons: decision.reasons, findings: decision.findings },
|
|
74
|
-
isError: true,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!params.approvedByUser) {
|
|
79
|
-
return {
|
|
80
|
-
content: [{ type: "text" as const, text: "Confirmation required before correcting memory. Re-run with approvedByUser=true only after explicit user approval.\n\nWould correct memory " + params.memoryId + " with:\n " + params.correction }],
|
|
81
|
-
isError: true,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const rel = params.relationship === "CONTRADICTS" ? "CONTRADICTS" : "EVOLVED_INTO";
|
|
86
|
-
const storeResult = await automemStore(
|
|
87
|
-
params.correction,
|
|
88
|
-
params.type || "Context",
|
|
89
|
-
Array.isArray(params.tags) ? params.tags : [],
|
|
90
|
-
{ importance: params.importance },
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const storeText = storeResult.content?.[0]?.text || "";
|
|
94
|
-
const uuidMatch = storeText.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
|
|
95
|
-
|
|
96
|
-
if (!uuidMatch) {
|
|
97
|
-
console.warn("[automem] automem_correct_memory: could not extract new memory ID from store response — skipping link");
|
|
98
|
-
return {
|
|
99
|
-
content: [{ type: "text" as const, text: "Stored correction (ID unknown — link not created). Store response: " + storeText.slice(0, 300) }],
|
|
100
|
-
details: { storeText },
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const newId = uuidMatch[0];
|
|
105
|
-
const assocResult = await automemAssociate(params.memoryId, newId, rel, 0.9);
|
|
106
|
-
const assocText = assocResult.content?.[0]?.text || "Association created.";
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
content: [{ type: "text" as const, text: "Stored correction as " + newId + ". Linked " + params.memoryId + " → " + rel + " → " + newId + ".\n\n" + assocText }],
|
|
110
|
-
details: { originalId: params.memoryId, newId, relationship: rel },
|
|
111
|
-
};
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
}
|
package/src/write-policy.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import type { AutoMemConfig, MemoryType } from "./config";
|
|
2
|
-
import { scanForSecrets, type SecretFinding } from "./secret-scan";
|
|
3
|
-
|
|
4
|
-
export type WriteMode = "off" | "propose" | "safe-auto" | "confirm-all";
|
|
5
|
-
export type WriteAction = "block" | "propose" | "confirm" | "auto";
|
|
6
|
-
|
|
7
|
-
export interface MemoryCandidate {
|
|
8
|
-
content: string;
|
|
9
|
-
type: MemoryType;
|
|
10
|
-
tags: string[];
|
|
11
|
-
importance: number;
|
|
12
|
-
confidence?: number;
|
|
13
|
-
source?: string;
|
|
14
|
-
category?: string;
|
|
15
|
-
metadata?: Record<string, unknown>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface PolicyDecision {
|
|
19
|
-
action: WriteAction;
|
|
20
|
-
reasons: string[];
|
|
21
|
-
findings: SecretFinding[];
|
|
22
|
-
normalized: MemoryCandidate;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const VALID_TYPES = new Set(["Decision", "Pattern", "Preference", "Style", "Habit", "Insight", "Context"]);
|
|
26
|
-
|
|
27
|
-
export function normalizeCandidate(input: MemoryCandidate, config: AutoMemConfig): MemoryCandidate {
|
|
28
|
-
const tags = Array.from(new Set([
|
|
29
|
-
...(config.writePolicy.alwaysTag || []),
|
|
30
|
-
...(input.tags || []),
|
|
31
|
-
].map(t => String(t).trim().toLowerCase()).filter(Boolean)));
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
content: String(input.content || "").replace(/\s+/g, " ").trim(),
|
|
35
|
-
type: input.type,
|
|
36
|
-
tags,
|
|
37
|
-
importance: clampNumber(input.importance, 0, 1, defaultImportanceForType(input.type)),
|
|
38
|
-
confidence: clampNumber(input.confidence, 0, 1, 0.9),
|
|
39
|
-
source: input.source || config.writePolicy.defaultSource || "pi-session",
|
|
40
|
-
category: (input.category || inferCategory(input.type, tags)).toLowerCase(),
|
|
41
|
-
metadata: input.metadata,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function evaluateWritePolicy(input: MemoryCandidate, config: AutoMemConfig): PolicyDecision {
|
|
46
|
-
const normalized = normalizeCandidate(input, config);
|
|
47
|
-
const reasons: string[] = [];
|
|
48
|
-
const findings = scanForSecrets(normalized.content + "\n" + normalized.tags.join("\n"));
|
|
49
|
-
const mode = ((config.writePolicy as any).mode || "propose") as WriteMode;
|
|
50
|
-
const preferredMax = config.behavior.preferredContentLength || 500;
|
|
51
|
-
const hardMax = config.behavior.maxContentLength || 2000;
|
|
52
|
-
const minImportance = Number((config.writePolicy as any).minImportanceToWrite ?? 0.7);
|
|
53
|
-
|
|
54
|
-
if (mode === "off") reasons.push("write policy mode is off");
|
|
55
|
-
if (!normalized.content) reasons.push("content is empty");
|
|
56
|
-
if (!VALID_TYPES.has(normalized.type)) reasons.push("invalid memory type");
|
|
57
|
-
if (normalized.content.length > hardMax) reasons.push("content exceeds hard length limit");
|
|
58
|
-
if (findings.length > 0) reasons.push("secret/privacy scanner found blocked content");
|
|
59
|
-
if (normalized.importance < minImportance) reasons.push("importance is below configured write threshold");
|
|
60
|
-
if (isBlockedCategory(normalized, config)) reasons.push("category is blocked by write policy");
|
|
61
|
-
|
|
62
|
-
if (reasons.length > 0) {
|
|
63
|
-
return { action: "block", reasons, findings, normalized };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (normalized.content.length > preferredMax) {
|
|
67
|
-
reasons.push("content exceeds preferred embedding length; consider shortening before commit");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (mode === "confirm-all") {
|
|
71
|
-
return { action: "confirm", reasons: ["write policy requires confirmation for all memories", ...reasons], findings, normalized };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (isConfirmCategory(normalized, config)) {
|
|
75
|
-
return { action: "confirm", reasons: ["category requires user confirmation", ...reasons], findings, normalized };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (mode === "safe-auto" && isAutoCategory(normalized, config)) {
|
|
79
|
-
return { action: "auto", reasons: ["category is eligible for safe automatic write", ...reasons], findings, normalized };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return { action: "propose", reasons: ["candidate should be proposed for approval before storing", ...reasons], findings, normalized };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function formatCandidate(candidate: MemoryCandidate): string {
|
|
86
|
-
return (
|
|
87
|
-
"Content: " + candidate.content + "\n" +
|
|
88
|
-
"Type: " + candidate.type + "\n" +
|
|
89
|
-
"Importance: " + candidate.importance + "\n" +
|
|
90
|
-
"Tags: " + (candidate.tags.join(", ") || "(none)") + "\n" +
|
|
91
|
-
"Category: " + (candidate.category || "(inferred)")
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function isBlockedCategory(candidate: MemoryCandidate, config: AutoMemConfig): boolean {
|
|
96
|
-
const blocked = new Set((config.writePolicy.blockedCategories || []).map(c => c.toLowerCase()));
|
|
97
|
-
return blocked.has(candidate.category || "") || candidate.tags.some(t => blocked.has(t));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function isConfirmCategory(candidate: MemoryCandidate, config: AutoMemConfig): boolean {
|
|
101
|
-
const confirm = new Set((config.writePolicy.confirmCategories || []).map(c => c.toLowerCase()));
|
|
102
|
-
return confirm.has(candidate.category || "") || candidate.tags.some(t => confirm.has(t));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function isAutoCategory(candidate: MemoryCandidate, config: AutoMemConfig): boolean {
|
|
106
|
-
const auto = new Set((config.writePolicy.autoWriteCategories || []).map(c => c.toLowerCase()));
|
|
107
|
-
return auto.has(candidate.category || "") || auto.has(candidate.type.toLowerCase()) || candidate.tags.some(t => auto.has(t));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function inferCategory(type: MemoryType, tags: string[]): string {
|
|
111
|
-
if (tags.includes("preference")) return "preference";
|
|
112
|
-
if (tags.includes("decision")) return "technical-decision";
|
|
113
|
-
if (tags.includes("bug-fix")) return "bug-fix";
|
|
114
|
-
if (tags.includes("pattern")) return "agent-pattern";
|
|
115
|
-
if (tags.includes("private") || tags.includes("personal")) return "private";
|
|
116
|
-
switch (type) {
|
|
117
|
-
case "Decision": return "technical-decision";
|
|
118
|
-
case "Preference": return "preference";
|
|
119
|
-
case "Pattern": return "agent-pattern";
|
|
120
|
-
case "Insight": return "tooling-lesson";
|
|
121
|
-
default: return "context";
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function defaultImportanceForType(type: MemoryType): number {
|
|
126
|
-
switch (type) {
|
|
127
|
-
case "Decision": return 0.9;
|
|
128
|
-
case "Preference": return 0.85;
|
|
129
|
-
case "Pattern": return 0.8;
|
|
130
|
-
case "Insight": return 0.8;
|
|
131
|
-
case "Style": return 0.7;
|
|
132
|
-
case "Habit": return 0.65;
|
|
133
|
-
case "Context": return 0.6;
|
|
134
|
-
default: return 0.5;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function clampNumber(value: unknown, min: number, max: number, fallback: number): number {
|
|
139
|
-
const n = Number(value);
|
|
140
|
-
if (!Number.isFinite(n)) return fallback;
|
|
141
|
-
return Math.min(max, Math.max(min, n));
|
|
142
|
-
}
|