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/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
+ }