Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"providerId": "official",
"name": "PI-Desktop Official Plugins",
"updatedAt": "2026-09-20T15:44:14Z",
"updatedAt": "2026-09-20T17:25:34Z",
"homepage": "https://github.com/vastsa/pi-desktop-plugins",
"plugins": [
{
Expand Down Expand Up @@ -150,6 +150,51 @@
}
]
},
{
"id": "in.memcode.memory",
"name": "Memcode Memory",
"description": "Give PI-Desktop agents explicit tools to save, search, and retrieve the signed-in user's Memcode memories.",
"i18n": {
"en": {
"name": "Memcode Memory",
"description": "Give PI-Desktop agents explicit tools to save, search, and retrieve the signed-in user's Memcode memories.",
"safetyNotes": "Makes bounded requests only to memory.memcode.in. Reads MEMCODE_API_KEY from the PI-Desktop process environment and never stores or returns it. Saving memory is persistent and requires PI-Desktop's high-risk tool approval."
},
"zh-CN": {
"name": "Memcode \u8bb0\u5fc6",
"description": "\u4e3a PI-Desktop Agent \u63d0\u4f9b\u660e\u786e\u7684\u5de5\u5177\uff0c\u7528\u4e8e\u4fdd\u5b58\u3001\u641c\u7d22\u548c\u68c0\u7d22\u5f53\u524d\u7528\u6237\u7684 Memcode \u8bb0\u5fc6\u3002",
"safetyNotes": "\u4ec5\u5411 memory.memcode.in \u53d1\u8d77\u53d7\u9650\u8bf7\u6c42\u3002\u4ece PI-Desktop \u8fdb\u7a0b\u73af\u5883\u8bfb\u53d6 MEMCODE_API_KEY\uff0c\u7edd\u4e0d\u4fdd\u5b58\u6216\u8fd4\u56de\u8be5\u5bc6\u94a5\u3002\u4fdd\u5b58\u8bb0\u5fc6\u662f\u6301\u4e45\u5316\u64cd\u4f5c\uff0c\u5e76\u53d7 PI-Desktop \u9ad8\u98ce\u9669\u5de5\u5177\u5ba1\u6279\u4fdd\u62a4\u3002"
}
},
"author": "Memcode",
"categories": [
"productivity",
"ai",
"memory"
],
"verified": true,
"downloads": 0,
"homepage": "https://github.com/vastsa/pi-desktop-plugins/tree/main/plugins/in.memcode.memory",
"repository": "https://github.com/vastsa/pi-desktop-plugins",
"readmeMarkdown": "# Memcode Memory\n\nAdds explicit Memcode long-term-memory tools to PI-Desktop agents. The plugin\nuses Memcode's personal v2 API, so the API credential determines the user; no\nuser ID or integration attribution is accepted from agent arguments.\n\n## Setup\n\nSet the credential in the environment that starts PI-Desktop, then restart the\napp:\n\n```bash\nexport MEMCODE_API_KEY=your_memcode_api_key\n```\n\nThe plugin never persists this value in PI-Desktop settings. Start with\n`memcode_test_connection`, then explicitly approve memory writes when PI asks.\n\n## Tools\n\n| Tool | Risk | Operation |\n| --- | --- | --- |\n| `memcode_test_connection` | Medium | `GET /v2/test`; validates the configured credential without reading memory |\n| `memcode_save_memory` | High | `POST /v2/memory/ingest`; persists user-approved text and returns an asynchronous job receipt |\n| `memcode_search_memories` | Medium | `POST /v2/memory/search`; searches the authenticated user's memory/chunk index |\n| `memcode_retrieve_answer` | Medium | `POST /v2/memory/retrieve`; returns a grounded answer and source records |\n\nPI-Desktop exposes these under its normal plugin-prefixed tool names.\n\n## Capability and data-flow review\n\n| Entry point | Data read | Destination | Permission | Confirmation/bounds | Cleanup |\n| --- | --- | --- | --- | --- | --- |\n| All tools | `MEMCODE_API_KEY` and tool arguments | `https://memory.memcode.in` only | `net.fetch` | PI install grant; 20s timeout; 256 KiB response cap; no redirects outside allowlist | No background task or retained token |\n| Save tool | User-approved text and optional assistant response | `/v2/memory/ingest` | `agent.tool.register`, `net.fetch` | High-risk agent-tool approval; 20,000-character fields | Durable data follows the user's Memcode retention settings |\n| Search/retrieve | Query and bounded numeric options | `/v2/memory/search`, `/v2/memory/retrieve` | `agent.tool.register`, `net.fetch` | Medium-risk tool policy; query and result bounds | No plugin-local cache |\n\nThe plugin has no third-party runtime dependencies, native code, dynamic code,\nfilesystem access, telemetry, retries, timers, or background services. It does\nnot send client-provided attribution headers or metadata; Memcode assigns any\nintegration attribution server-side.\n\n## Failure behavior\n\n- Missing or malformed credentials fail before the network call.\n- HTTP errors expose only the status (and bounded `Retry-After` for 429), not\n response bodies that could contain sensitive details.\n- Malformed or oversized JSON responses are rejected.\n- The plugin never retries a write automatically.\n- Unload unregisters every tool, including after a partially failed load.\n\nDeleting memories is intentionally outside this first version because\nPI-Desktop agent tools must not perform destructive remote actions without a\nseparate, explicit design and confirmation path.\n",
"safetyNotes": "Makes bounded requests only to memory.memcode.in. Reads MEMCODE_API_KEY from the PI-Desktop process environment and never stores or returns it. Saving memory is persistent and requires PI-Desktop's high-risk tool approval.",
"versions": [
{
"version": "0.1.0",
"publishedAt": "2026-09-20T17:25:34Z",
"changelog": "Initial release with credential verification, durable memory ingest, semantic search, and grounded retrieval tools.",
"minPiDesktop": ">=0.2.0",
"shasum": "c77e6aebaed1f09d9fb82568028b40910c97853fa289b114e819454279b54672",
"url": "packages/in.memcode.memory-0.1.0.piplug",
"sizeBytes": 14955,
"permissions": [
"agent.tool.register",
"net.fetch"
],
"fs": {}
}
]
},
{
"id": "io.github.liushunqiu.pi-idea-git",
"name": "IDEA Git",
Expand Down
Binary file added packages/in.memcode.memory-0.1.0.piplug
Binary file not shown.
58 changes: 58 additions & 0 deletions plugins/in.memcode.memory/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Memcode Memory

Adds explicit Memcode long-term-memory tools to PI-Desktop agents. The plugin
uses Memcode's personal v2 API, so the API credential determines the user; no
user ID or integration attribution is accepted from agent arguments.

## Setup

Open the [Memcode API-key dashboard](https://app.memcode.in/dashboard?section=api-keys&integration=pi-desktop) and create a
key with **PI-Desktop** selected under integration attribution. Set that key in
the environment that starts PI-Desktop, then restart the app:

```bash
export MEMCODE_API_KEY=your_memcode_api_key
```

Memcode binds the `pi-desktop` identity when the key is issued; the plugin does
not send an attribution header or metadata field. A generic personal key still
works, but its traffic is counted as generic direct API usage. The plugin never
persists this value in PI-Desktop settings. Start with
`memcode_test_connection`, then explicitly approve memory writes when PI asks.

## Tools

| Tool | Risk | Operation |
| --- | --- | --- |
| `memcode_test_connection` | Medium | `GET /v2/test`; validates the configured credential without reading memory |
| `memcode_save_memory` | High | `POST /v2/memory/ingest`; persists user-approved text and returns an asynchronous job receipt |
| `memcode_search_memories` | Medium | `POST /v2/memory/search`; searches the authenticated user's memory/chunk index |
| `memcode_retrieve_answer` | Medium | `POST /v2/memory/retrieve`; returns a grounded answer and source records |

PI-Desktop exposes these under its normal plugin-prefixed tool names.

## Capability and data-flow review

| Entry point | Data read | Destination | Permission | Confirmation/bounds | Cleanup |
| --- | --- | --- | --- | --- | --- |
| All tools | `MEMCODE_API_KEY` and tool arguments | `https://memory.memcode.in` only | `net.fetch` | PI install grant; 20s timeout; 256 KiB response cap; no redirects outside allowlist | No background task or retained token |
| Save tool | User-approved text and optional assistant response | `/v2/memory/ingest` | `agent.tool.register`, `net.fetch` | High-risk agent-tool approval; 20,000-character fields | Durable data follows the user's Memcode retention settings |
| Search/retrieve | Query and bounded numeric options | `/v2/memory/search`, `/v2/memory/retrieve` | `agent.tool.register`, `net.fetch` | Medium-risk tool policy; query and result bounds | No plugin-local cache |

The plugin has no third-party runtime dependencies, native code, dynamic code,
filesystem access, telemetry, retries, timers, or background services. It does
not send client-provided attribution headers or metadata; Memcode assigns any
integration attribution server-side.

## Failure behavior

- Missing or malformed credentials fail before the network call.
- HTTP errors expose only the status (and bounded `Retry-After` for 429), not
response bodies that could contain sensitive details.
- Malformed or oversized JSON responses are rejected.
- The plugin never retries a write automatically.
- Unload unregisters every tool, including after a partially failed load.

Deleting memories is intentionally outside this first version because
PI-Desktop agent tools must not perform destructive remote actions without a
separate, explicit design and confirmation path.
196 changes: 196 additions & 0 deletions plugins/in.memcode.memory/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
const API_ORIGIN = "https://memory.memcode.in";
const RESPONSE_LIMIT = 256 * 1024;
const registeredTools = [];

function requireApiKey() {
const value = String(process.env.MEMCODE_API_KEY || "").trim();
if (!value) {
throw new Error("MEMCODE_API_KEY is not configured for PI-Desktop.");
}
if (value.length > 16384 || /[\r\n]/.test(value)) {
throw new Error("MEMCODE_API_KEY is invalid.");
}
return value;
}

function requiredString(value, name, maxLength) {
const normalized = String(value ?? "").trim();
if (!normalized) throw new Error(`${name} is required.`);
if (normalized.length > maxLength) throw new Error(`${name} exceeds ${maxLength} characters.`);
return normalized;
}

function optionalString(value, name, maxLength) {
if (value === undefined || value === null || value === "") return undefined;
return requiredString(value, name, maxLength);
}

function optionalInteger(value, name, minimum, maximum) {
if (value === undefined || value === null) return undefined;
if (!Number.isInteger(value) || value < minimum || value > maximum) {
throw new Error(`${name} must be an integer between ${minimum} and ${maximum}.`);
}
return value;
}

function optionalNumber(value, name, minimum, maximum) {
if (value === undefined || value === null) return undefined;
if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum) {
throw new Error(`${name} must be between ${minimum} and ${maximum}.`);
}
return value;
}

function optionalEnum(value, name, values) {
if (value === undefined || value === null) return undefined;
if (!values.includes(value)) throw new Error(`${name} must be one of: ${values.join(", ")}.`);
return value;
}

function compact(object) {
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined));
}

async function memcodeRequest(path, { method = "GET", body, headers = {} } = {}) {
const apiKey = requireApiKey();
const response = await pi.net.fetch({
url: `${API_ORIGIN}${path}`,
method,
headers: {
accept: "application/json",
authorization: `Bearer ${apiKey}`,
...(body === undefined ? {} : { "content-type": "application/json" }),
...headers,
},
...(body === undefined ? {} : { body: JSON.stringify(body) }),
timeoutMs: 20000,
});

if (response.status < 200 || response.status >= 300) {
const retryAfter = response.headers?.["retry-after"] || response.headers?.["Retry-After"];
const suffix = response.status === 429 && retryAfter ? ` Retry after ${String(retryAfter).slice(0, 32)}.` : "";
throw new Error(`Memcode request failed with HTTP ${response.status}.${suffix}`);
}
const bodyText = String(response.bodyText || "");
if (bodyText.length > RESPONSE_LIMIT) throw new Error("Memcode response exceeded the 256 KiB limit.");
let payload;
try {
payload = bodyText ? JSON.parse(bodyText) : {};
} catch {
throw new Error("Memcode returned an invalid JSON response.");
}
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
throw new Error("Memcode returned an invalid response shape.");
}
return payload.data && typeof payload.data === "object" ? payload.data : payload;
}

const tools = [
{
name: "memcode_test_connection",
description: "Verify that the configured Memcode credential is valid without reading or writing memories.",
risk: "medium",
schema: { type: "object", properties: {}, additionalProperties: false },
execute: async () => memcodeRequest("/v2/test"),
},
{
name: "memcode_save_memory",
description: "Persist user-approved text to the authenticated user's Memcode long-term memory.",
risk: "high",
schema: {
type: "object",
properties: {
text: { type: "string", minLength: 1, maxLength: 20000 },
agent_response: { type: "string", maxLength: 20000 },
effort_level: { type: "string", enum: ["low", "high"] },
forget: { type: "boolean" },
idempotency_key: { type: "string", minLength: 1, maxLength: 256 },
},
required: ["text"],
additionalProperties: false,
},
execute: async (args = {}) => {
const idempotencyKey = optionalString(args.idempotency_key, "idempotency_key", 256);
return memcodeRequest("/v2/memory/ingest", {
method: "POST",
headers: idempotencyKey ? { "idempotency-key": idempotencyKey } : {},
body: compact({
user_query: requiredString(args.text, "text", 20000),
agent_response: optionalString(args.agent_response, "agent_response", 20000),
effort_level: optionalEnum(args.effort_level, "effort_level", ["low", "high"]) || "low",
forget: args.forget === undefined ? false : Boolean(args.forget),
}),
});
},
},
{
name: "memcode_search_memories",
description: "Search the authenticated user's Memcode memories and original stored chunks.",
risk: "medium",
schema: {
type: "object",
properties: {
query: { type: "string", minLength: 1, maxLength: 4000 },
mode: { type: "string", enum: ["default", "chunks", "memories"] },
top_k: { type: "integer", minimum: 1, maximum: 100 },
original_top_k: { type: "integer", minimum: 1, maximum: 100 },
include_original_chunks: { type: "boolean" },
search_mode: { type: "string", enum: ["default", "global"] },
minimum_score: { type: "number", minimum: 0, maximum: 1 },
},
required: ["query"],
additionalProperties: false,
},
execute: async (args = {}) => memcodeRequest("/v2/memory/search", {
method: "POST",
body: compact({
query: requiredString(args.query, "query", 4000),
mode: optionalEnum(args.mode, "mode", ["default", "chunks", "memories"]) || "default",
top_k: optionalInteger(args.top_k, "top_k", 1, 100) || 10,
original_top_k: optionalInteger(args.original_top_k, "original_top_k", 1, 100) || 10,
include_original_chunks: args.include_original_chunks === undefined ? true : Boolean(args.include_original_chunks),
search_mode: optionalEnum(args.search_mode, "search_mode", ["default", "global"]) || "default",
minimum_score: optionalNumber(args.minimum_score, "minimum_score", 0, 1) ?? 0,
}),
}),
},
{
name: "memcode_retrieve_answer",
description: "Answer a question from the authenticated user's Memcode memories with source records.",
risk: "medium",
schema: {
type: "object",
properties: {
query: { type: "string", minLength: 1, maxLength: 4000 },
top_k: { type: "integer", minimum: 1, maximum: 50 },
},
required: ["query"],
additionalProperties: false,
},
execute: async (args = {}) => memcodeRequest("/v2/memory/retrieve", {
method: "POST",
body: {
query: requiredString(args.query, "query", 4000),
top_k: optionalInteger(args.top_k, "top_k", 1, 50) || 5,
},
}),
},
];

async function onLoad() {
try {
for (const tool of tools) {
await pi.agent.registerTool(tool);
registeredTools.push(tool.name);
}
} catch (error) {
await Promise.allSettled(registeredTools.splice(0).map((name) => pi.agent.unregisterTool(name)));
throw error;
}
}

async function onUnload() {
await Promise.allSettled(registeredTools.splice(0).map((name) => pi.agent.unregisterTool(name)));
}

module.exports = { onLoad, onUnload, __test: { memcodeRequest, tools } };
Loading