diff --git a/scripts/install-skills.sh b/scripts/install-skills.sh index 321ea01..ba9c46c 100755 --- a/scripts/install-skills.sh +++ b/scripts/install-skills.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Install (or uninstall) agent-memory skills for Claude Code, Codex, Cursor, and Agent CLI. +# Install (or uninstall) agent-memory skills for Claude Code, Codex, Cursor, Agent CLI, pi, and Qoder. # Usage: bash scripts/install-skills.sh [--uninstall] set -euo pipefail @@ -63,12 +63,14 @@ SKILL_DIRS=( "$HOME/.codex/skills/agent-memory" "$HOME/.cursor/skills/agent-memory" "$HOME/.agents/skills/agent-memory" + "$HOME/.qoder/skills/agent-memory" ) SKILL_LABELS=( "Claude Code skill" "Codex skill" "Cursor skill" "Agent CLI skill" + "Qoder skill" ) if $UNINSTALL; then @@ -77,6 +79,13 @@ if $UNINSTALL; then for i in "${!SKILL_DIRS[@]}"; do uninstall_skill "${SKILL_LABELS[$i]}" "${SKILL_DIRS[$i]}" done + # pi extension (has .ts not .md) + if [ -f "$HOME/.pi/extensions/agent-memory.ts" ]; then + rm "$HOME/.pi/extensions/agent-memory.ts" + echo "Uninstalled pi extension: $HOME/.pi/extensions/agent-memory.ts" + else + echo "Skipping pi extension (not installed)" + fi echo "" echo "Done." else @@ -84,6 +93,25 @@ else install_skill "Codex skill" "$PROJECT_DIR/skills/codex" "$HOME/.codex/skills/agent-memory" "$HOME/.codex" '[ -f "$HOME/.codex/config.toml" ] || command_exists codex' install_skill "Cursor skill" "$PROJECT_DIR/skills/cursor" "$HOME/.cursor/skills/agent-memory" "$HOME/.cursor" install_skill "Agent CLI skill" "$PROJECT_DIR/skills/agent" "$HOME/.agents/skills/agent-memory" "$HOME/.agents" + install_skill "Qoder skill" "$PROJECT_DIR/skills/qoder" "$HOME/.qoder/skills/agent-memory" "$HOME/.qoder" '[ -f "$HOME/.qoder/settings.json" ] || [ -f "$HOME/.qoder/settings.local.json" ] || command_exists qoder' + + # pi extension (copies extension.ts → agent-memory.ts) + echo "Detecting pi extension..." + if [ ! -d "$HOME/.pi" ]; then + echo "Not found ($HOME/.pi not found)" + elif [ -z "$(command_exists pi)" ] && [ ! -d "$HOME/.pi/extensions" ]; then + echo "Not found (not detected)" + else + echo "Found" + if [ -d "$PROJECT_DIR/skills/pi" ]; then + mkdir -p "$HOME/.pi/extensions" + cp "$PROJECT_DIR/skills/pi/extension.ts" "$HOME/.pi/extensions/agent-memory.ts" + echo "Installed pi extension: $HOME/.pi/extensions/agent-memory.ts" + else + echo "Skipping pi extension ($PROJECT_DIR/skills/pi not found)" + fi + fi + echo "" echo "Done." fi @@ -110,4 +138,4 @@ else echo " $AGENT_MEMORY_BIN" fi echo "" -echo "Initialize memory: agent-memory init" +echo "Initialize memory: agent-memory init" \ No newline at end of file diff --git a/skills/pi/extension.ts b/skills/pi/extension.ts new file mode 100644 index 0000000..82ab065 --- /dev/null +++ b/skills/pi/extension.ts @@ -0,0 +1,22 @@ +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +/** + * AgentMemory extension for Pi Coding Agent. + * + * Subscribes to session_start and runs `agent-memory context` to inject + * persistent memory at the start of every session. + */ +export default function (pi: ExtensionAPI) { + pi.on("session_start", async (_event, ctx) => { + try { + const result = await pi.exec("agent-memory", ["context"], { + timeout: 10000, + }); + if (result.stdout?.trim()) { + ctx.ui.notify(`Memory loaded: ${result.stdout.split("\n").length} lines`, "info"); + } + } catch { + // agent-memory not installed or not on PATH — skip silently + } + }); +} diff --git a/skills/qoder/SKILL.md b/skills/qoder/SKILL.md new file mode 100644 index 0000000..e1fe225 --- /dev/null +++ b/skills/qoder/SKILL.md @@ -0,0 +1,164 @@ +--- +name: agent-memory +description: Persistent memory across coding sessions — long-term facts, daily logs, topic notes, scratchpad checklist, and semantic search. +--- + +# Agent Memory + +You have a persistent memory system. Use it **proactively** — don't wait to be asked. + +## Current Memory Context + +Run this to load memory context at session start: + +```bash +agent-memory context --no-search 2>/dev/null +``` + +## Session Lifecycle + +### On session start +1. Run `agent-memory context` to load memory — especially check **open scratchpad items** (pick up where you left off) +2. If the user's task relates to prior work, search for relevant memories: + ```bash + agent-memory search --query "" --mode keyword + ``` + +### On session end (after significant work) +1. Log what was accomplished in the daily log +2. Mark completed scratchpad items as done; add new follow-ups +3. Only write to long-term memory if you discovered a **durable fact** that doesn't already exist there + +## Where to Write — Decision Guide + +**Default to daily. Long-term is rare.** + +| What happened | Write to | Why | +|---|---|---| +| Made progress, fixed a bug, investigated something | `daily` | Session-specific — searchable later via qmd | +| Tracking a topic or event across days | `topic` | Builds a per-topic file with backlinks to daily logs | +| User said "remember this" about a preference or decision | `long_term` | Durable fact, needs to be in every session's context | +| Discovered a recurring pattern (3rd time seeing it) | `long_term` | Graduated from daily observations to established fact | +| Found a gotcha, workaround, or non-obvious behavior | `daily` first | If it keeps coming up, *then* promote to long-term | +| TODO or follow-up for any task (persistent todo) | `scratchpad` | Persistent, cross-session task tracking | + +**MEMORY.md is a curated wiki, not a log.** It should stay under ~50 lines of high-signal content. If you're appending to it frequently, you're probably writing to the wrong target. + +## Memory Commands + +### Write to daily log (default — no --target needed) + +```bash +# Session notes, progress, bugs found, decisions made +agent-memory write --content "Fixed auth bug in login.ts — token refresh was missing" +agent-memory write --content "Investigated slow queries — N+1 in getUserOrders, added .include(:orders)" +``` + +### Write to long-term memory (rare, curated) + +```bash +# Only for durable facts that belong in every session's context +agent-memory write --target long_term --content "Project uses Drizzle ORM with PostgreSQL. Migrations in db/migrations/. #architecture" + +# Overwrite MEMORY.md entirely (for curation — rewrite, don't append) +agent-memory write --target long_term --content "..." --mode overwrite +``` + +When writing to long-term, prefer **overwrite mode** to curate the whole file rather than blindly appending. Read it first, then rewrite with the new fact incorporated. + +### Write to a topic/event file + +```bash +# Event- or theme-based log with backlinks to the daily entry +agent-memory write --target topic --topic "auth" --content "JWT refresh rolled out to edge #auth" +``` + +### Read + +```bash +agent-memory read --target daily # Today's log +agent-memory read --target daily --date 2026-02-15 # Specific day +agent-memory read --target list # All daily log files +agent-memory read --target topic --topic "auth" +agent-memory read --target topics # All topic files +agent-memory read --target long_term # MEMORY.md +agent-memory read --target scratchpad # Scratchpad checklist +``` + +### Scratchpad (persistent TODOs) + +```bash +agent-memory scratchpad add --text "Review PR #42" +agent-memory scratchpad list +agent-memory scratchpad done --text "PR #42" # Matches by substring +agent-memory scratchpad undo --text "PR #42" +agent-memory scratchpad clear_done # Remove completed items +``` + +### Search — recall past work + +Search is how you find things written to daily logs. Use it before duplicating effort. + +```bash +agent-memory search --query "database choice" --mode keyword # Fast keyword +agent-memory search --query "how we handle auth" --mode semantic # Finds related concepts +agent-memory search --query "performance" --mode deep --limit 10 # Hybrid + reranking +``` + +If qmd is not installed, fall back to reading files directly: +```bash +agent-memory read --target long_term +agent-memory read --target daily +``` + +### Setup + +```bash +agent-memory init # Create dirs, detect qmd, setup collection +agent-memory sync # Re-index and embed all files (requires qmd) +agent-memory status # Show config, file counts, qmd status +``` + +## Writing Good Entries + +### Daily log entries +Describe what you did and what you learned. Include `#tags`. + +**Recommended tags** (use what fits, invent your own as needed): +`#architecture` `#auth` `#bugfix` `#database` `#deploy` `#docs` `#ops` `#perf` `#refactor` `#security` `#testing` `#ui` + +```bash +# Good — specific, searchable, tagged +agent-memory write --content "Refactored auth middleware to use jose instead of jsonwebtoken. Reduced bundle by 40KB. #refactor #auth" + +# Bad — too vague, no tags +agent-memory write --content "worked on auth stuff" +``` + +### Long-term entries +Only facts that should appear in **every** session's context. Use `#tags` and `[[links]]`. + +```bash +# Good — this belongs in every session +agent-memory write --target long_term --content "Deploy: 'bun run deploy:prod', requires AWS_PROFILE=prod. #ops [[deploy]]" + +# Bad — this is a daily log entry, not a durable fact +agent-memory write --target long_term --content "Fixed the deploy script today" +``` + +## Memory Hygiene + +- **Daily is the default** — when in doubt, write to daily (no `--target` needed) +- **MEMORY.md is a wiki** — curate it by reading + rewriting, not by appending endlessly +- **Keep MEMORY.md under ~50 lines** — it's injected into every session, so only high-signal facts belong there +- **Search before writing long-term** — the fact may already exist in a daily log, searchable via qmd +- **Promote deliberately** — if a pattern appears in daily logs 3+ times, that's when it earns a spot in MEMORY.md + +## Guidelines + +- When someone says "remember this", decide: is it a durable fact (long-term) or a session note (daily)? +- Default to daily for almost everything (just `--content "..."` — no `--target` needed) +- Use `--target long_term` sparingly: architecture, preferences, key commands, hard-won lessons +- Prefer the scratchpad for any TODOs or follow-ups (persistent, cross-session tracking) +- Use `#tags` and `[[links]]` in content to improve search recall +- Use `agent-memory search` to recall past work before starting related tasks \ No newline at end of file diff --git a/src/core.ts b/src/core.ts index d2042fc..f056b2b 100644 --- a/src/core.ts +++ b/src/core.ts @@ -985,6 +985,24 @@ export function installSkills(): InstallSkillsReport { destDir: path.join(homeDir, ".cursor", "skills", "agent-memory"), homeMarker: path.join(homeDir, ".cursor"), }, + { + label: "pi extension", + srcDir: path.join(skillsDir, "pi"), + destDir: path.join(homeDir, ".pi", "extensions"), + homeMarker: path.join(homeDir, ".pi"), + detectCommand: "pi", + }, + { + label: "Qoder skill", + srcDir: path.join(skillsDir, "qoder"), + destDir: path.join(homeDir, ".qoder", "skills", "agent-memory"), + homeMarker: path.join(homeDir, ".qoder"), + detectFiles: [ + path.join(homeDir, ".qoder", "settings.json"), + path.join(homeDir, ".qoder", "settings.local.json"), + ], + detectCommand: "qoder", + }, { label: "Agent CLI skill", srcDir: path.join(skillsDir, "agent"), @@ -1016,15 +1034,17 @@ export function installSkills(): InstallSkillsReport { detected.push({ label: target.label, homeMarker: target.homeMarker }); checked.push({ label: target.label, status: "detected" }); - const skillFile = path.join(target.srcDir, "SKILL.md"); + const isPiExt = target.label === "pi extension"; + const skillFile = path.join(target.srcDir, isPiExt ? "extension.ts" : "SKILL.md"); if (!fs.existsSync(skillFile)) { skipped.push({ label: target.label, reason: `${skillFile} not found` }); continue; } fs.mkdirSync(target.destDir, { recursive: true }); - fs.copyFileSync(skillFile, path.join(target.destDir, "SKILL.md")); - installed.push({ label: target.label, path: path.join(target.destDir, "SKILL.md") }); + const destFile = path.join(target.destDir, isPiExt ? "agent-memory.ts" : "SKILL.md"); + fs.copyFileSync(skillFile, destFile); + installed.push({ label: target.label, path: destFile }); } return { diff --git a/src/hooks.ts b/src/hooks.ts index 44b216f..b9beb01 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -36,7 +36,7 @@ function commandExists(command: string): boolean { // Hook installers (SessionStart auto-injection) // --------------------------------------------------------------------------- -export type HookAgentKey = "claude" | "codex" | "cursor" | "opencode" | "pi"; +export type HookAgentKey = "claude" | "codex" | "cursor" | "opencode" | "pi" | "qoder"; export interface HookTargetInfo { key: HookAgentKey; @@ -101,10 +101,20 @@ function hookTargets(homeDir: string): HookTargetInfo[] { key: "pi", label: "pi", homeMarker: path.join(homeDir, ".pi"), - detectFiles: [], + detectFiles: [path.join(homeDir, ".pi", "extensions")], detectCommand: "pi", - supported: false, - unsupportedReason: "no documented SessionStart hook mechanism", + supported: true, + }, + { + key: "qoder", + label: "Qoder", + homeMarker: path.join(homeDir, ".qoder"), + detectFiles: [ + path.join(homeDir, ".qoder", "settings.json"), + path.join(homeDir, ".qoder", "settings.local.json"), + ], + detectCommand: "qoder", + supported: true, }, ]; } @@ -305,6 +315,134 @@ function installOpencodeInstructions(homeDir: string): HookInstallResult { return { key: "opencode", label: "opencode", installed: true, path: configPath, backup }; } +const PI_EXTENSION_SOURCE = `import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +/** + * AgentMemory extension for Pi Coding Agent. + * + * Subscribes to session_start and runs \`agent-memory context\` to inject + * persistent memory at the start of every session. + */ +export default function (pi: ExtensionAPI) { + pi.on("session_start", async (_event, ctx) => { + try { + const result = await pi.exec("agent-memory", ["context"], { + timeout: 10000, + }); + if (result.stdout?.trim()) { + ctx.ui.notify( + \`Memory loaded: \${result.stdout.split("\\n").length} lines\`, + "info", + ); + } + } catch { + // agent-memory not installed or not on PATH — skip silently + } + }); +}`; + +function installPiExtension(homeDir: string): HookInstallResult { + const extDir = path.join(homeDir, ".pi", "extensions"); + const destPath = path.join(extDir, "agent-memory.ts"); + if (fs.existsSync(destPath)) { + return { key: "pi", label: "pi", installed: false, path: destPath, reason: "already installed" }; + } + fs.mkdirSync(extDir, { recursive: true }); + fs.writeFileSync(destPath, PI_EXTENSION_SOURCE, "utf-8"); + return { key: "pi", label: "pi", installed: true, path: destPath }; +} + +function uninstallPiExtension(homeDir: string): HookInstallResult { + const destPath = path.join(homeDir, ".pi", "extensions", "agent-memory.ts"); + if (!fs.existsSync(destPath)) { + return { key: "pi", label: "pi", installed: false, reason: "not installed" }; + } + fs.unlinkSync(destPath); + return { key: "pi", label: "pi", installed: true, path: destPath }; +} + +function installQoderHook(homeDir: string): HookInstallResult { + const settingsPath = path.join(homeDir, ".qoder", "settings.json"); + const backup = backupOnce(settingsPath); + const settings = readJsonConfig(settingsPath); + const hooks = (settings.hooks as Record) ?? {}; + const sessionStart = Array.isArray(hooks.SessionStart) ? [...(hooks.SessionStart as unknown[])] : []; + + const command = "agent-memory context"; + let managed = 0; + let updated = 0; + for (const group of sessionStart) { + if (!group || typeof group !== "object") continue; + const g = group as Record; + const list = Array.isArray(g.hooks) ? (g.hooks as unknown[]) : []; + for (const hook of list) { + if (!hook || typeof hook !== "object") continue; + const managedHook = hook as Record; + if (managedHook[HOOK_MARKER_JSON] !== true) continue; + managed++; + if (managedHook.command !== command) { + managedHook.command = command; + updated++; + } + } + } + if (managed && !updated) { + return { key: "qoder", label: "Qoder", installed: false, path: settingsPath, reason: "already installed" }; + } + if (updated) { + hooks.SessionStart = sessionStart; + settings.hooks = hooks; + writeJson(settingsPath, settings); + return { key: "qoder", label: "Qoder", installed: true, path: settingsPath, backup, reason: "updated" }; + } + + sessionStart.push({ + hooks: [{ type: "command", command, [HOOK_MARKER_JSON]: true }], + }); + hooks.SessionStart = sessionStart; + settings.hooks = hooks; + writeJson(settingsPath, settings); + return { key: "qoder", label: "Qoder", installed: true, path: settingsPath, backup }; +} + +function uninstallQoderHook(homeDir: string): HookInstallResult { + const settingsPath = path.join(homeDir, ".qoder", "settings.json"); + if (!fs.existsSync(settingsPath)) { + return { key: "qoder", label: "Qoder", installed: false, reason: "not installed" }; + } + const settings = readJsonConfig(settingsPath); + const hooks = (settings.hooks as Record) ?? {}; + const sessionStart = Array.isArray(hooks.SessionStart) ? (hooks.SessionStart as unknown[]) : []; + let removed = 0; + const filtered = sessionStart + .map((group) => { + if (!group || typeof group !== "object") return group; + const g = { ...(group as Record) }; + const list = Array.isArray(g.hooks) ? (g.hooks as unknown[]) : []; + const kept = list.filter((h) => { + const isOurs = h && typeof h === "object" && (h as Record)[HOOK_MARKER_JSON] === true; + if (isOurs) removed++; + return !isOurs; + }); + g.hooks = kept; + return g; + }) + .filter((group) => { + if (!group || typeof group !== "object") return true; + const g = group as Record; + return Array.isArray(g.hooks) && (g.hooks as unknown[]).length > 0; + }); + if (removed === 0) { + return { key: "qoder", label: "Qoder", installed: false, reason: "not installed" }; + } + hooks.SessionStart = filtered; + if (filtered.length === 0) delete (hooks as Record).SessionStart; + if (Object.keys(hooks).length === 0) delete (settings as Record).hooks; + else settings.hooks = hooks; + writeJson(settingsPath, settings); + return { key: "qoder", label: "Qoder", installed: true, path: settingsPath }; +} + export function installHooks(agents: Set): InstallHooksReport { const { homeDir, targets } = detectHookAgents(); if (!homeDir) { @@ -341,6 +479,8 @@ export function installHooks(agents: Set): InstallHooksReport { else if (target.key === "codex") results.push(installCodexHook(homeDir)); else if (target.key === "cursor") results.push(installCursorRule(homeDir)); else if (target.key === "opencode") results.push(installOpencodeInstructions(homeDir)); + else if (target.key === "qoder") results.push(installQoderHook(homeDir)); + else if (target.key === "pi") results.push(installPiExtension(homeDir)); } catch (err) { results.push({ key: target.key, @@ -463,7 +603,7 @@ export function uninstallHooks(agents?: Set): UninstallHooksReport error: "Home directory not found. Set HOME (or USERPROFILE on Windows) and retry.", }; } - const keys: HookAgentKey[] = ["claude", "codex", "cursor", "opencode"]; + const keys: HookAgentKey[] = ["claude", "codex", "cursor", "opencode", "qoder", "pi"]; const results: HookInstallResult[] = []; for (const key of keys) { if (agents && !agents.has(key)) continue; @@ -472,6 +612,8 @@ export function uninstallHooks(agents?: Set): UninstallHooksReport else if (key === "codex") results.push(uninstallCodexHook(homeDir)); else if (key === "cursor") results.push(uninstallCursorRule(homeDir)); else if (key === "opencode") results.push(uninstallOpencodeInstructions(homeDir)); + else if (key === "qoder") results.push(uninstallQoderHook(homeDir)); + else if (key === "pi") results.push(uninstallPiExtension(homeDir)); } catch (err) { results.push({ key,