pi-automem-core 0.2.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/LICENSE +21 -0
- package/README.md +173 -0
- package/examples/config.advanced.json +59 -0
- package/examples/config.minimal.json +10 -0
- package/package.json +60 -0
- package/prompts/automem-guidelines.md +42 -0
- package/skills/SKILL.md +41 -0
- package/src/commands/recall.ts +44 -0
- package/src/commands/status.ts +40 -0
- package/src/config.ts +251 -0
- package/src/context-injector.ts +42 -0
- package/src/index.ts +158 -0
- package/src/mcp-client.ts +361 -0
- package/src/project-detect.ts +94 -0
- package/src/recall.ts +254 -0
- package/src/secret-scan.ts +34 -0
- package/src/tools/memory-tools.ts +307 -0
- package/src/tools/relationship-tools.ts +114 -0
- package/src/write-policy.ts +142 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.ts - Config loading, defaults, validation, and env-var resolution.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export type MemoryType =
|
|
14
|
+
| "Decision"
|
|
15
|
+
| "Pattern"
|
|
16
|
+
| "Preference"
|
|
17
|
+
| "Style"
|
|
18
|
+
| "Habit"
|
|
19
|
+
| "Insight"
|
|
20
|
+
| "Context";
|
|
21
|
+
|
|
22
|
+
export type RelationshipType =
|
|
23
|
+
| "RELATES_TO"
|
|
24
|
+
| "LEADS_TO"
|
|
25
|
+
| "OCCURRED_BEFORE"
|
|
26
|
+
| "PREFERS_OVER"
|
|
27
|
+
| "EXEMPLIFIES"
|
|
28
|
+
| "CONTRADICTS"
|
|
29
|
+
| "REINFORCES"
|
|
30
|
+
| "INVALIDATED_BY"
|
|
31
|
+
| "EVOLVED_INTO"
|
|
32
|
+
| "DERIVED_FROM"
|
|
33
|
+
| "PART_OF";
|
|
34
|
+
|
|
35
|
+
export interface ProjectRecallOverride {
|
|
36
|
+
limit?: number;
|
|
37
|
+
maxBytes?: number;
|
|
38
|
+
contextTypes?: MemoryType[];
|
|
39
|
+
expandRelations?: boolean;
|
|
40
|
+
expandEntities?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AutoMemConfig {
|
|
44
|
+
mcpServerName: string;
|
|
45
|
+
startupRecall: {
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
queries: string[];
|
|
48
|
+
tags: string[];
|
|
49
|
+
tagMode: "any" | "all";
|
|
50
|
+
limit: number;
|
|
51
|
+
maxBytes: number;
|
|
52
|
+
showStatus: boolean;
|
|
53
|
+
};
|
|
54
|
+
turnRecall: {
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
limit: number;
|
|
57
|
+
maxBytes: number;
|
|
58
|
+
contextTypes: MemoryType[];
|
|
59
|
+
expandRelations: boolean;
|
|
60
|
+
expandEntities: boolean;
|
|
61
|
+
};
|
|
62
|
+
projectDetection: {
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
tagPrefix: string;
|
|
65
|
+
folderTags: Record<string, string[]>;
|
|
66
|
+
gitRepoToTag: Record<string, string>;
|
|
67
|
+
};
|
|
68
|
+
projectOverrides?: Record<string, ProjectRecallOverride>;
|
|
69
|
+
writePolicy: {
|
|
70
|
+
mode: "off" | "propose" | "safe-auto" | "confirm-all";
|
|
71
|
+
autoWriteCategories: string[];
|
|
72
|
+
confirmCategories: string[];
|
|
73
|
+
blockedCategories: string[];
|
|
74
|
+
defaultSource: string;
|
|
75
|
+
machineTag: boolean;
|
|
76
|
+
alwaysTag: string[];
|
|
77
|
+
minImportanceToWrite: number;
|
|
78
|
+
dedupeBeforeWrite: boolean;
|
|
79
|
+
dedupeLimit: number;
|
|
80
|
+
};
|
|
81
|
+
behavior: {
|
|
82
|
+
injectSystemPrompt: boolean;
|
|
83
|
+
displayRecall: "full" | "summary" | "hidden";
|
|
84
|
+
maxContentLength: number;
|
|
85
|
+
preferredContentLength: number;
|
|
86
|
+
};
|
|
87
|
+
viewer: {
|
|
88
|
+
enabled: boolean;
|
|
89
|
+
mode: "standalone" | "embedded";
|
|
90
|
+
port: number;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Defaults
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export const DEFAULT_CONFIG: AutoMemConfig = {
|
|
99
|
+
mcpServerName: "automem",
|
|
100
|
+
startupRecall: {
|
|
101
|
+
enabled: true,
|
|
102
|
+
queries: ["user preferences working style environment"],
|
|
103
|
+
tags: [],
|
|
104
|
+
tagMode: "any",
|
|
105
|
+
limit: 8,
|
|
106
|
+
maxBytes: 6000,
|
|
107
|
+
showStatus: true,
|
|
108
|
+
},
|
|
109
|
+
turnRecall: {
|
|
110
|
+
enabled: true,
|
|
111
|
+
limit: 6,
|
|
112
|
+
maxBytes: 4000,
|
|
113
|
+
contextTypes: ["Preference", "Decision", "Pattern", "Insight", "Context"],
|
|
114
|
+
expandRelations: true,
|
|
115
|
+
expandEntities: true,
|
|
116
|
+
},
|
|
117
|
+
projectDetection: {
|
|
118
|
+
enabled: true,
|
|
119
|
+
tagPrefix: "project:",
|
|
120
|
+
folderTags: {},
|
|
121
|
+
gitRepoToTag: {},
|
|
122
|
+
},
|
|
123
|
+
writePolicy: {
|
|
124
|
+
mode: "safe-auto",
|
|
125
|
+
autoWriteCategories: ["technical-decision", "agent-pattern", "bug-fix", "tooling-lesson"],
|
|
126
|
+
confirmCategories: ["personal", "financial", "private", "identity"],
|
|
127
|
+
blockedCategories: ["secret", "credential", "api-key", "raw-transcript"],
|
|
128
|
+
defaultSource: "pi-session",
|
|
129
|
+
machineTag: true,
|
|
130
|
+
alwaysTag: ["source:pi"],
|
|
131
|
+
minImportanceToWrite: 0.7,
|
|
132
|
+
dedupeBeforeWrite: true,
|
|
133
|
+
dedupeLimit: 3,
|
|
134
|
+
},
|
|
135
|
+
behavior: {
|
|
136
|
+
injectSystemPrompt: true,
|
|
137
|
+
displayRecall: "summary",
|
|
138
|
+
maxContentLength: 2000,
|
|
139
|
+
preferredContentLength: 500,
|
|
140
|
+
},
|
|
141
|
+
viewer: {
|
|
142
|
+
enabled: false,
|
|
143
|
+
mode: "standalone",
|
|
144
|
+
port: 3000,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Config path resolution
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
export function resolveConfigPath(): string {
|
|
153
|
+
const envPath = process.env.AUTOMEM_CONFIG_PATH;
|
|
154
|
+
if (envPath) return resolve(envPath);
|
|
155
|
+
return resolve(homedir(), ".pi", "agent", "automem.json");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Env-var interpolation
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
export function resolveEnvVars(value: string): string {
|
|
163
|
+
return value.replace(/\$\{([^}]+)\}/g, function(_match: string, name: string) {
|
|
164
|
+
const v = process.env[name];
|
|
165
|
+
if (v === undefined) {
|
|
166
|
+
console.warn('[automem] env var "' + name + '" referenced in config but not set');
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
return v;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Deep merge
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
function deepMerge(base: any, override: any): any {
|
|
178
|
+
const result = Object.assign({}, base);
|
|
179
|
+
const keys = Object.keys(override);
|
|
180
|
+
for (let i = 0; i < keys.length; i++) {
|
|
181
|
+
const key = keys[i];
|
|
182
|
+
const bVal = (base as any)[key];
|
|
183
|
+
const oVal = override[key];
|
|
184
|
+
if (
|
|
185
|
+
oVal !== undefined &&
|
|
186
|
+
typeof oVal === "object" &&
|
|
187
|
+
oVal !== null &&
|
|
188
|
+
!Array.isArray(oVal) &&
|
|
189
|
+
typeof bVal === "object" &&
|
|
190
|
+
bVal !== null &&
|
|
191
|
+
!Array.isArray(bVal)
|
|
192
|
+
) {
|
|
193
|
+
(result as any)[key] = deepMerge(bVal, oVal);
|
|
194
|
+
} else if (oVal !== undefined) {
|
|
195
|
+
(result as any)[key] = oVal;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Load + validate
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
export function loadConfig(): AutoMemConfig {
|
|
206
|
+
const configPath = resolveConfigPath();
|
|
207
|
+
|
|
208
|
+
if (!existsSync(configPath)) {
|
|
209
|
+
console.log("[automem] no config at " + configPath + ", using defaults");
|
|
210
|
+
return DEFAULT_CONFIG;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let raw: any;
|
|
214
|
+
try {
|
|
215
|
+
const text = readFileSync(configPath, "utf8");
|
|
216
|
+
raw = JSON.parse(text);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error("[automem] failed to read/parse config: " + err);
|
|
219
|
+
return DEFAULT_CONFIG;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (typeof raw !== "object" || raw === null) {
|
|
223
|
+
console.error("[automem] config root must be an object, using defaults");
|
|
224
|
+
return DEFAULT_CONFIG;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const config = deepMerge(DEFAULT_CONFIG, raw) as AutoMemConfig;
|
|
228
|
+
|
|
229
|
+
if (config.startupRecall.limit < 1 || config.startupRecall.limit > 20) {
|
|
230
|
+
console.warn("[automem] startupRecall.limit out of range (1-20), clamping to 8");
|
|
231
|
+
config.startupRecall.limit = 8;
|
|
232
|
+
}
|
|
233
|
+
if (config.turnRecall.limit < 1 || config.turnRecall.limit > 20) {
|
|
234
|
+
console.warn("[automem] turnRecall.limit out of range (1-20), clamping to 6");
|
|
235
|
+
config.turnRecall.limit = 6;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const validDisplayModes = ["full", "summary", "hidden"];
|
|
239
|
+
if (!validDisplayModes.includes(config.behavior.displayRecall)) {
|
|
240
|
+
console.warn("[automem] unknown behavior.displayRecall \"" + config.behavior.displayRecall + "\", valid values: full, summary, hidden. Defaulting to \"summary\"");
|
|
241
|
+
config.behavior.displayRecall = "summary";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const validWriteModes = ["off", "propose", "safe-auto", "confirm-all"];
|
|
245
|
+
if (!validWriteModes.includes(config.writePolicy.mode)) {
|
|
246
|
+
console.warn("[automem] unknown writePolicy.mode \"" + config.writePolicy.mode + "\", valid values: off, propose, safe-auto, confirm-all. Defaulting to \"propose\"");
|
|
247
|
+
config.writePolicy.mode = "propose";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return config;
|
|
251
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-injector.ts - Build the context message injected into the session
|
|
3
|
+
* from recall results.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RecallResult } from "./recall";
|
|
7
|
+
import type { ProjectDetection } from "./project-detect";
|
|
8
|
+
|
|
9
|
+
export interface Injection {
|
|
10
|
+
message: string;
|
|
11
|
+
projectTag: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildContextMessage(
|
|
15
|
+
startupResult: RecallResult,
|
|
16
|
+
turnResult: RecallResult,
|
|
17
|
+
project: ProjectDetection,
|
|
18
|
+
): Injection | null {
|
|
19
|
+
const sections: string[] = [];
|
|
20
|
+
|
|
21
|
+
if (startupResult.text) {
|
|
22
|
+
sections.push(
|
|
23
|
+
"## AutoMem Startup Recall (" + startupResult.count + " memories)\n" + startupResult.text
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (turnResult.text) {
|
|
28
|
+
const projectLabel = project.projectLabel ? " [" + project.projectLabel + "]" : "";
|
|
29
|
+
sections.push(
|
|
30
|
+
"## AutoMem Turn Recall" + projectLabel + " (" + turnResult.count + " memories)\n" + turnResult.text
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (sections.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
message: sections.join("\n\n"),
|
|
40
|
+
projectTag: project.projectTag,
|
|
41
|
+
};
|
|
42
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.ts - AutoMem Core Extension entry point.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1: Recall-only core.
|
|
5
|
+
* - session_start: load config, health check, startup recall
|
|
6
|
+
* - before_agent_start: turn-level recall, inject context
|
|
7
|
+
* - /automem-status: health + memory count
|
|
8
|
+
* - /automem-recall: manual recall
|
|
9
|
+
* - status widget in footer
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { loadConfig } from "./config";
|
|
14
|
+
import { automemHealth, discoverTools, setAutoMemMcpServerName } from "./mcp-client";
|
|
15
|
+
import { startupRecall, turnRecall, type RecallResult } from "./recall";
|
|
16
|
+
import { detectProject } from "./project-detect";
|
|
17
|
+
import { buildContextMessage } from "./context-injector";
|
|
18
|
+
import { registerStatusCommand } from "./commands/status";
|
|
19
|
+
import { registerRecallCommand } from "./commands/recall";
|
|
20
|
+
import { registerMemoryTools } from "./tools/memory-tools";
|
|
21
|
+
import { registerRelationshipTools } from "./tools/relationship-tools";
|
|
22
|
+
|
|
23
|
+
export default function (pi: ExtensionAPI) {
|
|
24
|
+
let config = loadConfig();
|
|
25
|
+
setAutoMemMcpServerName(config.mcpServerName);
|
|
26
|
+
let autoMemHealthy = false;
|
|
27
|
+
let autoMemCount: number | undefined;
|
|
28
|
+
let startupInjected = false;
|
|
29
|
+
let startupResult: RecallResult = { text: "", count: 0, truncated: false };
|
|
30
|
+
|
|
31
|
+
// Register commands and Phase 2 explicit write tools
|
|
32
|
+
registerStatusCommand(pi);
|
|
33
|
+
registerRecallCommand(pi);
|
|
34
|
+
registerMemoryTools(pi);
|
|
35
|
+
registerRelationshipTools(pi);
|
|
36
|
+
|
|
37
|
+
// session_start - Load config, check health, run startup recall
|
|
38
|
+
pi.on("session_start", async function(_event: any, ctx: any) {
|
|
39
|
+
config = loadConfig();
|
|
40
|
+
setAutoMemMcpServerName(config.mcpServerName);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await discoverTools();
|
|
44
|
+
const health = await automemHealth();
|
|
45
|
+
autoMemHealthy = health.healthy;
|
|
46
|
+
autoMemCount = health.memoryCount;
|
|
47
|
+
|
|
48
|
+
if (health.healthy) {
|
|
49
|
+
const count = health.memoryCount != null ? " (" + health.memoryCount + ")" : "";
|
|
50
|
+
ctx.ui.notify("AutoMem: healthy" + count, "info");
|
|
51
|
+
} else {
|
|
52
|
+
ctx.ui.notify("AutoMem: unhealthy - " + (health.error || "unreachable"), "warning");
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
autoMemHealthy = false;
|
|
56
|
+
ctx.ui.notify("AutoMem health check failed: " + err, "warning");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (config.startupRecall.enabled && autoMemHealthy) {
|
|
60
|
+
try {
|
|
61
|
+
startupResult = await startupRecall(config);
|
|
62
|
+
if (startupResult.count > 0 && config.startupRecall.showStatus) {
|
|
63
|
+
ctx.ui.notify("AutoMem: recalled " + startupResult.count + " memories at startup", "info");
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
ctx.ui.notify("AutoMem startup recall failed: " + err, "warning");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
updateStatusWidget(ctx);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// before_agent_start - Turn-level recall + context injection
|
|
74
|
+
pi.on("before_agent_start", async function(event: any, ctx: any) {
|
|
75
|
+
if (!autoMemHealthy) return;
|
|
76
|
+
|
|
77
|
+
const prompt = event.prompt || "";
|
|
78
|
+
if (!prompt.trim()) return;
|
|
79
|
+
|
|
80
|
+
const project = detectProject(ctx.cwd, prompt, config);
|
|
81
|
+
|
|
82
|
+
let turnResult;
|
|
83
|
+
try {
|
|
84
|
+
turnResult = await turnRecall(prompt, project, config);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.warn("[automem] turn recall failed: " + err);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const startupForInjection = startupInjected
|
|
91
|
+
? { text: "", count: 0, truncated: false }
|
|
92
|
+
: startupResult;
|
|
93
|
+
|
|
94
|
+
if (turnResult.count === 0 && !startupForInjection.text) return;
|
|
95
|
+
|
|
96
|
+
const injection = buildContextMessage(
|
|
97
|
+
startupForInjection,
|
|
98
|
+
{ text: turnResult.text, count: turnResult.count, truncated: turnResult.truncated },
|
|
99
|
+
project,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (!injection) return;
|
|
103
|
+
|
|
104
|
+
if (startupForInjection.text) startupInjected = true;
|
|
105
|
+
|
|
106
|
+
const displayRecall = config.behavior.displayRecall || "summary";
|
|
107
|
+
const displayFull = displayRecall === "full";
|
|
108
|
+
|
|
109
|
+
if (displayRecall === "summary") {
|
|
110
|
+
const parts: string[] = [];
|
|
111
|
+
if (startupForInjection.count > 0) parts.push("startup " + startupForInjection.count);
|
|
112
|
+
if (turnResult.count > 0) parts.push("turn " + turnResult.count);
|
|
113
|
+
const projectPart = project.projectLabel ? " [" + project.projectLabel + "]" : "";
|
|
114
|
+
ctx.ui.notify("AutoMem recalled" + projectPart + ": " + parts.join(", "), "info");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (displayFull) {
|
|
118
|
+
return {
|
|
119
|
+
message: {
|
|
120
|
+
customType: "automem-recall",
|
|
121
|
+
content: injection.message,
|
|
122
|
+
display: true,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Hidden/summary mode: inject into the per-turn system prompt instead of
|
|
128
|
+
// returning a session message. `display: false` hides from the TUI, but API
|
|
129
|
+
// transcripts can still expose message injections as large context blocks.
|
|
130
|
+
return {
|
|
131
|
+
systemPrompt: (event.systemPrompt || "") + "\n\n" + injection.message,
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// session_shutdown - Cleanup
|
|
136
|
+
pi.on("session_shutdown", async function(_event: any, _ctx: any) {
|
|
137
|
+
autoMemHealthy = false;
|
|
138
|
+
autoMemCount = undefined;
|
|
139
|
+
startupInjected = false;
|
|
140
|
+
startupResult = { text: "", count: 0, truncated: false };
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
function updateStatusWidget(ctx: any) {
|
|
144
|
+
const theme = ctx.ui.theme;
|
|
145
|
+
if (autoMemHealthy) {
|
|
146
|
+
const count = autoMemCount != null ? " (" + autoMemCount + ")" : "";
|
|
147
|
+
ctx.ui.setStatus(
|
|
148
|
+
"automem",
|
|
149
|
+
theme.fg("success", "\u25CF") + theme.fg("dim", " AutoMem" + count),
|
|
150
|
+
);
|
|
151
|
+
} else {
|
|
152
|
+
ctx.ui.setStatus(
|
|
153
|
+
"automem",
|
|
154
|
+
theme.fg("error", "\u25CF") + theme.fg("dim", " AutoMem (offline)"),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|