chore: import upstream snapshot with attribution
CI / test (20, macos-latest) (push) Has been cancelled
CI / test (20, ubuntu-latest) (push) Has been cancelled
CI / test (22, macos-latest) (push) Has been cancelled
CI / test (22, ubuntu-latest) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:01:18 +08:00
commit 979fb22d7c
613 changed files with 113494 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
<p align="center">
<img src="../../assets/banner.png" alt="agentmemory" width="640" />
</p>
<h1 align="center">
&nbsp;agentmemory for pi
</h1>
<p align="center">
<strong>Your pi sessions remember everything. No more re-explaining.</strong><br/>
<sub>Persistent cross-session memory via <a href="https://github.com/rohitg00/agentmemory">agentmemory</a> — shared with Claude Code, Codex CLI, Gemini CLI, Hermes, OpenClaw, and more.</sub>
</p>
---
## Quick setup
Start the agentmemory server in a separate terminal:
```bash
npx @agentmemory/agentmemory
```
Copy this folder into pi's global extensions directory:
```bash
mkdir -p ~/.pi/agent/extensions/agentmemory
cp integrations/pi/index.ts ~/.pi/agent/extensions/agentmemory/index.ts
```
Then enable it in `~/.pi/agent/settings.json` if you prefer explicit loading:
```json
{
"extensions": ["~/.pi/agent/extensions/agentmemory"]
}
```
If you place it under `~/.pi/agent/extensions/agentmemory/`, pi will also auto-discover it and `/reload` can hot-reload it.
## What it adds
- `memory_health` — confirm the shared memory server is reachable
- `memory_search` — search prior decisions, bugs, workflows, and preferences
- `memory_save` — write durable facts back to long-term memory
- `/agentmemory-status` — check health from inside pi
- `before_agent_start` recall — injects relevant memories into the prompt
- `agent_end` capture — saves completed conversation turns back to agentmemory
## Environment variables
| Variable | Default | Description |
|---|---|---|
| `AGENTMEMORY_URL` | `http://localhost:3111` | agentmemory server URL |
| `AGENTMEMORY_SECRET` | (none) | Bearer token for protected instances |
| `AGENTMEMORY_REQUIRE_HTTPS` | (off) | When set to `1`, refuse to send a bearer token over plaintext HTTP to a non-loopback host. Sends the token only when `AGENTMEMORY_URL` is `https://...` or points at `localhost`/`127.0.0.1`/`::1`. With this off, the plugin warns once but still sends. |
## Smoke test
Run pi and ask it to use the `memory_health` tool, or call the command directly:
```text
/agentmemory-status
```
You should see `agentmemory healthy` and a footer status like `🧠 agentmemory`.
## Notes
- This extension uses pi's extension API, not MCP, so it can hook directly into the agent lifecycle.
- One local agentmemory server can be shared across pi, pi2, Hermes, OpenClaw, Claude Code, Codex CLI, and Gemini CLI.
## See also
- [agentmemory main README](../../README.md)
- [Hermes integration](../hermes/README.md)
- [OpenClaw integration](../openclaw/README.md)
+275
View File
@@ -0,0 +1,275 @@
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "typebox";
import path from "node:path";
import crypto from "node:crypto";
import { createPlaintextBearerAuthGuard } from "./security.js";
type TextBlock = { type?: string; text?: string };
type AssistantMessage = { role?: string; content?: unknown };
type SmartSearchResult = {
title?: string;
narrative?: string;
type?: string;
combinedScore?: number;
score?: number;
observation?: {
title?: string;
narrative?: string;
type?: string;
};
};
type HealthResponse = {
status?: string;
service?: string;
version?: string;
health?: {
status?: string;
notes?: string[];
};
};
const DEFAULT_URL = process.env.AGENTMEMORY_URL || "http://localhost:3111";
const guardPlaintextBearerAuth = createPlaintextBearerAuthGuard();
const TOOL_GUIDANCE = [
"agentmemory is available for cross-session memory.",
"Use memory_search to recall prior decisions, preferences, bugs, and workflows.",
"Use memory_save when you discover durable facts worth remembering beyond this session.",
].join(" ");
function normalizeBaseUrl(url: string): string {
return url.replace(/\/+$/, "");
}
function getText(content: unknown): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
return content
.flatMap((part) => {
if (!part || typeof part !== "object") return [] as string[];
const block = part as TextBlock;
if (block.type === "text" && typeof block.text === "string") return [block.text];
return [] as string[];
})
.join("\n")
.trim();
}
function getLastAssistantText(messages: unknown[]): string {
for (const msg of [...messages].reverse()) {
if (!msg || typeof msg !== "object") continue;
const assistant = msg as AssistantMessage;
if (assistant.role !== "assistant") continue;
const text = getText(assistant.content);
if (text) return text;
}
return "";
}
function formatSearchResults(results: SmartSearchResult[]): string {
if (!results.length) return "No relevant memories found.";
return results
.slice(0, 5)
.map((result, index) => {
const obs = result.observation ?? result;
const title = obs.title?.trim() || `Memory ${index + 1}`;
const narrative = obs.narrative?.trim() || "";
const type = obs.type?.trim() || "memory";
const score = result.combinedScore ?? result.score;
const scoreText = typeof score === "number" ? ` [score=${score.toFixed(3)}]` : "";
return `- ${title} (${type})${scoreText}${narrative ? `: ${narrative}` : ""}`;
})
.join("\n");
}
async function callAgentMemory<T>(
pathname: string,
options?: {
method?: "GET" | "POST";
body?: unknown;
baseUrl?: string;
},
): Promise<T | null> {
const baseUrl = normalizeBaseUrl(options?.baseUrl || process.env.AGENTMEMORY_URL || DEFAULT_URL);
const method = options?.method || "POST";
const url = `${baseUrl}/agentmemory/${pathname.replace(/^\/+/, "")}`;
const headers: Record<string, string> = {};
const secret = process.env.AGENTMEMORY_SECRET;
guardPlaintextBearerAuth(baseUrl, secret);
if (options?.body !== undefined) headers["Content-Type"] = "application/json";
if (secret) headers.Authorization = `Bearer ${secret}`;
try {
const response = await fetch(url, {
method,
headers,
body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
});
if (!response.ok) return null;
return (await response.json()) as T;
} catch {
return null;
}
}
export default function agentmemoryExtension(pi: ExtensionAPI) {
if (process.env.AGENTMEMORY_REQUIRE_HTTPS === "1") {
guardPlaintextBearerAuth(
normalizeBaseUrl(process.env.AGENTMEMORY_URL || DEFAULT_URL),
process.env.AGENTMEMORY_SECRET,
);
}
let sessionId = `ephemeral-${crypto.randomUUID().slice(0, 8)}`;
let currentProject = process.cwd();
let lastPrompt = "";
let lastHealthOk = false;
async function getHealth() {
return await callAgentMemory<HealthResponse>("health", { method: "GET" });
}
async function refreshStatus(ctx: { ui: { setStatus: (key: string, text: string) => void } }) {
const health = await getHealth();
lastHealthOk = !!health && (health.status === "healthy" || health.health?.status === "healthy");
ctx.ui.setStatus("agentmemory", lastHealthOk ? "🧠 agentmemory" : "🧠 agentmemory off");
}
pi.registerCommand("agentmemory-status", {
description: "Check local agentmemory server health",
handler: async (_args, ctx) => {
const health = await getHealth();
if (!health) {
ctx.ui.notify("agentmemory is unreachable at http://localhost:3111", "warning");
return;
}
ctx.ui.notify(
`agentmemory ${health.status || health.health?.status || "unknown"}${health.version ? ` v${health.version}` : ""}`,
"info",
);
},
});
pi.registerTool({
name: "memory_health",
label: "Memory Health",
description: "Check whether the local agentmemory server is reachable and healthy",
parameters: Type.Object({}),
async execute() {
const health = await getHealth();
if (!health) {
return {
content: [{ type: "text", text: "agentmemory is unreachable at http://localhost:3111" }],
details: { ok: false },
};
}
return {
content: [
{
type: "text",
text: `agentmemory status: ${health.status || health.health?.status || "unknown"}${health.version ? ` (v${health.version})` : ""}`,
},
],
details: health,
};
},
});
pi.registerTool({
name: "memory_search",
label: "Memory Search",
description: "Search agentmemory for cross-session project memory, prior decisions, bugs, and user preferences",
parameters: Type.Object({
query: Type.String({ description: "What to search for in memory" }),
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10, default: 5, description: "Maximum results" })),
}),
async execute(_toolCallId, params) {
const result = await callAgentMemory<{ results?: SmartSearchResult[] }>("smart-search", {
body: { query: params.query, limit: params.limit ?? 5 },
});
const results = result?.results || [];
return {
content: [{ type: "text", text: formatSearchResults(results) }],
details: { query: params.query, results },
};
},
});
pi.registerTool({
name: "memory_save",
label: "Memory Save",
description: "Save a durable fact, convention, workflow, preference, or bug fix into agentmemory",
parameters: Type.Object({
content: Type.String({ description: "What should be remembered" }),
type: Type.Optional(
Type.String({
description: "Memory type",
default: "fact",
}),
),
}),
async execute(_toolCallId, params) {
const result = await callAgentMemory<Record<string, unknown>>("remember", {
body: { content: params.content, type: params.type || "fact" },
});
if (!result) {
return {
content: [{ type: "text", text: "Failed to save memory to agentmemory." }],
details: { ok: false },
};
}
return {
content: [{ type: "text", text: `Saved memory (${params.type || "fact"}): ${params.content}` }],
details: result,
};
},
});
pi.on("session_start", async (_event, ctx) => {
const sessionFile = ctx.sessionManager.getSessionFile();
sessionId = sessionFile ? path.basename(sessionFile).replace(/\.[^.]+$/, "") : `ephemeral-${crypto.randomUUID().slice(0, 8)}`;
currentProject = process.cwd();
await refreshStatus(ctx);
});
pi.on("before_agent_start", async (event, ctx) => {
currentProject = event.systemPromptOptions.cwd || process.cwd();
lastPrompt = event.prompt?.trim() || "";
if (!lastPrompt) return;
const result = await callAgentMemory<{ results?: SmartSearchResult[] }>("smart-search", {
body: { query: lastPrompt, limit: 5 },
});
const results = result?.results || [];
const recallBlock = results.length
? [
"Relevant long-term memory from agentmemory:",
formatSearchResults(results),
].join("\n")
: "";
await refreshStatus(ctx);
return {
systemPrompt: [event.systemPrompt, TOOL_GUIDANCE, recallBlock].filter(Boolean).join("\n\n"),
};
});
pi.on("agent_end", async (event) => {
if (!lastHealthOk || !lastPrompt) return;
const assistantText = getLastAssistantText(event.messages as unknown[]);
if (!assistantText) return;
void callAgentMemory("observe", {
body: {
hookType: "post_tool_use",
sessionId,
project: currentProject,
cwd: currentProject,
timestamp: new Date().toISOString(),
data: {
tool_name: "conversation",
tool_input: lastPrompt.slice(0, 500),
tool_output: assistantText.slice(0, 4000),
},
},
});
});
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "agentmemory-pi-extension",
"private": true,
"type": "module"
}
+35
View File
@@ -0,0 +1,35 @@
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
function normalizedHostname(hostname: string): string {
return hostname.replace(/^\[|\]$/g, "").toLowerCase();
}
export function usesPlaintextBearerAuth(baseUrl: string, secret?: string): boolean {
if (!secret) return false;
try {
const parsed = new URL(baseUrl);
return parsed.protocol === "http:" && !LOOPBACK_HOSTS.has(normalizedHostname(parsed.hostname));
} catch {
return false;
}
}
export function plaintextBearerAuthMessage(baseUrl: string): string {
return `agentmemory: AGENTMEMORY_SECRET is configured for plaintext HTTP to ${baseUrl}. Bearer tokens and memory payloads can be observed on the network; use HTTPS or an SSH tunnel.`;
}
export function createPlaintextBearerAuthGuard(
warn: (message: string) => void = (message) => console.warn(message),
env?: { AGENTMEMORY_REQUIRE_HTTPS?: string },
): (baseUrl: string, secret?: string) => void {
let warned = false;
return (baseUrl, secret) => {
if (!usesPlaintextBearerAuth(baseUrl, secret)) return;
const message = plaintextBearerAuthMessage(baseUrl);
if ((env || process.env).AGENTMEMORY_REQUIRE_HTTPS === "1") throw new Error(message);
if (!warned) {
warned = true;
warn(message);
}
};
}