myagentmemory 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -26,13 +26,13 @@ import * as fs from "node:fs";
26
26
  import * as os from "node:os";
27
27
  import * as path from "node:path";
28
28
  import { COMMAND_DESCRIPTIONS, COMMAND_OPTIONS, COMMANDS, GLOBAL_OPTIONS, optionTakesValue, PLUGIN_COMMAND_OPTIONS, renderCommandHelp, SCRATCHPAD_ACTION_OPTIONS, } from "./cli-spec.js";
29
- import { detectCompletionShell, generateCompletion, installCompletion } from "./completions.js";
29
+ import { detectCompletionShell, generateCompletion, installCompletion, uninstallCompletion, } from "./completions.js";
30
30
  import { _setBaseDir, buildDynamicContext, buildMemoryContext, buildStableContext, checkCollection, dailyPath, detectQmd, distilMemories, ensureDirs, ensureQmdAvailableForSync, ensureQmdAvailableForUpdate, getCollectionName, getDailyDir, getMemoryDir, getMemoryFile, getQmdEmbedMode, getQmdHealth, getQmdResultPath, getQmdResultText, getScratchpadFile, getTopicsDir, installSkills, memoryWrite, nowTimestamp, parseScratchpad, probeEmbeddings, readFileSafe, readHookMode, redactSecrets, runQmdEmbedDetached, runQmdSearch, runQmdSync, runQmdUpdateNow, scheduleQmdUpdate, scratchpadAction, searchRelevantMemories, serializeScratchpad, setupQmdCollection, slugifyTopic, todayStr, topicPath, uninstallSkills, } from "./core.js";
31
- import { detectHookAgents, installHooks, isHookInstalled, isStopHookInstalled, isUserPromptSubmitInstalled, uninstallHooks, } from "./hooks.js";
31
+ import { detectHookAgents, getPiMemoryState, installHooks, isHookInstalled, isStopHookInstalled, isUserPromptSubmitInstalled, uninstallHooks, } from "./hooks.js";
32
32
  import { StdioMcpServer } from "./mcp-server.js";
33
- import { createDefaultPluginBootstrap, PluginBootstrapFailure, } from "./plugin-bootstrap.js";
33
+ import { createDefaultPluginBootstrap, getDefaultPluginInstallRoot, PluginBootstrapFailure, } from "./plugin-bootstrap.js";
34
34
  import { InstalledPluginRuntimeV1 } from "./plugin-runtime.js";
35
- import { checkForUpgrades, detectInstallMethod, formatUpgradeNotice, isCacheFresh, readUpgradeCache, refreshUpgradeCacheBackground, runInstaller, } from "./upgrade.js";
35
+ import { checkForUpgrades, detectInstallMethod, formatUpgradeNotice, isCacheFresh, readUpgradeCache, readUpgradePolicy, refreshUpgradeCacheBackground, runInstaller, writeUpgradeCache, writeUpgradePolicy, } from "./upgrade.js";
36
36
  function readPackageVersion() {
37
37
  try {
38
38
  const packageJson = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
@@ -124,10 +124,6 @@ function levenshtein(a, b) {
124
124
  }
125
125
  return prev[b.length];
126
126
  }
127
- // ---------------------------------------------------------------------------
128
- // Pro plan / cap-exhausted UX
129
- // ---------------------------------------------------------------------------
130
- const UPGRADE_URL = "https://agentmemory.paperpilot.me/upgrade";
131
127
  function detectCapExhausted(result) {
132
128
  if (result.ok !== false)
133
129
  return null;
@@ -194,13 +190,11 @@ function printCapExhaustedBox(command, info) {
194
190
  "─────────────────────────────────────────────────────────────",
195
191
  usedLine,
196
192
  "",
197
- " Upgrade for unlimited recall + automatic capture:",
198
- ` ${UPGRADE_URL}`,
193
+ " Paid plans are not available yet. Try again after the free-preview allowance resets.",
199
194
  "─────────────────────────────────────────────────────────────",
200
195
  "",
201
196
  ];
202
197
  console.error(lines.join("\n"));
203
- openExternalUrl(UPGRADE_URL);
204
198
  }
205
199
  // Persist the last usage decision to disk so `pro status` can show counters.
206
200
  function cacheProUsage(decision) {
@@ -394,9 +388,9 @@ function printProOverview(installed) {
394
388
  console.log("Core remembers what you save. Pro learns from what you do.");
395
389
  console.log("");
396
390
  console.log("AgentMemory Pro:");
397
- console.log(' Remember past sessions Ask "what did we decide about auth?" across Claude Code, Codex, and Cursor.');
391
+ console.log(' Remember past sessions Ask "what did we decide about auth?" across Claude Code, Codex, and Pi history.');
398
392
  console.log(" Learn from your patterns Turn repeated corrections into memory you can inspect and undo.");
399
- console.log(" Private by default Memory and session content index locally and never leave this machine.");
393
+ console.log(" Private by default AgentMemory services never receive memory or session content.");
400
394
  console.log("");
401
395
  if (installed) {
402
396
  printProUsageCounters();
@@ -443,7 +437,7 @@ function printPluginResult(result, json, allowBrowser) {
443
437
  console.log(`AgentMemory Pro${version} has an update available.`);
444
438
  break;
445
439
  case "uninstalled":
446
- console.log("AgentMemory Pro executable components were removed. Memory and billing state were preserved.");
440
+ console.log("AgentMemory Pro executable components were removed. Memory and local activation state were preserved.");
447
441
  break;
448
442
  case "not_installed":
449
443
  console.log("AgentMemory Pro is not installed.");
@@ -674,10 +668,15 @@ const STOP_NAG_REASON = "Before stopping: if this session produced a durable fac
674
668
  "ignore this and stop normally.";
675
669
  /**
676
670
  * Stop hook handler — fires at the end of every assistant turn (not once per
677
- * session). Blocks at most once every STOP_NAG_INTERVAL turns per session_id
678
- * to nudge a memory-write check without being disruptive. Always allows the
679
- * stop (empty stdout) on missing session_id, `stop_hook_active` (Claude Code's
680
- * own re-entrancy signal never block twice in a row), or any internal error.
671
+ * session). Continues the conversation at most once every STOP_NAG_INTERVAL
672
+ * turns per session_id to nudge a memory-write check without being
673
+ * disruptive. Uses `hookSpecificOutput.additionalContext` rather than
674
+ * `decision: "block"`functionally identical (both go through the same
675
+ * `stop_hook_active` re-entry check and Claude Code's loop-protection cap),
676
+ * but additionalContext renders as "Stop hook feedback" in the transcript
677
+ * instead of the alarming-looking "Stop hook error". Always allows the stop
678
+ * (empty stdout) on missing session_id, `stop_hook_active` (Claude Code's own
679
+ * re-entrancy signal — never nag twice in a row), or any internal error.
681
680
  */
682
681
  async function cmdStop(_flags) {
683
682
  const TIMEOUT_MS = 3_000;
@@ -695,7 +694,9 @@ async function cmdStop(_flags) {
695
694
  if (!sessionId || payload?.stop_hook_active === true)
696
695
  return;
697
696
  if (shouldNagOnStop(sessionId, Date.now())) {
698
- process.stdout.write(JSON.stringify({ decision: "block", reason: STOP_NAG_REASON }));
697
+ process.stdout.write(JSON.stringify({
698
+ hookSpecificOutput: { hookEventName: "Stop", additionalContext: STOP_NAG_REASON },
699
+ }));
699
700
  }
700
701
  })().catch(() => {
701
702
  // Any failure in the Stop hook must be swallowed — never trap the user
@@ -1070,6 +1071,11 @@ async function cmdInstallHooks(flags) {
1070
1071
  const json = hasFlag(flags, "json");
1071
1072
  const requested = getFlag(flags, "only");
1072
1073
  const requestedKeys = requested ? new Set(requested.split(",").map((value) => value.trim())) : null;
1074
+ // Internal-only signal from cmdSetup: pi's action is a real network package
1075
+ // install (`pi install npm:pi-memory`), not a local config-file edit like
1076
+ // every other agent here — so setup's internally-manufactured `yes: true`
1077
+ // must not cover it. Not a public flag; never documented in cli-spec.
1078
+ const deferPi = hasFlag(flags, "_setup-defer-pi");
1073
1079
  const modeFlag = getFlag(flags, "mode");
1074
1080
  if (modeFlag !== undefined && modeFlag !== "stable" && modeFlag !== "per-turn") {
1075
1081
  exitError(`--mode must be 'stable' or 'per-turn' (got ${modeFlag})`, json);
@@ -1078,11 +1084,19 @@ async function cmdInstallHooks(flags) {
1078
1084
  const { homeDir, targets } = detectHookAgents();
1079
1085
  if (!homeDir)
1080
1086
  exitError("Home directory not found.", json);
1081
- const eligible = targets.filter((target) => target.supported && target.detected && (!requestedKeys || requestedKeys.has(target.key)));
1087
+ const eligible = targets.filter((target) => {
1088
+ if (!(target.supported && target.detected))
1089
+ return false;
1090
+ if (requestedKeys && !requestedKeys.has(target.key))
1091
+ return false;
1092
+ if (target.key === "pi" && deferPi)
1093
+ return false;
1094
+ return true;
1095
+ });
1082
1096
  if (!eligible.length) {
1083
- if (json)
1084
- return output({ ok: true, homeDir, results: [] }, true);
1085
- return output("No eligible agents. Nothing to install.", false);
1097
+ const report = { ok: true, homeDir, results: [] };
1098
+ output(json ? report : "No eligible agents. Nothing to install.", json);
1099
+ return report;
1086
1100
  }
1087
1101
  // Consider an agent "already installed" only when the wiring matches the requested mode.
1088
1102
  // per-turn requires BOTH SessionStart and UserPromptSubmit; stable requires SessionStart AND
@@ -1112,43 +1126,50 @@ async function cmdInstallHooks(flags) {
1112
1126
  console.log(`Automatic context already active for: ${labels}.`);
1113
1127
  }
1114
1128
  if (!pending.length) {
1115
- if (json) {
1116
- return output({
1117
- ok: true,
1118
- homeDir,
1119
- results: alreadyInstalled.map((target) => ({
1120
- key: target.key,
1121
- label: target.label,
1122
- installed: false,
1123
- reason: "already installed",
1124
- mode,
1125
- })),
1126
- }, true);
1127
- }
1128
- return output("Nothing to install.", false);
1129
+ const report = {
1130
+ ok: true,
1131
+ homeDir,
1132
+ results: alreadyInstalled.map((target) => ({
1133
+ key: target.key,
1134
+ label: target.label,
1135
+ installed: false,
1136
+ reason: "already installed",
1137
+ mode,
1138
+ })),
1139
+ };
1140
+ output(json ? report : "Nothing to install.", json);
1141
+ return report;
1129
1142
  }
1130
1143
  const selected = new Set();
1131
1144
  const applyAll = hasFlag(flags, "yes") || hasFlag(flags, "all") || !process.stdin.isTTY;
1132
1145
  const hookLabel = mode === "per-turn" ? "SessionStart + UserPromptSubmit hooks" : "SessionStart hook";
1133
1146
  for (const target of pending) {
1134
- if (applyAll || (await promptYesNo(`Install ${hookLabel} for ${target.label}?`, true)))
1147
+ // pi gets a real package install (`pi install npm:pi-memory`), not a config-file hook edit —
1148
+ // word the prompt accordingly so the confirmation matches what actually happens.
1149
+ const question = target.key === "pi"
1150
+ ? `Install pi-memory (native pi extension) for ${target.label}?`
1151
+ : `Install ${hookLabel} for ${target.label}?`;
1152
+ if (applyAll || (await promptYesNo(question, true)))
1135
1153
  selected.add(target.key);
1136
1154
  }
1137
1155
  if (!selected.size) {
1138
- if (json)
1139
- return output({ ok: true, homeDir, results: [] }, true);
1140
- return output("Nothing selected. Skipped.", false);
1156
+ const report = { ok: true, homeDir, results: [] };
1157
+ output(json ? report : "Nothing selected. Skipped.", json);
1158
+ return report;
1141
1159
  }
1142
1160
  const report = installHooks(selected, mode);
1143
1161
  if (!report.ok)
1144
1162
  exitError(report.error ?? "install failed", json);
1145
- if (json)
1146
- return output(report, true);
1163
+ if (json) {
1164
+ output(report, true);
1165
+ return report;
1166
+ }
1147
1167
  for (const result of report.results) {
1148
1168
  console.log(result.installed
1149
1169
  ? `Installed ${result.label} hook (${result.mode ?? mode}): ${result.path}`
1150
1170
  : `Skipped ${result.label} (${result.reason ?? "unknown"})`);
1151
1171
  }
1172
+ return report;
1152
1173
  }
1153
1174
  function cmdUninstallHooks(flags) {
1154
1175
  const json = hasFlag(flags, "json");
@@ -1434,15 +1455,14 @@ async function cmdSetup(flags) {
1434
1455
  const steps = [];
1435
1456
  const runQuiet = async (fn) => {
1436
1457
  if (!json) {
1437
- await fn();
1438
- return;
1458
+ return await fn();
1439
1459
  }
1440
1460
  const originalLog = console.log;
1441
1461
  const originalInfo = console.info;
1442
1462
  console.log = () => { };
1443
1463
  console.info = () => { };
1444
1464
  try {
1445
- await fn();
1465
+ return await fn();
1446
1466
  }
1447
1467
  finally {
1448
1468
  console.log = originalLog;
@@ -1473,8 +1493,31 @@ async function cmdSetup(flags) {
1473
1493
  // Step 3: hooks (uses the improved preflight — silent when all already installed)
1474
1494
  if (!skipHooks) {
1475
1495
  try {
1476
- await runQuiet(() => cmdInstallHooks(subFlags));
1477
- steps.push({ name: "hooks", ok: true });
1496
+ const userSuppliedYes = hasFlag(flags, "yes");
1497
+ // pi's install is a real network package fetch (`pi install npm:pi-memory`),
1498
+ // unlike every other agent here (local config-file edits) — setup's
1499
+ // manufactured `yes: true` above must not silently cover that unless the
1500
+ // user actually asked for --yes themselves.
1501
+ const { homeDir: detectedHomeDir, targets: detectedTargets } = detectHookAgents();
1502
+ const piTarget = detectedTargets.find((target) => target.key === "pi");
1503
+ const piDetected = Boolean(piTarget?.supported && piTarget?.detected);
1504
+ const piAlreadyActive = piDetected && detectedHomeDir ? isHookInstalled(detectedHomeDir, "pi") : false;
1505
+ const hooksFlags = userSuppliedYes ? subFlags : { ...subFlags, "_setup-defer-pi": true };
1506
+ const report = await runQuiet(() => cmdInstallHooks(hooksFlags));
1507
+ const piFailure = report?.results.find((result) => result.key === "pi" && !result.installed && result.reason && result.reason !== "already installed");
1508
+ if (piFailure) {
1509
+ steps.push({ name: "hooks", ok: false, detail: `pi-memory install failed: ${piFailure.reason}` });
1510
+ }
1511
+ else if (piDetected && !piAlreadyActive && !userSuppliedYes) {
1512
+ steps.push({
1513
+ name: "hooks",
1514
+ ok: true,
1515
+ detail: "pi-memory deferred — re-run with --yes, or agent-memory install-hooks --only pi",
1516
+ });
1517
+ }
1518
+ else {
1519
+ steps.push({ name: "hooks", ok: true });
1520
+ }
1478
1521
  }
1479
1522
  catch (error) {
1480
1523
  steps.push({ name: "hooks", ok: false, detail: error.message });
@@ -1656,7 +1699,7 @@ async function cmdSetup(flags) {
1656
1699
  console.log(colorize("The local plugin is live. Feel the magic now:", "green"));
1657
1700
  console.log(` ${colorize('agent-memory recall "what did we decide about auth?"', "cyan")} — search past sessions`);
1658
1701
  console.log(` ${colorize("agent-memory learn", "cyan")} — surface repeated corrections`);
1659
- console.log(` ${colorize("agent-memory worker start", "cyan")} capture new sessions in real time`);
1702
+ console.log(` ${colorize("agent-memory index", "cyan")} refresh the supported local session index`);
1660
1703
  console.log(` ${colorize("agent-memory dashboard", "cyan")} — private local dashboard`);
1661
1704
  }
1662
1705
  else if (skipPlugin) {
@@ -1665,6 +1708,138 @@ async function cmdSetup(flags) {
1665
1708
  }
1666
1709
  console.log("");
1667
1710
  }
1711
+ /**
1712
+ * Reverse of {@link cmdSetup}: removes every install artifact agent-memory
1713
+ * creates outside of this package — hooks, skills, MCP registrations, shell
1714
+ * completions, and the Pro plugin executables. Memory data under
1715
+ * `getMemoryDir()` (MEMORY.md, daily logs, scratchpad, topics, qmd index) is
1716
+ * left untouched unless `--data` is passed, since that's the one step a user
1717
+ * can't undo. Destructive by nature, so it always requires either an
1718
+ * interactive confirmation or `--yes`.
1719
+ */
1720
+ async function cmdUninstall(flags) {
1721
+ const json = hasFlag(flags, "json");
1722
+ const yes = hasFlag(flags, "yes");
1723
+ const wipeData = hasFlag(flags, "data");
1724
+ const interactive = !json && Boolean(process.stdin.isTTY && process.stdout.isTTY);
1725
+ if (!yes) {
1726
+ const message = wipeData
1727
+ ? "Re-run with --yes to remove agent-memory's hooks, skills, MCP registrations, completions, and Pro plugin, and permanently delete ~/.agent-memory (MEMORY.md, daily logs, scratchpad, topics, qmd index)."
1728
+ : "Re-run with --yes to remove agent-memory's hooks, skills, MCP registrations, completions, and Pro plugin. Your memory data is left untouched.";
1729
+ if (interactive) {
1730
+ const question = wipeData
1731
+ ? "This will also permanently delete your memory data (MEMORY.md, daily logs, scratchpad). Continue?"
1732
+ : "Remove agent-memory's hooks, skills, MCP registrations, completions, and Pro plugin?";
1733
+ if (!(await promptYesNo(question, false))) {
1734
+ console.log("Aborted. Nothing was removed.");
1735
+ return;
1736
+ }
1737
+ }
1738
+ else {
1739
+ if (json)
1740
+ output({ ok: false, error: { code: "confirmation_required", message } }, true);
1741
+ else
1742
+ console.error(`Error: ${message}`);
1743
+ process.exitCode = 1;
1744
+ return;
1745
+ }
1746
+ }
1747
+ const steps = [];
1748
+ try {
1749
+ const report = uninstallSkills();
1750
+ if (!report.ok)
1751
+ throw new Error(report.error ?? "failed to remove skills");
1752
+ steps.push({
1753
+ name: "skills",
1754
+ ok: true,
1755
+ detail: report.removed.length ? `removed ${report.removed.length}` : "not installed",
1756
+ });
1757
+ }
1758
+ catch (error) {
1759
+ steps.push({ name: "skills", ok: false, detail: error.message });
1760
+ }
1761
+ try {
1762
+ const report = uninstallHooks();
1763
+ if (!report.ok)
1764
+ throw new Error(report.error ?? "failed to remove hooks");
1765
+ const removed = report.results.filter((r) => r.installed).length;
1766
+ // agent-memory never removes pi-memory itself (see uninstallPiMemoryDelegate) — make that
1767
+ // explicit here rather than letting a generic "removed N" detail imply everything is gone.
1768
+ const piResult = report.results.find((r) => r.key === "pi");
1769
+ const piNote = piResult?.reason?.startsWith("pi-memory left installed") ? `; ${piResult.reason}` : "";
1770
+ steps.push({
1771
+ name: "hooks",
1772
+ ok: true,
1773
+ detail: (removed ? `removed ${removed}` : "not installed") + piNote,
1774
+ });
1775
+ }
1776
+ catch (error) {
1777
+ steps.push({ name: "hooks", ok: false, detail: error.message });
1778
+ }
1779
+ try {
1780
+ const results = unregisterMcpFromAgents(null);
1781
+ const removed = results.filter((r) => r.status === "unregistered").length;
1782
+ steps.push({ name: "mcp", ok: true, detail: removed ? `unregistered ${removed}` : "not registered" });
1783
+ }
1784
+ catch (error) {
1785
+ steps.push({ name: "mcp", ok: false, detail: error.message });
1786
+ }
1787
+ try {
1788
+ const results = uninstallCompletion();
1789
+ const removed = results.filter((r) => r.removed || r.profileUpdated).length;
1790
+ steps.push({ name: "completions", ok: true, detail: removed ? `removed ${removed}` : "not installed" });
1791
+ }
1792
+ catch (error) {
1793
+ steps.push({ name: "completions", ok: false, detail: error.message });
1794
+ }
1795
+ try {
1796
+ const manager = createDefaultPluginBootstrap(VERSION);
1797
+ const pluginResult = await manager.uninstall();
1798
+ steps.push({ name: "plugin", ok: pluginResult.ok, detail: pluginResult.result });
1799
+ }
1800
+ catch (error) {
1801
+ steps.push({ name: "plugin", ok: false, detail: error.message });
1802
+ }
1803
+ if (wipeData) {
1804
+ try {
1805
+ const memoryDir = getMemoryDir();
1806
+ if (fs.existsSync(memoryDir))
1807
+ fs.rmSync(memoryDir, { recursive: true, force: true });
1808
+ const pluginRoot = getDefaultPluginInstallRoot();
1809
+ if (fs.existsSync(pluginRoot))
1810
+ fs.rmSync(pluginRoot, { recursive: true, force: true });
1811
+ steps.push({ name: "data", ok: true, detail: memoryDir });
1812
+ }
1813
+ catch (error) {
1814
+ steps.push({ name: "data", ok: false, detail: error.message });
1815
+ }
1816
+ }
1817
+ const allOk = steps.every((step) => step.ok);
1818
+ if (!allOk)
1819
+ process.exitCode = 1;
1820
+ if (json) {
1821
+ output({ ok: allOk, data: wipeData, steps }, true);
1822
+ return;
1823
+ }
1824
+ console.log("");
1825
+ console.log(colorize("agent-memory uninstall", "bold"));
1826
+ for (const step of steps) {
1827
+ const mark = step.ok ? MARK_OK : MARK_FAIL;
1828
+ const detail = step.detail ? colorize(` ${step.detail}`, "dim") : "";
1829
+ console.log(` ${mark} ${step.name}${detail}`);
1830
+ }
1831
+ console.log("");
1832
+ if (!allOk) {
1833
+ console.log(colorize("Some steps failed — see details above.", "yellow"));
1834
+ }
1835
+ else if (wipeData) {
1836
+ console.log(colorize("agent-memory has been fully removed, including your memory data.", "green"));
1837
+ }
1838
+ else {
1839
+ console.log(colorize("agent-memory's install artifacts have been removed.", "green"));
1840
+ console.log(colorize(`Your notes are untouched at ${getMemoryDir()}. Re-run with --data to remove them too.`, "dim"));
1841
+ }
1842
+ }
1668
1843
  /**
1669
1844
  * Explain the optional plugin without making a successful core setup feel
1670
1845
  * incomplete. Keep the quota line here in sync with `freeEntitlement()`.
@@ -1678,10 +1853,10 @@ function printProPitch(mode) {
1678
1853
  else {
1679
1854
  console.log(`${colorize("Optional: AgentMemory Pro", "bold")} — ${colorize("memory that learns from your work", "dim")}`);
1680
1855
  }
1681
- console.log(` ${colorize("Recall across sessions", "cyan")} Ask "what did we decide about auth?" across Claude, Codex, Cursor.`);
1856
+ console.log(` ${colorize("Recall across sessions", "cyan")} Ask "what did we decide about auth?" across Claude Code, Codex, and Pi history.`);
1682
1857
  console.log(` ${colorize("Learn from corrections", "cyan")} Turn repeated fixes into memory you can inspect and undo.`);
1683
- console.log(` ${colorize("Real-time capture", "cyan")} Local worker indexes new sessions as they happen.`);
1684
- console.log(` ${colorize("Private by default", "cyan")} Memory and session content stay on this device — no account required.`);
1858
+ console.log(` ${colorize("Local session index", "cyan")} Scan supported session history without uploading it to AgentMemory.`);
1859
+ console.log(` ${colorize("Private by default", "cyan")} AgentMemory services never receive memory or session content.`);
1685
1860
  console.log(` ${colorize("Included at no cost:", "green")} ${colorize("20 recalls + 5 learning scans per day", "bold")}. Local indexing and dashboard remain free.`);
1686
1861
  console.log("");
1687
1862
  }
@@ -1900,6 +2075,14 @@ async function cmdDoctor(flags) {
1900
2075
  });
1901
2076
  }
1902
2077
  }
2078
+ // Auto-upgrade policy
2079
+ const upgradePolicy = readUpgradePolicy();
2080
+ rows.push({
2081
+ status: "ok",
2082
+ label: "Auto-upgrade",
2083
+ detail: `CLI: ${upgradePolicy.cli}, Pro: ${upgradePolicy.plugin}`,
2084
+ fix: upgradePolicy.cli === "off" && upgradePolicy.plugin === "off" ? "agent-memory upgrade policy auto" : undefined,
2085
+ });
1903
2086
  // Skills + hooks per detected host
1904
2087
  const { homeDir, targets } = detectHookAgents();
1905
2088
  const detected = targets.filter((target) => target.detected);
@@ -1915,7 +2098,7 @@ async function cmdDoctor(flags) {
1915
2098
  rows.push({
1916
2099
  status: "warn",
1917
2100
  label: "Agent hosts",
1918
- detail: "no supported agents detected (Claude Code, Codex, Cursor, opencode)",
2101
+ detail: "no supported agents detected (Claude Code, Codex, Cursor, opencode, pi)",
1919
2102
  fix: "install one of the agents first, then: agent-memory install-skills",
1920
2103
  });
1921
2104
  }
@@ -1927,7 +2110,35 @@ async function cmdDoctor(flags) {
1927
2110
  });
1928
2111
  for (const target of detected) {
1929
2112
  if (!target.supported) {
1930
- // Skip skill/hook rows for hosts we don't yet integrate with (e.g. pi has its own extension).
2113
+ // Skip skill/hook rows for hosts we don't yet integrate with.
2114
+ continue;
2115
+ }
2116
+ if (target.key === "pi") {
2117
+ // pi has no SKILL.md — agent-memory delegates entirely to the pi-memory
2118
+ // extension, installed via `pi install npm:pi-memory` rather than a
2119
+ // config-file edit, so it gets its own row instead of the generic
2120
+ // Skill:/Hook: pair below.
2121
+ const installed = homeDir ? isHookInstalled(homeDir, "pi") : false;
2122
+ const state = getPiMemoryState();
2123
+ let detail;
2124
+ if (installed) {
2125
+ detail = "pi-memory extension active (github.com/jayzeng/pi-memory)";
2126
+ }
2127
+ else if (state && !state.ok) {
2128
+ detail = `pi-memory not detected — last install attempt (${state.lastAttemptAt}) failed: ${state.detail}`;
2129
+ }
2130
+ else if (state?.ok) {
2131
+ detail = `pi-memory installed (${state.lastAttemptAt}) but no session has run yet`;
2132
+ }
2133
+ else {
2134
+ detail = "pi-memory not detected — agent-memory can install it";
2135
+ }
2136
+ rows.push({
2137
+ status: installed ? "ok" : "warn",
2138
+ label: "Memory: pi",
2139
+ detail,
2140
+ fix: installed ? undefined : "agent-memory install-hooks --only pi",
2141
+ });
1931
2142
  continue;
1932
2143
  }
1933
2144
  const skillPath = homeDir ? `${target.homeMarker}/skills/agent-memory/SKILL.md` : null;
@@ -2173,9 +2384,10 @@ Usage:
2173
2384
  agent-memory plugin manage [--no-browser]
2174
2385
 
2175
2386
  The public core remains fully usable without AgentMemory Pro. Install uses a random
2176
- installation identifier and requires no account or email. The free tier includes
2177
- 20 recalls and 5 learning scans per local day; indexing and the Memory Dashboard
2178
- remain available. Memory and session content stay on this device.`);
2387
+ installation identifier and requires no account or email. The free tier includes
2388
+ 20 recalls and 5 learning scans per local day; indexing and the Memory Dashboard
2389
+ remain available. AgentMemory services never receive memory or session content;
2390
+ recall results are subject to the coding agent or model provider you invoke.`);
2179
2391
  }
2180
2392
  function pluginCommandFailure(command, error) {
2181
2393
  return {
@@ -2482,16 +2694,108 @@ async function resolvePluginLatestHint() {
2482
2694
  return { latest: null, updateAvailable: false };
2483
2695
  }
2484
2696
  }
2485
- async function cmdUpgrade(flags) {
2697
+ async function cmdUpgradePolicy(flags, positional) {
2698
+ const json = hasFlag(flags, "json");
2699
+ const value = positional[0];
2700
+ if (!value) {
2701
+ const policy = readUpgradePolicy();
2702
+ if (json)
2703
+ output({ cli: policy.cli, plugin: policy.plugin }, true);
2704
+ else
2705
+ console.log(` cli: ${policy.cli}\n plugin: ${policy.plugin}`);
2706
+ return;
2707
+ }
2708
+ if (value !== "off" && value !== "notify" && value !== "auto") {
2709
+ exitError(`Invalid policy '${value}'. Expected one of: off, notify, auto.`, json);
2710
+ }
2711
+ const onlyCli = hasFlag(flags, "cli");
2712
+ const onlyPlugin = hasFlag(flags, "plugin");
2713
+ const patch = {};
2714
+ if (onlyCli || !onlyPlugin)
2715
+ patch.cli = value;
2716
+ if (onlyPlugin || !onlyCli)
2717
+ patch.plugin = value;
2718
+ const next = writeUpgradePolicy(patch);
2719
+ if (json)
2720
+ output(next, true);
2721
+ else
2722
+ console.log(` cli: ${next.cli}\n plugin: ${next.plugin}`);
2723
+ }
2724
+ /**
2725
+ * `--background`-mode install: only touches a target when its persisted policy
2726
+ * (see `readUpgradePolicy`) is `"auto"`. Always non-interactive — this path is
2727
+ * only ever reached from a detached, non-TTY child spawned by
2728
+ * `refreshUpgradeCacheBackground()`. Failures are recorded, never thrown.
2729
+ */
2730
+ async function runAutoUpgrade(status, policy, pluginCurrent, quiet) {
2731
+ const now = new Date().toISOString();
2732
+ let cliAuto;
2733
+ let pluginAuto;
2734
+ if (policy.cli === "auto" && status.cli.upgradeAvailable) {
2735
+ const method = detectInstallMethod();
2736
+ if (!quiet)
2737
+ console.log(`Auto-upgrading CLI via: ${method.command.join(" ")}`);
2738
+ try {
2739
+ const result = runInstaller(method);
2740
+ cliAuto = result.ok
2741
+ ? { at: now, ok: true, version: status.cli.latest }
2742
+ : {
2743
+ at: now,
2744
+ ok: false,
2745
+ version: status.cli.current,
2746
+ error: (result.stderr || result.stdout || `exit ${result.code}`).trim(),
2747
+ };
2748
+ }
2749
+ catch (error) {
2750
+ cliAuto = {
2751
+ at: now,
2752
+ ok: false,
2753
+ version: status.cli.current,
2754
+ error: error instanceof Error ? error.message : String(error),
2755
+ };
2756
+ }
2757
+ }
2758
+ if (policy.plugin === "auto" && status.plugin.upgradeAvailable) {
2759
+ if (!quiet)
2760
+ console.log("Auto-upgrading Pro plugin bundle…");
2761
+ try {
2762
+ const pluginResult = await cmdPlugin({ json: true }, ["update"]);
2763
+ const ok = Boolean(pluginResult?.ok);
2764
+ pluginAuto = ok
2765
+ ? { at: now, ok: true, version: status.plugin.latest }
2766
+ : {
2767
+ at: now,
2768
+ ok: false,
2769
+ version: pluginCurrent,
2770
+ error: pluginResult?.error?.message ?? "plugin update failed",
2771
+ };
2772
+ }
2773
+ catch (error) {
2774
+ pluginAuto = {
2775
+ at: now,
2776
+ ok: false,
2777
+ version: pluginCurrent,
2778
+ error: error instanceof Error ? error.message : String(error),
2779
+ };
2780
+ }
2781
+ }
2782
+ return { cliAuto, pluginAuto };
2783
+ }
2784
+ async function cmdUpgrade(flags, positional = []) {
2785
+ if (positional[0] === "policy") {
2786
+ await cmdUpgradePolicy(flags, positional.slice(1));
2787
+ return;
2788
+ }
2486
2789
  const json = hasFlag(flags, "json");
2487
2790
  const quiet = hasFlag(flags, "quiet");
2488
2791
  const checkOnly = hasFlag(flags, "check");
2792
+ const background = hasFlag(flags, "background");
2489
2793
  const refresh = hasFlag(flags, "refresh");
2490
2794
  const onlyCli = hasFlag(flags, "cli");
2491
2795
  const onlyPlugin = hasFlag(flags, "plugin");
2492
2796
  const targetCli = onlyCli || !onlyPlugin;
2493
2797
  const targetPlugin = onlyPlugin || !onlyCli;
2494
- const applyAll = hasFlag(flags, "yes") || !process.stdin.isTTY || !process.stdout.isTTY;
2798
+ const applyAll = hasFlag(flags, "yes") || background || !process.stdin.isTTY || !process.stdout.isTTY;
2495
2799
  const pluginCurrent = await readPluginCurrentVersion();
2496
2800
  const pluginProbe = targetPlugin ? await resolvePluginLatestHint() : { latest: null, updateAvailable: false };
2497
2801
  const status = await checkForUpgrades({
@@ -2508,6 +2812,30 @@ async function cmdUpgrade(flags) {
2508
2812
  printUpgradeStatus(status, { targetCli, targetPlugin });
2509
2813
  return;
2510
2814
  }
2815
+ if (background) {
2816
+ const policy = readUpgradePolicy();
2817
+ const { cliAuto, pluginAuto } = await runAutoUpgrade(status, policy, pluginCurrent, quiet);
2818
+ writeUpgradeCache({
2819
+ checkedAt: status.checkedAt,
2820
+ cliCurrent: VERSION,
2821
+ cliLatest: status.cli.latest,
2822
+ pluginCurrent,
2823
+ pluginLatest: status.plugin.latest,
2824
+ cliAuto,
2825
+ pluginAuto,
2826
+ });
2827
+ if (json)
2828
+ output({ ...status, cliAuto, pluginAuto }, true);
2829
+ else if (!quiet) {
2830
+ if (cliAuto)
2831
+ console.log(` ${cliAuto.ok ? MARK_OK : MARK_FAIL} CLI auto-upgrade: ${cliAuto.ok ? `→ ${cliAuto.version}` : cliAuto.error}`);
2832
+ if (pluginAuto)
2833
+ console.log(` ${pluginAuto.ok ? MARK_OK : MARK_FAIL} Pro auto-upgrade: ${pluginAuto.ok ? `→ ${pluginAuto.version}` : pluginAuto.error}`);
2834
+ }
2835
+ if ((cliAuto && !cliAuto.ok) || (pluginAuto && !pluginAuto.ok))
2836
+ process.exitCode = 1;
2837
+ return;
2838
+ }
2511
2839
  const cliNeedsUpgrade = targetCli && status.cli.upgradeAvailable;
2512
2840
  const pluginNeedsUpgrade = targetPlugin && status.plugin.upgradeAvailable;
2513
2841
  if (!cliNeedsUpgrade && !pluginNeedsUpgrade) {
@@ -2648,6 +2976,64 @@ function registerMcpInAgents(only) {
2648
2976
  }
2649
2977
  return results;
2650
2978
  }
2979
+ /**
2980
+ * Reverse of {@link registerMcpInAgents}: removes the `agent-memory` MCP server
2981
+ * entry from every supported local harness. Missing config files are reported
2982
+ * as `not-installed`, files that never had the entry as `not-registered`.
2983
+ */
2984
+ function unregisterMcpFromAgents(only) {
2985
+ const home = os.homedir();
2986
+ const want = (key) => !only || only.has(key);
2987
+ const results = [];
2988
+ const unregisterJson = (key, displayName, configFile) => {
2989
+ const p = path.join(home, configFile);
2990
+ if (!fs.existsSync(p)) {
2991
+ results.push({ key, displayName, path: p, status: "not-installed" });
2992
+ return;
2993
+ }
2994
+ let s = {};
2995
+ try {
2996
+ s = JSON.parse(fs.readFileSync(p, "utf8"));
2997
+ }
2998
+ catch { }
2999
+ const servers = (s.mcpServers ?? {});
3000
+ if (!servers["agent-memory"]) {
3001
+ results.push({ key, displayName, path: p, status: "not-registered" });
3002
+ return;
3003
+ }
3004
+ delete servers["agent-memory"];
3005
+ if (Object.keys(servers).length === 0)
3006
+ delete s.mcpServers;
3007
+ else
3008
+ s.mcpServers = servers;
3009
+ fs.writeFileSync(p, `${JSON.stringify(s, null, 2)}\n`);
3010
+ results.push({ key, displayName, path: p, status: "unregistered" });
3011
+ };
3012
+ if (want("claude"))
3013
+ unregisterJson("claude", "Claude Code", ".claude.json");
3014
+ if (want("cursor"))
3015
+ unregisterJson("cursor", "Cursor", ".cursor/mcp.json");
3016
+ if (want("windsurf"))
3017
+ unregisterJson("windsurf", "Windsurf", ".windsurf/mcp_settings.json");
3018
+ if (want("codex")) {
3019
+ const p = path.join(home, ".codex", "config.toml");
3020
+ if (!fs.existsSync(p)) {
3021
+ results.push({ key: "codex", displayName: "Codex", path: p, status: "not-installed" });
3022
+ }
3023
+ else {
3024
+ const existing = fs.readFileSync(p, "utf8");
3025
+ if (!existing.includes("[mcp_servers.agent-memory]")) {
3026
+ results.push({ key: "codex", displayName: "Codex", path: p, status: "not-registered" });
3027
+ }
3028
+ else {
3029
+ const pattern = /\n?\[mcp_servers\.agent-memory\]\n(?:(?!\[)[^\n]*\n?)*/;
3030
+ fs.writeFileSync(p, existing.replace(pattern, ""), "utf8");
3031
+ results.push({ key: "codex", displayName: "Codex", path: p, status: "unregistered" });
3032
+ }
3033
+ }
3034
+ }
3035
+ return results;
3036
+ }
2651
3037
  async function cmdServe(flags) {
2652
3038
  const isMcp = hasFlag(flags, "mcp");
2653
3039
  const isRegister = hasFlag(flags, "register");
@@ -2689,7 +3075,7 @@ async function cmdServe(flags) {
2689
3075
  // Core tools: free tier, available without Pro.
2690
3076
  server.addTool({
2691
3077
  name: "memory_read",
2692
- description: "Read the current long-term memory (MEMORY.md) and scratchpad checklist.",
3078
+ description: "Read ONLY the curated long-term memory (MEMORY.md) and open scratchpad items — a small saved snapshot, not a search. For finding things in daily logs/topics, or recalling past chat sessions, use the `agent-memory` skill (search/recall commands) instead of this tool.",
2693
3079
  inputSchema: { type: "object", properties: {} },
2694
3080
  }, async () => {
2695
3081
  const memFile = getMemoryFile();
@@ -2739,11 +3125,16 @@ async function cmdServe(flags) {
2739
3125
  server.addTool({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }, (input) => runtime.runMcpTool(tool.name, input));
2740
3126
  }
2741
3127
  server.addStartupHook(() => runtime.runMcpStartup());
3128
+ server.addShutdownHook(() => runtime.runMcpShutdown());
2742
3129
  }
2743
3130
  catch {
2744
3131
  // Pro not installed or failed to load — serve with core tools only.
2745
3132
  }
2746
3133
  await server.start();
3134
+ // Hard backstop: a Pro plugin's fs.watch handles (or any other resource
3135
+ // that keeps the event loop alive) must never prevent this process from
3136
+ // exiting once stdin has closed.
3137
+ process.exit(0);
2747
3138
  }
2748
3139
  // ---------------------------------------------------------------------------
2749
3140
  // Usage
@@ -2753,7 +3144,7 @@ function printUsage() {
2753
3144
  ["Do things", ["save", "note", "recall", "search"]],
2754
3145
  ["See things", ["status", "doctor", "dashboard"]],
2755
3146
  ["Advanced", ["write", "read", "context", "scratchpad", "distil", "sync"]],
2756
- ["Setup", ["setup", "install-skills", "install-hooks", "completion"]],
3147
+ ["Setup", ["setup", "install-skills", "install-hooks", "completion", "uninstall"]],
2757
3148
  ["Pro", ["pro", "learn"]],
2758
3149
  ];
2759
3150
  const knownGroupCommands = new Set(groups.flatMap(([, list]) => list));
@@ -2854,7 +3245,7 @@ async function main() {
2854
3245
  process.stdout.isTTY &&
2855
3246
  !json &&
2856
3247
  command &&
2857
- !["init", "help", "version", "doctor", "status", "completion", "hook", "serve"].includes(command) &&
3248
+ !["init", "help", "version", "doctor", "status", "completion", "hook", "serve", "uninstall"].includes(command) &&
2858
3249
  !fs.existsSync(getMemoryDir())) {
2859
3250
  console.log(colorize("It looks like this is your first run — no memory directory yet.", "yellow"));
2860
3251
  if (await promptYesNo("Run agent-memory init to set things up?", true)) {
@@ -2966,6 +3357,9 @@ async function main() {
2966
3357
  case "uninstall-hooks":
2967
3358
  cmdUninstallHooks(flags);
2968
3359
  break;
3360
+ case "uninstall":
3361
+ await cmdUninstall(flags);
3362
+ break;
2969
3363
  case "hook": {
2970
3364
  const sub = positional[0];
2971
3365
  if (sub !== "session-start" && sub !== "user-prompt-submit" && sub !== "stop") {
@@ -2988,6 +3382,13 @@ async function main() {
2988
3382
  const layer = readHookMode() === "per-turn" ? "stable" : undefined;
2989
3383
  await cmdContext(layer ? { "no-search": true, layer } : { "no-search": true });
2990
3384
  try {
3385
+ const policy = readUpgradePolicy();
3386
+ if (!policy.existed) {
3387
+ // Persist the (possibly env-overridden) defaults now, so this notice
3388
+ // only ever fires once — the file's mere existence is the "seen" flag.
3389
+ writeUpgradePolicy({ cli: policy.cli, plugin: policy.plugin });
3390
+ console.error("agent-memory: auto-upgrade is on by default (CLI + Pro plugin). Disable with: agent-memory upgrade policy off");
3391
+ }
2991
3392
  const cache = readUpgradeCache();
2992
3393
  if (cache) {
2993
3394
  const status = await checkForUpgrades({
@@ -2995,7 +3396,7 @@ async function main() {
2995
3396
  pluginCurrent: cache.pluginCurrent,
2996
3397
  cacheOnly: true,
2997
3398
  });
2998
- const notice = formatUpgradeNotice(status);
3399
+ const notice = formatUpgradeNotice(status, cache);
2999
3400
  if (notice)
3000
3401
  console.error(notice);
3001
3402
  }
@@ -3022,7 +3423,7 @@ async function main() {
3022
3423
  capability: "session",
3023
3424
  });
3024
3425
  if (decision.state === "exhausted") {
3025
- console.error(`AgentMemory free session allowance resets in ${formatResetTime(decision.resetAt)}. Upgrade: ${UPGRADE_URL}`);
3426
+ console.error(`AgentMemory free session allowance resets in ${formatResetTime(decision.resetAt)}. Paid plans are not available yet.`);
3026
3427
  }
3027
3428
  }
3028
3429
  }
@@ -3041,7 +3442,7 @@ async function main() {
3041
3442
  await cmdPro(flags, positional);
3042
3443
  break;
3043
3444
  case "upgrade":
3044
- await cmdUpgrade(flags);
3445
+ await cmdUpgrade(flags, positional);
3045
3446
  break;
3046
3447
  case "serve":
3047
3448
  await cmdServe(flags);