feat(extensions): version-control pi mempalace extension + install.sh symlink
The pi coding-agent extension at ~/.pi/agent/extensions/mempalace.ts was living only on tor-ms22, including hand-edited fixes (Type.Unsafe schema-passthrough for MCP tool parameters). One disk wipe away from losing it, and no way to reproduce the install on a new machine. - extensions/pi/mempalace.ts: canonical copy (matches tor-ms22 byte-for-byte) - extensions/pi/README.md: what it does, the schema-passthrough gotcha, debugging knobs - install.sh: new install_pi_extension step — gated on ~/.pi/agent/extensions/ existing, backs up any real file in the way, idempotent re-runs, mirror block in uninstall. Works on macOS and Linux (plain ln -s, readlink -f). - README.md: mention extensions/pi/ in the repo-contents list and in the Setup section Verified on tor-ms22: install (backs up existing real file) → uninstall (removes symlink) → reinstall (clean symlink). Re-runs are no-ops.
This commit is contained in:
@@ -8,6 +8,7 @@ Producer-side tooling for [MemPalace](https://github.com/MemPalace/mempalace)
|
|||||||
- `bin/mempalace-docs` — mines project directories into MemPalace while excluding source code, keeping the palace signal-dense.
|
- `bin/mempalace-docs` — mines project directories into MemPalace while excluding source code, keeping the palace signal-dense.
|
||||||
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — **canonical spec**: architecture diagram, component details, setup recipe, operational notes, upstream-retirement roadmap.
|
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — **canonical spec**: architecture diagram, component details, setup recipe, operational notes, upstream-retirement roadmap.
|
||||||
- [`SKILL.md`](SKILL.md) — the companion agent skill, symlinked into `~/.agents/skills/opencode-mempalace-bridge/` on install.
|
- [`SKILL.md`](SKILL.md) — the companion agent skill, symlinked into `~/.agents/skills/opencode-mempalace-bridge/` on install.
|
||||||
|
- [`extensions/pi/`](extensions/pi/) — the [pi coding-agent](https://github.com/mariozechner/pi-coding-agent) extension that bridges pi to the MemPalace MCP server (wake-up auto-injection, `/mempalace-diary` command, schema passthrough). Symlinked into `~/.pi/agent/extensions/` on install when pi is detected.
|
||||||
|
|
||||||
**If you're just trying to get this working on a new machine → jump to [Setup](#setup).**
|
**If you're just trying to get this working on a new machine → jump to [Setup](#setup).**
|
||||||
**If you want the full architecture story → read [`ARCHITECTURE.md`](ARCHITECTURE.md).**
|
**If you want the full architecture story → read [`ARCHITECTURE.md`](ARCHITECTURE.md).**
|
||||||
@@ -221,7 +222,7 @@ cd ~/mempalace-toolkit
|
|||||||
./install.sh
|
./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The installer symlinks `bin/*` into `~/.local/bin/` and optionally installs the agent skill into `~/.agents/skills/opencode-mempalace-bridge/`.
|
The installer symlinks `bin/*` into `~/.local/bin/` and installs the agent skill into `~/.agents/skills/opencode-mempalace-bridge/`. If [pi](https://github.com/mariozechner/pi-coding-agent) is installed (detected via `~/.pi/agent/extensions/`), it also symlinks [`extensions/pi/mempalace.ts`](extensions/pi/) into that directory so the pi↔mempalace bridge tracks version control. On machines without pi this step is silently skipped. Works on macOS and Linux.
|
||||||
|
|
||||||
Ensure `~/.local/bin` is on `$PATH`:
|
Ensure `~/.local/bin` is on `$PATH`:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# pi ↔ MemPalace extension
|
||||||
|
|
||||||
|
The canonical source of `~/.pi/agent/extensions/mempalace.ts` — the bridge
|
||||||
|
that wires the [MemPalace](https://github.com/MemPalace/mempalace) MCP
|
||||||
|
server into the [pi coding-agent](https://github.com/mariozechner/pi-coding-agent)
|
||||||
|
harness.
|
||||||
|
|
||||||
|
`install.sh` at the repo root symlinks `mempalace.ts` from this directory
|
||||||
|
into `~/.pi/agent/extensions/` so the live file on every machine tracks
|
||||||
|
version control. Works on macOS and Linux (the extension itself is plain
|
||||||
|
Node / TypeScript; the symlink is a POSIX `ln -s`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
1. **Spawns `mempalace-mcp`** as a subprocess and does the MCP stdio
|
||||||
|
JSON-RPC handshake (`initialize` + `notifications/initialized` +
|
||||||
|
`tools/list`).
|
||||||
|
2. **Registers each MCP tool** as a pi tool with its real `inputSchema`
|
||||||
|
passed through via `Type.Unsafe(...)` (see gotcha below).
|
||||||
|
3. **Wake-up auto-injection** (`before_agent_start`, one-shot per fresh
|
||||||
|
session): calls `mempalace_status` + `mempalace_diary_read` and
|
||||||
|
injects the result as a `mempalace-wakeup` system message so the
|
||||||
|
agent orients itself the way `~/.agents/skills/mempalace/SKILL.md`
|
||||||
|
describes. Skipped on resume/fork (context is already in the thread).
|
||||||
|
4. **Manual wind-down** via a `/mempalace-diary [topic]` slash command:
|
||||||
|
sends a prompt asking the LLM to call `mempalace_diary_write` with
|
||||||
|
an AAAK-formatted entry summarizing the session. Not fully auto
|
||||||
|
because pi sessions are typically short/tactical and
|
||||||
|
`session_shutdown` fires too late to drive another LLM turn.
|
||||||
|
|
||||||
|
## Fail-soft
|
||||||
|
|
||||||
|
If `mempalace-mcp` can't be spawned (PATH missing, binary crashes at
|
||||||
|
startup, …) the extension logs to stderr and returns early. pi keeps
|
||||||
|
working without palace tools rather than refusing to start.
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
`agent_name` for diary calls comes from `$MEMPALACE_AGENT_NAME`, defaulting
|
||||||
|
to `"pi"`. First diary write against that identity creates `wing_<name>`
|
||||||
|
in the palace. Set the env var if you want to run pi under a distinct
|
||||||
|
identity on a given machine (e.g. `pi-laptop` vs `pi-server`).
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
- `MEMPALACE_EXT_DEBUG=1` — surface `mempalace-mcp` stderr into pi's
|
||||||
|
stderr. Without this, stderr is drained silently so a misbehaving
|
||||||
|
server doesn't flood the TUI.
|
||||||
|
- If a tool call fails with a generic "Internal tool error", spawn
|
||||||
|
`mempalace-mcp` manually with raw JSON-RPC on stdin to read the
|
||||||
|
server-side error — much faster than guessing.
|
||||||
|
|
||||||
|
## The `Type.Unsafe` gotcha
|
||||||
|
|
||||||
|
Earlier versions of this extension registered every MCP tool with
|
||||||
|
`parameters: Type.Object({}, { additionalProperties: true })`, which
|
||||||
|
discarded each tool's real `inputSchema`. The LLM then saw no parameter
|
||||||
|
names and had to guess, leading to bugs like `mempalace_diary_read`
|
||||||
|
being called with `agent=` instead of the required `agent_name=` and
|
||||||
|
crashing the Python server with `TypeError: missing 1 required
|
||||||
|
positional argument`.
|
||||||
|
|
||||||
|
The fix (≈ lines 160-170) is to wrap the incoming JSON Schema with
|
||||||
|
`Type.Unsafe<...>(tool.inputSchema)`. TypeBox schemas are plain JSON
|
||||||
|
Schema at runtime plus a `Symbol` marker, so wrapping an
|
||||||
|
externally-sourced schema with `Unsafe` is sufficient — no conversion
|
||||||
|
to a full TypeBox tree is needed, and the LLM now sees every tool's
|
||||||
|
real parameter names.
|
||||||
|
|
||||||
|
If you ever need to re-loosen the schema for debugging, fall back to
|
||||||
|
the `Type.Object({}, { additionalProperties: true })` default only for
|
||||||
|
that specific tool, not globally.
|
||||||
|
|
||||||
|
## File layout
|
||||||
|
|
||||||
|
```
|
||||||
|
mempalace-toolkit/
|
||||||
|
└── extensions/
|
||||||
|
└── pi/
|
||||||
|
├── README.md ← this file
|
||||||
|
└── mempalace.ts ← symlinked into ~/.pi/agent/extensions/
|
||||||
|
```
|
||||||
|
|
||||||
|
`install.sh` detects pi by probing for `~/.pi/agent/extensions/` and
|
||||||
|
only creates the symlink when that directory exists. On machines
|
||||||
|
without pi the file stays dormant in the repo. Re-runs are idempotent
|
||||||
|
(same pattern as `bin/` and `SKILL.md`).
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
/**
|
||||||
|
* MemPalace ↔ pi bridge.
|
||||||
|
*
|
||||||
|
* Spawns the `mempalace-mcp` MCP stdio server as a subprocess, performs the
|
||||||
|
* MCP `initialize` handshake, lists available tools, and registers each
|
||||||
|
* one as a pi tool that proxies to `tools/call`.
|
||||||
|
*
|
||||||
|
* Lifecycle automation (per ~/.agents/skills/mempalace/SKILL.md):
|
||||||
|
* - Wake-up (auto): on first user prompt of a fresh session, inject
|
||||||
|
* `mempalace_status` + `mempalace_diary_read` output as context so the
|
||||||
|
* agent orients itself the way the mempalace skill describes. Skipped
|
||||||
|
* on resume/fork (palace context is already in the thread).
|
||||||
|
* - Wind-down (manual): `/mempalace-diary` command prompts the LLM to
|
||||||
|
* write an AAAK-formatted diary entry. Not fully auto because pi
|
||||||
|
* sessions are typically short/tactical and session_shutdown is too
|
||||||
|
* late to drive an LLM turn.
|
||||||
|
*
|
||||||
|
* Identity: `agent_name` for diary calls comes from $MEMPALACE_AGENT_NAME,
|
||||||
|
* defaulting to "pi". First diary write creates `wing_pi`.
|
||||||
|
*
|
||||||
|
* Fail-soft: if the MCP subprocess can't start, pi keeps working without
|
||||||
|
* palace tools (warning on stderr only).
|
||||||
|
*
|
||||||
|
* Debug: set MEMPALACE_EXT_DEBUG=1 to surface mempalace-mcp stderr.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
||||||
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { Type } from "typebox";
|
||||||
|
|
||||||
|
// Minimal MCP stdio JSON-RPC client. MCP uses newline-delimited JSON.
|
||||||
|
interface McpTool {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
inputSchema?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
class McpClient {
|
||||||
|
private proc: ChildProcessWithoutNullStreams | null = null;
|
||||||
|
private nextId = 1;
|
||||||
|
private pending = new Map<number, { resolve: (v: any) => void; reject: (e: Error) => void }>();
|
||||||
|
private stdoutBuf = "";
|
||||||
|
private ready: Promise<void> | null = null;
|
||||||
|
public tools: McpTool[] = [];
|
||||||
|
|
||||||
|
async start(command: string, args: string[] = []): Promise<void> {
|
||||||
|
if (this.ready) return this.ready;
|
||||||
|
this.ready = (async () => {
|
||||||
|
this.proc = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
||||||
|
this.proc.on("error", (err) => this.failAll(err));
|
||||||
|
this.proc.on("exit", (code) =>
|
||||||
|
this.failAll(new Error(`mempalace-mcp exited (code=${code})`)),
|
||||||
|
);
|
||||||
|
this.proc.stdout.setEncoding("utf8");
|
||||||
|
this.proc.stdout.on("data", (chunk: string) => this.onStdout(chunk));
|
||||||
|
// Drain stderr silently. Re-enable by setting MEMPALACE_EXT_DEBUG=1.
|
||||||
|
this.proc.stderr.setEncoding("utf8");
|
||||||
|
if (process.env.MEMPALACE_EXT_DEBUG) {
|
||||||
|
this.proc.stderr.on("data", (chunk: string) => {
|
||||||
|
process.stderr.write(`[mempalace-mcp stderr] ${chunk}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.proc.stderr.resume(); // drain without logging
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCP initialize handshake.
|
||||||
|
await this.request("initialize", {
|
||||||
|
protocolVersion: "2024-11-05",
|
||||||
|
capabilities: {},
|
||||||
|
clientInfo: { name: "pi-mempalace-ext", version: "0.1.0" },
|
||||||
|
});
|
||||||
|
this.notify("notifications/initialized", {});
|
||||||
|
|
||||||
|
const listed = await this.request("tools/list", {});
|
||||||
|
this.tools = (listed?.tools as McpTool[]) ?? [];
|
||||||
|
})();
|
||||||
|
return this.ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onStdout(chunk: string) {
|
||||||
|
this.stdoutBuf += chunk;
|
||||||
|
let nl: number;
|
||||||
|
while ((nl = this.stdoutBuf.indexOf("\n")) !== -1) {
|
||||||
|
const line = this.stdoutBuf.slice(0, nl).trim();
|
||||||
|
this.stdoutBuf = this.stdoutBuf.slice(nl + 1);
|
||||||
|
if (!line) continue;
|
||||||
|
let msg: any;
|
||||||
|
try {
|
||||||
|
msg = JSON.parse(line);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (typeof msg.id === "number" && this.pending.has(msg.id)) {
|
||||||
|
const { resolve, reject } = this.pending.get(msg.id)!;
|
||||||
|
this.pending.delete(msg.id);
|
||||||
|
if (msg.error) reject(new Error(msg.error.message ?? "MCP error"));
|
||||||
|
else resolve(msg.result);
|
||||||
|
}
|
||||||
|
// notifications (no id) ignored for now.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private failAll(err: Error) {
|
||||||
|
for (const { reject } of this.pending.values()) reject(err);
|
||||||
|
this.pending.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private write(obj: unknown) {
|
||||||
|
if (!this.proc) throw new Error("MCP process not started");
|
||||||
|
this.proc.stdin.write(`${JSON.stringify(obj)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private notify(method: string, params: unknown) {
|
||||||
|
this.write({ jsonrpc: "2.0", method, params });
|
||||||
|
}
|
||||||
|
|
||||||
|
request(method: string, params: unknown): Promise<any> {
|
||||||
|
const id = this.nextId++;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.pending.set(id, { resolve, reject });
|
||||||
|
this.write({ jsonrpc: "2.0", id, method, params });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async callTool(name: string, args: Record<string, unknown>): Promise<any> {
|
||||||
|
return this.request("tools/call", { name, arguments: args });
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.proc) {
|
||||||
|
try {
|
||||||
|
this.proc.kill("SIGTERM");
|
||||||
|
} catch {}
|
||||||
|
this.proc = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function mempalaceExtension(pi: ExtensionAPI) {
|
||||||
|
const client = new McpClient();
|
||||||
|
let available = false;
|
||||||
|
const agentName = process.env.MEMPALACE_AGENT_NAME ?? "pi";
|
||||||
|
|
||||||
|
// Gate: inject wake-up context only on the first before_agent_start of a
|
||||||
|
// fresh session. Set true on resume/fork (context already in thread).
|
||||||
|
let wokeUp = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.start("mempalace-mcp");
|
||||||
|
available = true;
|
||||||
|
} catch (err) {
|
||||||
|
process.stderr.write(
|
||||||
|
`[mempalace ext] failed to start mempalace-mcp: ${(err as Error).message}\n`,
|
||||||
|
);
|
||||||
|
return; // fail-soft: pi keeps working without palace tools
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register MCP tools as pi tools. Pass the MCP `inputSchema` through as
|
||||||
|
// the pi `parameters` schema so the LLM sees the real parameter names
|
||||||
|
// (e.g. `agent_name`, not guessed `agent`). TypeBox schemas are plain
|
||||||
|
// JSON Schema at runtime, so `Type.Unsafe` is sufficient to wrap an
|
||||||
|
// externally-sourced JSON Schema — no conversion needed.
|
||||||
|
for (const tool of client.tools) {
|
||||||
|
const schema =
|
||||||
|
tool.inputSchema && typeof tool.inputSchema === "object"
|
||||||
|
? (Type.Unsafe<Record<string, unknown>>(tool.inputSchema as object) as unknown as ReturnType<typeof Type.Object>)
|
||||||
|
: Type.Object({}, { additionalProperties: true });
|
||||||
|
pi.registerTool({
|
||||||
|
name: tool.name,
|
||||||
|
label: tool.name,
|
||||||
|
description: tool.description ?? `MemPalace tool: ${tool.name}`,
|
||||||
|
parameters: schema,
|
||||||
|
async execute(_toolCallId, params) {
|
||||||
|
if (!available) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "mempalace-mcp not available" }],
|
||||||
|
details: {},
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await client.callTool(tool.name, (params ?? {}) as Record<string, unknown>);
|
||||||
|
// MCP tool results use { content: [...], isError?: boolean }
|
||||||
|
return {
|
||||||
|
content: result?.content ?? [{ type: "text", text: JSON.stringify(result) }],
|
||||||
|
details: { raw: result },
|
||||||
|
isError: result?.isError === true,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `MCP call failed: ${(err as Error).message}` }],
|
||||||
|
details: {},
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pi.on("session_shutdown", async () => {
|
||||||
|
client.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("session_start", async (event, ctx) => {
|
||||||
|
// On resume/fork, the previous session's palace context is already in
|
||||||
|
// the thread — skip the wake-up injection.
|
||||||
|
if (event.reason === "resume" || event.reason === "fork") {
|
||||||
|
wokeUp = true;
|
||||||
|
}
|
||||||
|
ctx.ui.notify(
|
||||||
|
`mempalace bridge: ${client.tools.length} tools registered (agent=${agentName})`,
|
||||||
|
"info",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Auto wake-up (mempalace skill Phase 1) ---
|
||||||
|
pi.on("before_agent_start", async (_event, _ctx) => {
|
||||||
|
if (wokeUp || !available) return;
|
||||||
|
wokeUp = true; // one-shot, even if the calls below fail
|
||||||
|
|
||||||
|
const sections: string[] = [];
|
||||||
|
try {
|
||||||
|
const status = await client.callTool("mempalace_status", {});
|
||||||
|
const text = extractText(status);
|
||||||
|
if (text) sections.push(`## mempalace_status\n\n${text}`);
|
||||||
|
} catch (err) {
|
||||||
|
sections.push(`## mempalace_status\n\n(error: ${(err as Error).message})`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const diary = await client.callTool("mempalace_diary_read", {
|
||||||
|
agent_name: agentName,
|
||||||
|
last_n: 5,
|
||||||
|
});
|
||||||
|
const text = extractText(diary);
|
||||||
|
if (text) sections.push(`## mempalace_diary_read (agent=${agentName}, last_n=5)\n\n${text}`);
|
||||||
|
} catch (err) {
|
||||||
|
sections.push(`## mempalace_diary_read\n\n(error: ${(err as Error).message})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sections.length === 0) return;
|
||||||
|
|
||||||
|
const body =
|
||||||
|
`MemPalace wake-up context (auto-injected by the mempalace extension). ` +
|
||||||
|
`This is your palace orientation for this session — do not announce it to the user, ` +
|
||||||
|
`just use it to inform your answers. Agent identity for diary tools: "${agentName}".\n\n` +
|
||||||
|
sections.join("\n\n---\n\n");
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
customType: "mempalace-wakeup",
|
||||||
|
content: body,
|
||||||
|
display: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Manual wind-down (mempalace skill Phase 3) ---
|
||||||
|
pi.registerCommand("mempalace-diary", {
|
||||||
|
description: "Ask the LLM to write an AAAK diary entry summarizing this session",
|
||||||
|
handler: async (args, ctx) => {
|
||||||
|
if (!available) {
|
||||||
|
ctx.ui.notify("mempalace bridge not available", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const topic = args.trim() || "session-summary";
|
||||||
|
const prompt =
|
||||||
|
`Write a MemPalace diary entry for this session using the AAAK format ` +
|
||||||
|
`described in the mempalace skill. Call mempalace_diary_write with ` +
|
||||||
|
`agent_name="${agentName}", topic="${topic}", and a compressed AAAK entry ` +
|
||||||
|
`that summarizes what we worked on, what was discovered, and any open ` +
|
||||||
|
`threads. Then confirm the write succeeded. Do not ask me for ` +
|
||||||
|
`clarification — draft from the session so far.`;
|
||||||
|
pi.sendUserMessage(prompt);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Flatten MCP tool result content into plain text for context injection. */
|
||||||
|
function extractText(mcpResult: any): string {
|
||||||
|
const parts = mcpResult?.content;
|
||||||
|
if (!Array.isArray(parts)) return typeof mcpResult === "string" ? mcpResult : "";
|
||||||
|
return parts
|
||||||
|
.map((p: any) => (typeof p?.text === "string" ? p.text : ""))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
+52
@@ -16,6 +16,11 @@ SKILL_SRC="${SCRIPT_DIR}/SKILL.md"
|
|||||||
SKILL_DEST_DIR="${HOME}/.agents/skills/opencode-mempalace-bridge"
|
SKILL_DEST_DIR="${HOME}/.agents/skills/opencode-mempalace-bridge"
|
||||||
SKILL_DEST="${SKILL_DEST_DIR}/SKILL.md"
|
SKILL_DEST="${SKILL_DEST_DIR}/SKILL.md"
|
||||||
|
|
||||||
|
# pi coding-agent extension (optional — only linked if pi is installed)
|
||||||
|
PI_EXT_SRC="${SCRIPT_DIR}/extensions/pi/mempalace.ts"
|
||||||
|
PI_EXT_DEST_DIR="${HOME}/.pi/agent/extensions"
|
||||||
|
PI_EXT_DEST="${PI_EXT_DEST_DIR}/mempalace.ts"
|
||||||
|
|
||||||
# ── args ─────────────────────────────────────────────
|
# ── args ─────────────────────────────────────────────
|
||||||
ACTION="install"
|
ACTION="install"
|
||||||
ASSUME_YES="no"
|
ASSUME_YES="no"
|
||||||
@@ -38,12 +43,15 @@ What install does:
|
|||||||
- Symlinks SKILL.md into ~/.agents/skills/opencode-mempalace-bridge/SKILL.md
|
- Symlinks SKILL.md into ~/.agents/skills/opencode-mempalace-bridge/SKILL.md
|
||||||
(auto-discovered by opencode; run agents-sync from cli_utils to also
|
(auto-discovered by opencode; run agents-sync from cli_utils to also
|
||||||
reach Claude Code and Kiro)
|
reach Claude Code and Kiro)
|
||||||
|
- If pi (~/.pi/agent/extensions/) exists, symlinks extensions/pi/mempalace.ts
|
||||||
|
into ~/.pi/agent/extensions/mempalace.ts (pi bridge). Skipped otherwise.
|
||||||
- Drops a .skill-source marker in the skill dir so sibling tooling
|
- Drops a .skill-source marker in the skill dir so sibling tooling
|
||||||
(deploy-skills.sh, agents-sync.zsh) knows the dir is externally owned
|
(deploy-skills.sh, agents-sync.zsh) knows the dir is externally owned
|
||||||
|
|
||||||
What uninstall does:
|
What uninstall does:
|
||||||
- Removes symlinks in ~/.local/bin/ that point into this repo
|
- Removes symlinks in ~/.local/bin/ that point into this repo
|
||||||
- Removes the skill symlink if it points into this repo
|
- Removes the skill symlink if it points into this repo
|
||||||
|
- Removes the pi extension symlink if it points into this repo
|
||||||
- Removes the .skill-source marker and empty skill dir
|
- Removes the .skill-source marker and empty skill dir
|
||||||
EOF
|
EOF
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
@@ -219,6 +227,36 @@ check_opencode_mcp() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install_pi_extension() {
|
||||||
|
# The pi coding-agent extension is optional: link it only if pi is
|
||||||
|
# already installed on this machine (its ~/.pi/agent/extensions/
|
||||||
|
# directory exists). Otherwise silently skip — mempalace-toolkit is
|
||||||
|
# useful on opencode-only boxes too.
|
||||||
|
if [[ ! -d "$PI_EXT_DEST_DIR" ]]; then
|
||||||
|
note "pi not detected at $PI_EXT_DEST_DIR — skipping pi extension"
|
||||||
|
printf ' (install pi first if you want the pi↔mempalace bridge:\n'
|
||||||
|
printf ' https://github.com/mariozechner/pi-coding-agent)\n'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
note "Linking pi extension into $PI_EXT_DEST_DIR"
|
||||||
|
if [[ -e "$PI_EXT_DEST" || -L "$PI_EXT_DEST" ]]; then
|
||||||
|
if link_if_into_repo "$PI_EXT_DEST"; then
|
||||||
|
ok "pi extension already linked"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# Non-symlink (or foreign symlink) in the way — back it up rather
|
||||||
|
# than clobber. User may have local edits they want to preserve.
|
||||||
|
local backup="${PI_EXT_DEST}.bak.$(date +%Y%m%d-%H%M%S)"
|
||||||
|
mv "$PI_EXT_DEST" "$backup"
|
||||||
|
warn "Existing $PI_EXT_DEST backed up to $backup"
|
||||||
|
fi
|
||||||
|
ln -s "$PI_EXT_SRC" "$PI_EXT_DEST"
|
||||||
|
ok "Linked mempalace.ts → $PI_EXT_SRC"
|
||||||
|
printf ' Restart pi to load the extension (it reads ~/.pi/agent/extensions/\n'
|
||||||
|
printf ' at startup only).\n'
|
||||||
|
}
|
||||||
|
|
||||||
do_install() {
|
do_install() {
|
||||||
echo
|
echo
|
||||||
echo "mempalace-toolkit installer"
|
echo "mempalace-toolkit installer"
|
||||||
@@ -227,6 +265,9 @@ do_install() {
|
|||||||
echo "==> Installation plan:"
|
echo "==> Installation plan:"
|
||||||
echo " Symlink executables in bin/ into $BIN_DEST"
|
echo " Symlink executables in bin/ into $BIN_DEST"
|
||||||
echo " Symlink SKILL.md into $SKILL_DEST"
|
echo " Symlink SKILL.md into $SKILL_DEST"
|
||||||
|
if [[ -d "$PI_EXT_DEST_DIR" ]]; then
|
||||||
|
echo " Symlink extensions/pi/mempalace.ts into $PI_EXT_DEST"
|
||||||
|
fi
|
||||||
echo
|
echo
|
||||||
confirm || { echo "Aborted."; exit 0; }
|
confirm || { echo "Aborted."; exit 0; }
|
||||||
echo
|
echo
|
||||||
@@ -234,6 +275,8 @@ do_install() {
|
|||||||
echo
|
echo
|
||||||
install_skill
|
install_skill
|
||||||
echo
|
echo
|
||||||
|
install_pi_extension
|
||||||
|
echo
|
||||||
check_path
|
check_path
|
||||||
echo
|
echo
|
||||||
check_wake_up_protocol
|
check_wake_up_protocol
|
||||||
@@ -278,6 +321,15 @@ do_uninstall() {
|
|||||||
ok "No skill symlink to remove"
|
ok "No skill symlink to remove"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
note "Removing pi extension symlink"
|
||||||
|
if link_if_into_repo "$PI_EXT_DEST"; then
|
||||||
|
rm "$PI_EXT_DEST"
|
||||||
|
ok "Removed pi extension symlink"
|
||||||
|
else
|
||||||
|
ok "No pi extension symlink to remove"
|
||||||
|
fi
|
||||||
|
|
||||||
# Remove the marker and the now-empty skill directory, but only if
|
# Remove the marker and the now-empty skill directory, but only if
|
||||||
# the marker was written by us and the directory has nothing else in it.
|
# the marker was written by us and the directory has nothing else in it.
|
||||||
local marker="$SKILL_DEST_DIR/.skill-source"
|
local marker="$SKILL_DEST_DIR/.skill-source"
|
||||||
|
|||||||
Reference in New Issue
Block a user