import { createHash } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, join, normalize, resolve } from "node:path"; import { Type, type Model, type UserMessage } from "@earendil-works/pi-ai"; import type { ContextEvent, ExtensionAPI, ExtensionCommandContext, ExtensionContext, InputEvent, ToolDefinition, TurnEndEvent, } from "@earendil-works/pi-coding-agent"; const DEFAULT_API_BASE_URL = "https://api.supermemory.ai"; const DEFAULT_CONTAINER_TAG = "pi-supermemory"; const DEFAULT_MAX_RECALL = 5; const EXTENSION_SOURCE = "pi-supermemory"; const MAX_MEMORY_CONTENT_CHARS = 10_000; const CHUNK_PAYLOAD_CHARS = 9_000; const CHUNK_OVERLAP_CHARS = 500; const EMPTY_SCHEMA = Type.Object({}, { additionalProperties: false }); const SEARCH_SCHEMA = Type.Object( { query: Type.String({ description: "Query to search in Supermemory." }), limit: Type.Optional(Type.Number({ description: "Maximum memories to return." })), }, { additionalProperties: false }, ); const SAVE_SCHEMA = Type.Object( { content: Type.String({ description: "Memory content to save." }), is_static: Type.Optional(Type.Boolean({ description: "Whether Supermemory should treat this as static memory." })), }, { additionalProperties: false }, ); const SAVE_FILE_SCHEMA = Type.Object( { path: Type.String({ description: "Path to the file to save into Supermemory." }), container_tag: Type.Optional(Type.String({ description: "Optional container tag to save to. Uses the configured container if omitted." })), is_static: Type.Optional(Type.Boolean({ description: "Whether Supermemory should treat this as static memory." })), }, { additionalProperties: false }, ); type SearchParams = { query: string; limit?: number; }; type SaveParams = { content: string; is_static?: boolean; }; type SaveFileParams = { path: string; container_tag?: string; is_static?: boolean; }; type TextToolResult = { content: Array<{ type: "text"; text: string }>; details: TDetails; }; export type PiSupermemoryOptions = { apiKey?: string; apiBaseUrl?: string; containerTag?: string; configPath?: string; commandName?: string; toolNamePrefix?: string; enabled?: boolean; maxRecall?: number; autoRecall?: boolean; autoCapture?: boolean; captureMode?: CaptureMode; client?: SupermemoryClient; clock?: () => number; }; export type SupermemorySearchResult = { id?: string; content: string; score?: number; metadata?: Record; }; export type SupermemorySaveResult = { ok: boolean; status: number; id?: string; ids?: string[]; chunks?: number; response?: unknown; }; export type SupermemoryDocumentResult = { ok: boolean; status: number; id?: string; customId: string; title: string; response?: unknown; }; export interface SupermemoryClient { search(query: string, options?: { limit?: number }): Promise; save(content: string, options?: { isStatic?: boolean; metadata?: Record }): Promise; addDocument?(content: string, options: { customId: string; title: string; metadata?: Record; }): Promise; } type Config = { enabled: boolean; apiKey: string | undefined; apiBaseUrl: string; containerTag: string; commandName: string; toolNamePrefix: string; maxRecall: number; autoRecall: boolean; autoCapture: boolean; captureMode: CaptureMode; permissions: Permission | undefined; clock: () => number; configPath: string | undefined; matchedDirectory: string | undefined; matchedModel: string | undefined; matchedRule: string | undefined; }; export type RuntimeConfig = Omit; export type Permission = "read-only" | "write-only" | "read-write"; export type CaptureMode = "signal" | "all"; export type ConfigOverride = { enabled?: boolean; apiKey?: string; apiBaseUrl?: string; containerTag?: string; maxRecall?: number; autoRecall?: boolean; autoCapture?: boolean; captureMode?: CaptureMode; permissions?: Permission; }; export type DirectoryOverride = ConfigOverride & { path?: string; }; export type Rule = { path: string; modelPattern?: string; containerTag?: string; enabled?: boolean; apiKey?: string; apiBaseUrl?: string; maxRecall?: number; autoRecall?: boolean; autoCapture?: boolean; captureMode?: CaptureMode; permissions?: Permission; }; export type SupermemoryConfigFile = { default?: ConfigOverride; directories?: Record | DirectoryOverride[]; models?: Record; rules?: Rule[]; }; type TextishMessage = { role?: string; content?: unknown; timestamp?: number; [key: string]: unknown; }; type SupermemoryApiMemory = { id?: string; content?: string; text?: string; score?: number; similarity?: number; metadata?: Record; document?: string | { id?: string; content?: string; text?: string; metadata?: Record; }; memory?: string | { id?: string; content?: string; text?: string; metadata?: Record; }; }; type SupermemorySearchResponse = { results?: SupermemoryApiMemory[]; memories?: SupermemoryApiMemory[]; documents?: SupermemoryApiMemory[]; data?: SupermemoryApiMemory[] | { results?: SupermemoryApiMemory[]; memories?: SupermemoryApiMemory[] }; }; type SupermemoryMemoryPayload = { content: string; isStatic: boolean; metadata: Record; }; export class SupermemoryHttpClient implements SupermemoryClient { readonly #apiKey: string; readonly #apiBaseUrl: string; readonly #containerTag: string; readonly #fetch: typeof fetch; constructor(options: { apiKey: string; apiBaseUrl?: string; containerTag?: string; fetchImpl?: typeof fetch }) { this.#apiKey = options.apiKey; this.#apiBaseUrl = trimTrailingSlash(options.apiBaseUrl ?? DEFAULT_API_BASE_URL); this.#containerTag = options.containerTag ?? DEFAULT_CONTAINER_TAG; this.#fetch = options.fetchImpl ?? fetch; } async search(query: string, options: { limit?: number } = {}): Promise { const response = await this.#request("/v4/search", { q: query, containerTag: this.#containerTag, limit: options.limit ?? DEFAULT_MAX_RECALL, searchMode: "hybrid", }); return normalizeSearchResponse(response); } async save( content: string, options: { isStatic?: boolean; metadata?: Record } = {}, ): Promise { const memories = createMemoryPayloads(content, { isStatic: options.isStatic ?? false, metadata: { sm_source: EXTENSION_SOURCE, ...options.metadata, }, }); const responses: unknown[] = []; let status = 0; const ids: string[] = []; for (let index = 0; index < memories.length; index += 100) { const batch = memories.slice(index, index + 100); const response = await this.#fetch(`${this.#apiBaseUrl}/v4/memories`, { method: "POST", headers: this.#headers(), body: JSON.stringify({ containerTag: this.#containerTag, memories: batch, }), }); status = response.status; const body = await readJson(response); if (!response.ok) { throw new Error(`Supermemory save failed with HTTP ${response.status}: ${formatSupermemoryError(body)}`); } responses.push(body); ids.push(...extractIds(body)); } return { ok: true, status, ...(ids[0] ? { id: ids[0] } : {}), ...(ids.length > 0 ? { ids } : {}), ...(memories.length > 1 ? { chunks: memories.length } : {}), response: responses.length === 1 ? responses[0] : responses, }; } async addDocument(content: string, options: { customId: string; title: string; metadata?: Record; }): Promise { const response = await this.#fetch(`${this.#apiBaseUrl}/v3/documents`, { method: "POST", headers: this.#headers(), body: JSON.stringify({ content, containerTag: this.#containerTag, customId: options.customId, metadata: { title: options.title, source: EXTENSION_SOURCE, ...options.metadata, }, }), }); const body = await readJson(response); if (!response.ok) { throw new Error(`Supermemory document ingest failed with HTTP ${response.status}: ${formatSupermemoryError(body)}`); } return { ok: true, status: response.status, ...(isRecord(body) && typeof body.id === "string" ? { id: body.id } : {}), customId: options.customId, title: options.title, response: body, }; } async #request(path: string, body: Record): Promise { const response = await this.#fetch(`${this.#apiBaseUrl}${path}`, { method: "POST", headers: this.#headers(), body: JSON.stringify(body), }); const json = await readJson(response); if (!response.ok) { throw new Error(`Supermemory request failed with HTTP ${response.status}: ${JSON.stringify(json)}`); } return json as TResponse; } #headers(): Record { return { Authorization: `Bearer ${this.#apiKey}`, "Content-Type": "application/json", }; } } export function createSupermemoryExtension(options: PiSupermemoryOptions = {}) { const baseConfig = resolveBaseConfig(options); const explicitConfigPath = baseConfig.configPath; const explicitPolicy = explicitConfigPath ? loadConfigFile(explicitConfigPath) : undefined; function getPolicy(cwd: string | undefined): SupermemoryConfigFile | undefined { if (explicitConfigPath !== undefined) return explicitPolicy; if (!cwd) return undefined; return loadMergedPolicyForCwd(cwd); } let latestUserInput = ""; let latestSavedFingerprint = ""; return { register(pi: ExtensionAPI): void { const searchTool: ToolDefinition = { name: `${baseConfig.toolNamePrefix}supermemory_search`, label: "Supermemory Search", description: "Search the active Supermemory container.", parameters: SEARCH_SCHEMA, async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const config = resolveRuntimeConfig(baseConfig, getPolicy(ctx?.cwd), ctx); const client = clientForConfig(config, options.client); if (!config.enabled) return makeTextResult({ error: "Supermemory is disabled by configuration." }); if (!canRead(config)) return makeTextResult({ error: "Supermemory search is disabled by configuration (write-only)." }); if (!client) return makeTextResult({ error: "Supermemory is not configured. Set SUPERMEMORY_API_KEY." }); const results = await client.search(params.query, { limit: clampLimit(params.limit, config.maxRecall) }); return makeTextResult({ query: params.query, results }); }, }; pi.registerTool(searchTool); const saveTool: ToolDefinition = { name: `${baseConfig.toolNamePrefix}supermemory_save`, label: "Supermemory Save", description: "Save a durable memory into the active Supermemory container.", parameters: SAVE_SCHEMA, async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const config = resolveRuntimeConfig(baseConfig, getPolicy(ctx?.cwd), ctx); const client = clientForConfig(config, options.client); if (!config.enabled) return makeTextResult({ error: "Supermemory is disabled by configuration." }); if (!canWrite(config)) return makeTextResult({ error: "Supermemory save is disabled by configuration (read-only)." }); if (!client) return makeTextResult({ error: "Supermemory is not configured. Set SUPERMEMORY_API_KEY." }); return makeTextResult(await safeSave(client, params.content, { isStatic: params.is_static ?? false, metadata: memoryMetadata(config, "manual_tool"), })); }, }; pi.registerTool(saveTool); const saveFileTool: ToolDefinition = { name: `${baseConfig.toolNamePrefix}supermemory_save_file`, label: "Supermemory Save File", description: "Save the contents of a file into Supermemory.", parameters: SAVE_FILE_SCHEMA, async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const config = resolveRuntimeConfig(baseConfig, getPolicy(ctx?.cwd), ctx); if (!config.enabled) return makeTextResult({ error: "Supermemory is disabled by configuration." }); if (!canWrite(config)) return makeTextResult({ error: "Supermemory save is disabled by configuration (read-only)." }); if (!config.apiKey) return makeTextResult({ error: "Supermemory is not configured. Set SUPERMEMORY_API_KEY." }); let content: string; try { content = readFileSync(params.path, "utf8"); } catch (err) { return makeTextResult({ error: `Failed to read file "${params.path}": ${(err as Error).message}` }); } const client = new SupermemoryHttpClient({ apiKey: config.apiKey, apiBaseUrl: config.apiBaseUrl, containerTag: params.container_tag ?? config.containerTag, }); return makeTextResult(await safeSave(client, content, { isStatic: params.is_static ?? false, metadata: memoryMetadata(config, "manual_tool"), })); }, }; pi.registerTool(saveFileTool); const statusTool: ToolDefinition, unknown> = { name: `${baseConfig.toolNamePrefix}supermemory_status`, label: "Supermemory Status", description: "Show Supermemory configuration for the Pi extension.", parameters: EMPTY_SCHEMA, async execute(_toolCallId, _params, _signal, _onUpdate, ctx) { const config = resolveRuntimeConfig(baseConfig, getPolicy(ctx?.cwd), ctx); return makeTextResult(statusPayload(config, Boolean(clientForConfig(config, options.client)))); }, }; pi.registerTool(statusTool); pi.registerCommand(baseConfig.commandName, { description: "Search, save, or inspect Supermemory.", handler: async (argumentString: string, ctx: ExtensionCommandContext) => { const config = resolveRuntimeConfig(baseConfig, getPolicy(ctx.cwd), ctx); const client = clientForConfig(config, options.client); const args = argumentString.trim().split(/\s+/).filter(Boolean); const [action, ...rest] = args; if (!action || action === "status") { await notify(ctx, JSON.stringify(statusPayload(config, Boolean(client)), null, 2)); return; } if (!config.enabled) { await notify(ctx, "Supermemory is disabled by configuration."); return; } if (!client) { await notify(ctx, "Supermemory is not configured. Set SUPERMEMORY_API_KEY."); return; } if (action === "search") { if (!canRead(config)) { await notify(ctx, "Supermemory search is disabled by configuration (write-only)."); return; } const query = rest.join(" ").trim(); if (!query) { await notify(ctx, `Usage: /${config.commandName} search `); return; } const results = await client.search(query, { limit: config.maxRecall }); await notify(ctx, formatSearchResults(results)); return; } if (action === "save") { if (!canWrite(config)) { await notify(ctx, "Supermemory save is disabled by configuration (read-only)."); return; } const content = rest.join(" ").trim(); if (!content) { await notify(ctx, `Usage: /${config.commandName} save `); return; } const result = await safeSave(client, content, { metadata: memoryMetadata(config, "manual_command") }); if ("error" in result) { await notify(ctx, result.error, "error"); return; } await notify(ctx, savedMessage(config.containerTag, result)); return; } if (action === "save-file") { if (!canWrite(config)) { await notify(ctx, "Supermemory save is disabled by configuration (read-only)."); return; } const [filePath, overrideContainerTag] = rest; if (!filePath) { await notify(ctx, `Usage: /${config.commandName} save-file [containerTag]`); return; } let content: string; try { content = readFileSync(filePath, "utf8"); } catch (err) { await notify(ctx, `Failed to read file "${filePath}": ${(err as Error).message}`); return; } const targetContainer = overrideContainerTag ?? config.containerTag; const saveClient = new SupermemoryHttpClient({ apiKey: config.apiKey!, apiBaseUrl: config.apiBaseUrl, containerTag: targetContainer, }); const result = await safeSave(saveClient, content, { metadata: memoryMetadata(config, "manual_command") }); if ("error" in result) { await notify(ctx, result.error, "error"); return; } await notify(ctx, `Saved "${filePath}" to Supermemory container "${targetContainer}"${result.chunks ? ` as ${result.chunks} chunks` : ""}.`); return; } await notify(ctx, `Unknown /${config.commandName} action "${action}". Try status, search, save, or save-file.`); }, }); pi.on("input", (event: InputEvent) => { if (event.source === "extension") return; const input = cleanCaptureText(extractText(event)); if (input) latestUserInput = input; }); pi.on("context", async (event: ContextEvent, ctx) => { const config = resolveRuntimeConfig(baseConfig, getPolicy(ctx?.cwd), ctx); const client = clientForConfig(config, options.client); if (!client || !config.autoRecall || !canRead(config) || !latestUserInput.trim()) return { messages: event.messages }; const results = await client.search(latestUserInput, { limit: config.maxRecall }); if (results.length === 0) return { messages: event.messages }; const recallMessage: UserMessage = { role: "user", content: `Relevant Supermemory context from "${config.containerTag}":\n\n${formatSearchResults(results)}`, timestamp: config.clock(), }; return { messages: [recallMessage, ...event.messages] }; }); pi.on("turn_end", async (event: TurnEndEvent, ctx) => { const config = resolveRuntimeConfig(baseConfig, getPolicy(ctx?.cwd), ctx); const client = clientForConfig(config, options.client); if (!client || !config.autoCapture || !canWrite(config)) return; const assistantText = cleanCaptureText(extractText(event.message)); const userText = latestUserInput.trim(); if (!userText || !assistantText.trim()) return; if (!shouldCaptureTurn(config.captureMode, userText, assistantText)) return; const content = `Pi coding-agent turn\n\nUser:\n${userText}\n\nAssistant:\n${assistantText.trim()}`; const fingerprint = `${userText}\n---\n${assistantText.trim()}`; if (fingerprint === latestSavedFingerprint) return; latestSavedFingerprint = fingerprint; const capturedAt = new Date(config.clock()).toISOString(); const title = turnTitle(userText); const result = await safeCaptureTurn(client, content, { customId: turnCustomId(capturedAt, fingerprint), title, metadata: documentMetadata(config, ctx, { capturedAt, title }), fallbackMetadata: memoryMetadata(config, "turn_end", { capturedAt, title }), }); if ("error" in result) { await notify(ctx, `Supermemory auto-capture skipped: ${result.error}`, "warning"); } }); }, }; } export function createMemoryPayloads( content: string, options: { isStatic?: boolean; metadata?: Record } = {}, ): SupermemoryMemoryPayload[] { const metadata = options.metadata ?? {}; if (content.length <= MAX_MEMORY_CONTENT_CHARS) { return [{ content, isStatic: options.isStatic ?? false, metadata }]; } const chunks = splitWithOverlap(content, CHUNK_PAYLOAD_CHARS, CHUNK_OVERLAP_CHARS); const groupId = `memory-${typeof metadata.captured_at === "string" ? metadata.captured_at : new Date().toISOString()}`; return chunks.map((chunk, index) => { const chunkNumber = index + 1; const header = [ `MEMORY ${groupId} ${chunkNumber}/${chunks.length}`, `Part ${chunkNumber} of ${chunks.length}; ${CHUNK_OVERLAP_CHARS} character overlap where possible.`, "", ].join("\n"); return { content: `${header}${chunk}`, isStatic: options.isStatic ?? false, metadata: { ...metadata, chunk_group: groupId, chunk_index: chunkNumber, chunk_total: chunks.length, chunk_overlap_chars: CHUNK_OVERLAP_CHARS, original_content_chars: content.length, }, }; }); } function splitWithOverlap(content: string, chunkSize: number, overlap: number): string[] { const chunks: string[] = []; let start = 0; while (start < content.length) { const end = Math.min(content.length, start + chunkSize); chunks.push(content.slice(start, end)); if (end === content.length) break; start = Math.max(0, end - overlap); } return chunks; } async function safeSave( client: SupermemoryClient, content: string, options: { isStatic?: boolean; metadata?: Record } = {}, ): Promise { try { return await client.save(content, options); } catch (err) { return { error: conciseError(err) }; } } async function safeCaptureTurn( client: SupermemoryClient, content: string, options: { customId: string; title: string; metadata: Record; fallbackMetadata: Record; }, ): Promise { try { if (client.addDocument) { return await client.addDocument(content, { customId: options.customId, title: options.title, metadata: options.metadata, }); } return await client.save(content, { metadata: options.fallbackMetadata }); } catch (err) { return { error: conciseError(err) }; } } export function resolveSupermemoryConfig(input: { base?: Partial; policy?: SupermemoryConfigFile; cwd?: string; model?: Model | { id?: string; name?: string; provider?: string; api?: string } | undefined; }): RuntimeConfig { const base: RuntimeConfig = { enabled: input.base?.enabled ?? true, apiKey: input.base?.apiKey, apiBaseUrl: input.base?.apiBaseUrl ?? DEFAULT_API_BASE_URL, containerTag: input.base?.containerTag ?? DEFAULT_CONTAINER_TAG, commandName: input.base?.commandName ?? "supermemory", toolNamePrefix: input.base?.toolNamePrefix ?? "", maxRecall: input.base?.maxRecall ?? DEFAULT_MAX_RECALL, autoRecall: input.base?.autoRecall ?? true, autoCapture: input.base?.autoCapture ?? true, captureMode: input.base?.captureMode ?? "signal", permissions: input.base?.permissions, configPath: input.base?.configPath, matchedDirectory: undefined, matchedModel: undefined, matchedRule: undefined, }; const withDefault = mergeOverride(base, input.policy?.default); const directoryMatch = findDirectoryOverride(input.policy?.directories, input.cwd); const withDirectory = mergeOverride(withDefault, directoryMatch?.override); const modelMatch = findModelOverride(input.policy?.models, input.model); const withModel = mergeOverride(withDirectory, modelMatch?.override); const ruleMatch = findRuleOverride(input.policy?.rules, input.cwd, input.model); const withRule = mergeOverride(withModel, ruleMatch?.override); return { ...withRule, matchedDirectory: directoryMatch?.path, matchedModel: modelMatch?.key, matchedRule: ruleMatch ? `${ruleMatch.path}${ruleMatch.override.permissions ? ` [${ruleMatch.override.permissions}]` : ""}` : undefined, }; } function resolveBaseConfig(options: PiSupermemoryOptions): Config { return { enabled: options.enabled ?? parseBoolean(process.env.PI_SUPERMEMORY_ENABLED, true), apiKey: options.apiKey ?? process.env.SUPERMEMORY_API_KEY ?? process.env.SUPERMEMORY_CC_API_KEY ?? process.env.SUPERMEMORY_OPENCLAW_API_KEY, apiBaseUrl: trimTrailingSlash(options.apiBaseUrl ?? process.env.SUPERMEMORY_API_BASE_URL ?? DEFAULT_API_BASE_URL), containerTag: options.containerTag ?? process.env.PI_SUPERMEMORY_CONTAINER_TAG ?? process.env.SUPERMEMORY_CONTAINER_TAG ?? DEFAULT_CONTAINER_TAG, commandName: options.commandName ?? "supermemory", toolNamePrefix: options.toolNamePrefix ?? "", maxRecall: options.maxRecall ?? parsePositiveInteger(process.env.PI_SUPERMEMORY_MAX_RECALL, DEFAULT_MAX_RECALL), autoRecall: options.autoRecall ?? parseBoolean(process.env.PI_SUPERMEMORY_AUTO_RECALL, true), autoCapture: options.autoCapture ?? parseBoolean(process.env.PI_SUPERMEMORY_AUTO_CAPTURE, true), captureMode: options.captureMode ?? parseCaptureMode(process.env.PI_SUPERMEMORY_CAPTURE_MODE, "signal"), permissions: undefined, clock: options.clock ?? Date.now, configPath: options.configPath ?? process.env.PI_SUPERMEMORY_CONFIG ?? undefined, matchedDirectory: undefined, matchedModel: undefined, matchedRule: undefined, }; } function resolveRuntimeConfig(baseConfig: Config, policy: SupermemoryConfigFile | undefined, ctx?: Partial): Config { const resolved = resolveSupermemoryConfig({ base: baseConfig, ...(policy === undefined ? {} : { policy }), ...(ctx?.cwd === undefined ? {} : { cwd: ctx.cwd }), ...(ctx?.model === undefined ? {} : { model: ctx.model }), }); return { ...resolved, clock: baseConfig.clock }; } function clientForConfig(config: Config | RuntimeConfig, injected: SupermemoryClient | undefined): SupermemoryClient | undefined { if (!config.enabled) return undefined; if (injected) return injected; if (!config.apiKey) return undefined; return new SupermemoryHttpClient({ apiKey: config.apiKey, apiBaseUrl: config.apiBaseUrl, containerTag: config.containerTag }); } function loadConfigFile(configPath: string): SupermemoryConfigFile | undefined { if (!existsSync(configPath)) return undefined; const raw = readFileSync(configPath, "utf8"); const parsed = JSON.parse(raw) as unknown; if (!isRecord(parsed)) { throw new Error(`Pi Supermemory config must be a JSON object: ${configPath}`); } return parsed as SupermemoryConfigFile; } function mergeOverride(config: RuntimeConfig, override: ConfigOverride | undefined): RuntimeConfig { if (!override) return config; const result: RuntimeConfig = { ...config }; if (override.enabled !== undefined) result.enabled = override.enabled; if (override.apiKey !== undefined) result.apiKey = override.apiKey; if (override.apiBaseUrl !== undefined) result.apiBaseUrl = trimTrailingSlash(override.apiBaseUrl); if (override.containerTag !== undefined) result.containerTag = override.containerTag; if (override.maxRecall !== undefined) result.maxRecall = override.maxRecall; if (override.autoRecall !== undefined) result.autoRecall = override.autoRecall; if (override.autoCapture !== undefined) result.autoCapture = override.autoCapture; if (override.captureMode !== undefined) result.captureMode = override.captureMode; if (override.permissions !== undefined) result.permissions = override.permissions; return result; } function findDirectoryOverride( directories: SupermemoryConfigFile["directories"] | undefined, cwd: string | undefined, ): { path: string; override: ConfigOverride } | undefined { if (!directories || !cwd) return undefined; const cwdPath = normalize(resolve(cwd)); const entries = Array.isArray(directories) ? directories.flatMap((entry) => { if (!entry.path) return []; const { path, ...override } = entry; return [{ path, override }]; }) : Object.entries(directories).map(([path, override]) => ({ path, override })); return entries .map((entry) => ({ path: normalize(resolve(entry.path)), override: entry.override })) .filter((entry) => isDirectoryMatch(cwdPath, entry.path)) .sort((a, b) => b.path.length - a.path.length)[0]; } function isDirectoryMatch(cwd: string, candidate: string): boolean { return cwd === candidate || cwd.startsWith(`${candidate}/`); } function findModelOverride( models: SupermemoryConfigFile["models"] | undefined, model: Model | { id?: string; name?: string; provider?: string; api?: string } | undefined, ): { key: string; override: ConfigOverride } | undefined { if (!models || !model) return undefined; const candidates = modelMatchKeys(model); for (const key of candidates) { const override = models[key]; if (override) return { key, override }; } return undefined; } function modelMatchKeys(model: Model | { id?: string; name?: string; provider?: string; api?: string }): string[] { const id = model.id; const name = model.name; const provider = model.provider; const api = model.api; return uniqueStrings([ id, name, provider && id ? `${provider}/${id}` : undefined, provider && name ? `${provider}/${name}` : undefined, provider && id ? `${provider}:${id}` : undefined, provider && name ? `${provider}:${name}` : undefined, api && id ? `${api}/${id}` : undefined, ]); } function uniqueStrings(values: Array): string[] { return [...new Set(values.filter((value): value is string => Boolean(value)))]; } const policyCache = new Map(); function discoverConfigFiles(cwd: string): string[] { const levels: string[][] = []; let current = resolve(cwd); const homeDir = homedir(); while (true) { const filesAtLevel: string[] = []; const supermemoryPath = join(current, ".pi", "supermemory.json"); const piSupermemoryPath = join(current, ".pi", "pi-supermemory.json"); const agentPath = join(current, ".pi", "agent", "pi-supermemory.json"); if (existsSync(supermemoryPath)) filesAtLevel.push(supermemoryPath); if (existsSync(piSupermemoryPath)) filesAtLevel.push(piSupermemoryPath); if (existsSync(agentPath)) filesAtLevel.push(agentPath); if (filesAtLevel.length > 0) { levels.push(filesAtLevel); } if (current === homeDir) break; const parent = dirname(current); if (parent === current) break; current = parent; } const homeFiles: string[] = []; const homeSupermemory = join(homeDir, ".pi", "supermemory.json"); const homePiSupermemory = join(homeDir, ".pi", "pi-supermemory.json"); const homeAgent = join(homeDir, ".pi", "agent", "pi-supermemory.json"); if (existsSync(homeSupermemory)) homeFiles.push(homeSupermemory); if (existsSync(homePiSupermemory)) homeFiles.push(homePiSupermemory); if (existsSync(homeAgent)) homeFiles.push(homeAgent); if (homeFiles.length > 0 && !levels.some((level) => level.some((f) => f === homeSupermemory || f === homePiSupermemory || f === homeAgent))) { levels.push(homeFiles); } return levels.reverse().flat(); } export function loadMergedPolicyForCwd(cwd: string): SupermemoryConfigFile | undefined { const cached = policyCache.get(cwd); if (cached !== undefined || policyCache.has(cwd)) return cached; const files = discoverConfigFiles(cwd); if (files.length === 0) { policyCache.set(cwd, undefined); return undefined; } let merged: SupermemoryConfigFile = {}; for (const file of files) { const policy = loadConfigFile(file); if (!policy) continue; merged = mergePolicies(merged, policy); } policyCache.set(cwd, merged); return merged; } function mergePolicies(parent: SupermemoryConfigFile, child: SupermemoryConfigFile): SupermemoryConfigFile { const merged: SupermemoryConfigFile = {}; if (child.default) { merged.default = { ...(parent.default ?? {}), ...child.default }; } else if (parent.default) { merged.default = parent.default; } const directories = mergeDirectoryMaps(parent.directories, child.directories); if (directories !== undefined) merged.directories = directories; const models = mergeModelMaps(parent.models, child.models); if (models !== undefined) merged.models = models; const rules = [...(child.rules ?? []), ...(parent.rules ?? [])]; if (rules.length > 0) merged.rules = rules; return merged; } function mergeDirectoryMaps( parent: SupermemoryConfigFile["directories"], child: SupermemoryConfigFile["directories"], ): SupermemoryConfigFile["directories"] { if (!child) return parent; if (!parent) return child; const toRecord = (d: SupermemoryConfigFile["directories"]): Record => { if (!d) return {}; if (Array.isArray(d)) { return Object.fromEntries( d .filter((e): e is DirectoryOverride & { path: string } => Boolean(e.path)) .map((e) => { const { path, ...override } = e; return [path, override]; }), ); } return d; }; return { ...toRecord(parent), ...toRecord(child) }; } function mergeModelMaps( parent: SupermemoryConfigFile["models"], child: SupermemoryConfigFile["models"], ): SupermemoryConfigFile["models"] { if (!child) return parent; if (!parent) return child; return { ...parent, ...child }; } function findRuleOverride( rules: Rule[] | undefined, cwd: string | undefined, model: Model | { id?: string; name?: string; provider?: string; api?: string } | undefined, ): { path: string; override: ConfigOverride } | undefined { if (!rules || !cwd) return undefined; const cwdPath = normalize(resolve(cwd)); const modelKeys = model ? modelMatchKeys(model) : []; const matches = rules .map((rule, index) => ({ rule, index, pathLength: normalize(resolve(rule.path)).length })) .filter(({ rule }) => { const rulePath = normalize(resolve(rule.path)); if (!isDirectoryMatch(cwdPath, rulePath)) return false; if (!rule.modelPattern) return true; if (modelKeys.length === 0) return false; try { const regex = new RegExp(rule.modelPattern); return modelKeys.some((key) => regex.test(key)); } catch { return false; } }) .sort((a, b) => { if (b.pathLength !== a.pathLength) return b.pathLength - a.pathLength; return a.index - b.index; }); if (matches.length === 0) return undefined; const match = matches[0]; if (!match) return undefined; const winner = match.rule; const override: ConfigOverride = {}; if (winner.containerTag !== undefined) override.containerTag = winner.containerTag; if (winner.enabled !== undefined) override.enabled = winner.enabled; if (winner.apiKey !== undefined) override.apiKey = winner.apiKey; if (winner.apiBaseUrl !== undefined) override.apiBaseUrl = winner.apiBaseUrl; if (winner.maxRecall !== undefined) override.maxRecall = winner.maxRecall; if (winner.permissions !== undefined) { switch (winner.permissions) { case "read-only": override.autoRecall = true; override.autoCapture = false; break; case "write-only": override.autoRecall = false; override.autoCapture = true; break; case "read-write": override.autoRecall = true; override.autoCapture = true; break; } override.permissions = winner.permissions; } else { if (winner.autoRecall !== undefined) override.autoRecall = winner.autoRecall; if (winner.autoCapture !== undefined) override.autoCapture = winner.autoCapture; } if (winner.captureMode !== undefined) override.captureMode = winner.captureMode; return { path: winner.path, override }; } function canRead(config: Config | RuntimeConfig): boolean { if (!config.enabled) return false; if (config.permissions === "write-only") return false; return true; } function canWrite(config: Config | RuntimeConfig): boolean { if (!config.enabled) return false; if (config.permissions === "read-only") return false; return true; } function normalizeSearchResponse(response: SupermemorySearchResponse): SupermemorySearchResult[] { const candidates = Array.isArray(response.results) ? response.results : Array.isArray(response.memories) ? response.memories : Array.isArray(response.documents) ? response.documents : Array.isArray(response.data) ? response.data : Array.isArray(response.data?.results) ? response.data.results : Array.isArray(response.data?.memories) ? response.data.memories : []; return candidates.flatMap((candidate) => { const nested = candidate.memory ?? candidate.document; const content = typeof nested === "string" ? nested : (nested?.content ?? nested?.text ?? candidate.content ?? candidate.text); if (!content) return []; const id = typeof nested === "string" ? candidate.id : (nested?.id ?? candidate.id); const score = candidate.score ?? candidate.similarity; const metadata = typeof nested === "string" ? candidate.metadata : (nested?.metadata ?? candidate.metadata); return [ { ...(id ? { id } : {}), content, ...(score === undefined ? {} : { score }), ...(metadata ? { metadata } : {}), }, ]; }); } function formatSearchResults(results: SupermemorySearchResult[]): string { if (results.length === 0) return "No Supermemory results found."; return results .map((result, index) => { const score = typeof result.score === "number" ? ` score=${result.score.toFixed(3)}` : ""; return `${index + 1}. ${result.content}${score}`; }) .join("\n\n"); } function extractText(value: unknown): string { if (typeof value === "string") return value; if (!isRecord(value)) return ""; const message = value as TextishMessage; const content = message.content; if (typeof content === "string") return content; if (Array.isArray(content)) { return content .flatMap((part) => { if (typeof part === "string") return [part]; if (isRecord(part) && typeof part.text === "string") return [part.text]; return []; }) .join("\n") .trim(); } if (typeof message.text === "string") return message.text; if (typeof message.input === "string") return message.input; if (Array.isArray(message.contentParts)) return extractText({ content: message.contentParts }); return ""; } function cleanCaptureText(text: string): string { return stripInjectedSupermemoryContext(text) .replace(/[\s\S]*?<\/supermemory-context>\s*/g, "") .replace(/[\s\S]*?<\/supermemory-containers>\s*/g, "") .replace(/[\s\S]*?<\/system-reminder>\s*/g, "") .trim(); } function stripInjectedSupermemoryContext(text: string): string { const trimmed = text.trimStart(); if (!trimmed.startsWith("Relevant Supermemory context from")) return text; const blocks = trimmed.split(/\n{2,}/); if (blocks.length <= 1) return ""; let index = 1; while (index < blocks.length && /^\d+\.\s/.test(blocks[index]?.trim() ?? "")) index += 1; return blocks.slice(index).join("\n\n"); } function shouldCaptureTurn(captureMode: CaptureMode, userText: string, assistantText: string): boolean { if (captureMode === "all") return true; const user = userText.toLowerCase(); const assistant = assistantText.toLowerCase(); if (/\b(?:remember|memorize|save this|save that|note this|note that|don't forget|dont forget|keep in mind|for future reference|commit to memory)\b/.test(user)) { return true; } if (/\b(?:i prefer|i always|i usually|we decided|we use|this project uses|should use|canonical|preference|decision)\b/.test(user)) { return true; } if (/\b(?:changes summary|file modifications|verification steps|implemented|fixed|recorded|captured|research finding)\b/.test(assistant)) { return true; } return false; } function memoryMetadata(config: Config, captureMode: string, extra: { capturedAt?: string; title?: string } = {}): Record { const capturedAt = extra.capturedAt ?? new Date(config.clock()).toISOString(); return { source: EXTENSION_SOURCE, capture_mode: captureMode, capture_filter: config.captureMode, container_tag: config.containerTag, captured_at: capturedAt, ...(extra.title ? { title: extra.title } : {}), agent_name: "Pi coding-agent", agent_source: EXTENSION_SOURCE, }; } function documentMetadata( config: Config, ctx: Partial | undefined, extra: { capturedAt: string; title: string }, ): Record { const model = ctx?.model; return compactFlatMetadata({ title: extra.title, source: EXTENSION_SOURCE, type: "conversation", capture_mode: "turn_end", capture_filter: config.captureMode, container_tag: config.containerTag, captured_at: extra.capturedAt, agent_name: "Pi coding-agent", agent_source: EXTENSION_SOURCE, app_name: "Pi", cwd: ctx?.cwd, matched_directory: config.matchedDirectory, matched_model: config.matchedModel, matched_rule: config.matchedRule, model_id: model?.id, model_name: model?.name, model_provider: model?.provider, model_api: model?.api, }); } function compactFlatMetadata(input: Record): Record { const result: Record = {}; for (const [key, value] of Object.entries(input)) { if (typeof value === "string" && value.trim()) result[key] = value; else if (typeof value === "number" && Number.isFinite(value)) result[key] = value; else if (typeof value === "boolean") result[key] = value; } return result; } function turnTitle(userInput: string): string { const firstLine = userInput .split(/\r?\n/) .map((line) => line.trim()) .find(Boolean) ?? "Pi coding-agent turn"; const withoutMarkdown = firstLine .replace(/^#+\s+/, "") .replace(/^>\s+/, "") .replace(/[`*_~[\]()]/g, "") .replace(/\s+/g, " ") .trim(); const title = withoutMarkdown || "Pi coding-agent turn"; return title.length <= 96 ? title : `${title.slice(0, 93).trimEnd()}...`; } function turnCustomId(_capturedAt: string, fingerprint: string): string { const digest = createHash("sha256").update(fingerprint).digest("hex").slice(0, 16); return `${EXTENSION_SOURCE}-turn-${digest}`; } function statusPayload(config: Config, configured: boolean): Record { return { enabled: config.enabled, configured, containerTag: config.containerTag, apiBaseUrl: config.apiBaseUrl, autoRecall: config.autoRecall, autoCapture: config.autoCapture, captureMode: config.captureMode, permissions: config.permissions, maxRecall: config.maxRecall, configPath: config.configPath, matchedDirectory: config.matchedDirectory, matchedModel: config.matchedModel, matchedRule: config.matchedRule, }; } function makeTextResult(details: TDetails): TextToolResult { return { content: [{ type: "text", text: typeof details === "string" ? details : JSON.stringify(details, null, 2) }], details, }; } function savedMessage(containerTag: string, result: SupermemorySaveResult): string { return `Saved to Supermemory container "${containerTag}"${result.chunks ? ` as ${result.chunks} chunks` : ""}.`; } function clampLimit(value: number | undefined, fallback: number): number { if (!Number.isFinite(value) || value === undefined) return fallback; return Math.max(1, Math.min(25, Math.floor(value))); } function parsePositiveInteger(value: string | undefined, fallback: number): number { if (!value) return fallback; const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } function parseBoolean(value: string | undefined, fallback: boolean): boolean { if (value === undefined) return fallback; if (["1", "true", "yes", "on"].includes(value.toLowerCase())) return true; if (["0", "false", "no", "off"].includes(value.toLowerCase())) return false; return fallback; } function parseCaptureMode(value: string | undefined, fallback: CaptureMode): CaptureMode { if (value === "all" || value === "signal") return value; return fallback; } function trimTrailingSlash(value: string): string { return value.replace(/\/+$/, ""); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } async function readJson(response: Response): Promise { const text = await response.text(); if (!text.trim()) return {}; try { return JSON.parse(text) as unknown; } catch { return { text }; } } function extractId(value: unknown): string | undefined { return extractIds(value)[0]; } function extractIds(value: unknown): string[] { if (!isRecord(value)) return []; if (typeof value.id === "string") return [value.id]; if (Array.isArray(value.ids)) return value.ids.filter((id): id is string => typeof id === "string"); if (Array.isArray(value.memories)) return value.memories.flatMap((memory) => (isRecord(memory) && typeof memory.id === "string" ? [memory.id] : [])); if (isRecord(value.data) && typeof value.data.id === "string") return [value.data.id]; return []; } function formatSupermemoryError(body: unknown): string { if (isRecord(body) && Array.isArray(body.error)) { const messages = body.error.flatMap((entry) => { if (!isRecord(entry)) return []; const path = Array.isArray(entry.path) ? entry.path.join(".") : undefined; const message = typeof entry.message === "string" ? entry.message : undefined; return message ? [`${path ? `${path}: ` : ""}${message}`] : []; }); if (messages.length > 0) return messages.join("; "); } return JSON.stringify(body); } function conciseError(err: unknown): string { if (!(err instanceof Error)) return String(err); return err.message.split("\n")[0] ?? err.message; } async function notify(ctx: Pick, message: string, type: "info" | "warning" | "error" = "info"): Promise { const ui = ctx.ui as { notify?: (message: string, type?: "info" | "warning" | "error") => Promise | void } | undefined; if (ui?.notify) { await ui.notify(message, type); return; } if (!ctx.hasUI && type === "info") return; console[type === "error" ? "error" : "warn"](message); } function piSupermemoryExtension(pi: ExtensionAPI): void { createSupermemoryExtension().register(pi); } export default piSupermemoryExtension;