myagentmemory 0.5.1 → 0.5.3

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"));
@@ -617,8 +617,11 @@ async function cmdUserPromptSubmit(_flags) {
617
617
  }
618
618
  // How many Stop events must elapse (per session_id) before the periodic
619
619
  // memory-write nudge fires again. Balances "long sessions get checked
620
- // repeatedly" against "don't block every single turn".
621
- const STOP_NAG_INTERVAL = 12;
620
+ // repeatedly" against "don't block every single turn". Deliberately short —
621
+ // most real sessions are well under a dozen turns, so a wider interval meant
622
+ // the nudge rarely fired in practice (see stop-hook.json in the wild: sessions
623
+ // topping out around 7 turns, zero nags ever recorded).
624
+ const STOP_NAG_INTERVAL = 6;
622
625
  // Bound state/stop-hook.json so it can't grow unboundedly across many sessions.
623
626
  const STOP_HOOK_MAX_SESSIONS = 50;
624
627
  function stopHookStatePath() {
@@ -1067,6 +1070,11 @@ async function cmdInstallHooks(flags) {
1067
1070
  const json = hasFlag(flags, "json");
1068
1071
  const requested = getFlag(flags, "only");
1069
1072
  const requestedKeys = requested ? new Set(requested.split(",").map((value) => value.trim())) : null;
1073
+ // Internal-only signal from cmdSetup: pi's action is a real network package
1074
+ // install (`pi install npm:pi-memory`), not a local config-file edit like
1075
+ // every other agent here — so setup's internally-manufactured `yes: true`
1076
+ // must not cover it. Not a public flag; never documented in cli-spec.
1077
+ const deferPi = hasFlag(flags, "_setup-defer-pi");
1070
1078
  const modeFlag = getFlag(flags, "mode");
1071
1079
  if (modeFlag !== undefined && modeFlag !== "stable" && modeFlag !== "per-turn") {
1072
1080
  exitError(`--mode must be 'stable' or 'per-turn' (got ${modeFlag})`, json);
@@ -1075,11 +1083,19 @@ async function cmdInstallHooks(flags) {
1075
1083
  const { homeDir, targets } = detectHookAgents();
1076
1084
  if (!homeDir)
1077
1085
  exitError("Home directory not found.", json);
1078
- const eligible = targets.filter((target) => target.supported && target.detected && (!requestedKeys || requestedKeys.has(target.key)));
1086
+ const eligible = targets.filter((target) => {
1087
+ if (!(target.supported && target.detected))
1088
+ return false;
1089
+ if (requestedKeys && !requestedKeys.has(target.key))
1090
+ return false;
1091
+ if (target.key === "pi" && deferPi)
1092
+ return false;
1093
+ return true;
1094
+ });
1079
1095
  if (!eligible.length) {
1080
- if (json)
1081
- return output({ ok: true, homeDir, results: [] }, true);
1082
- return output("No eligible agents. Nothing to install.", false);
1096
+ const report = { ok: true, homeDir, results: [] };
1097
+ output(json ? report : "No eligible agents. Nothing to install.", json);
1098
+ return report;
1083
1099
  }
1084
1100
  // Consider an agent "already installed" only when the wiring matches the requested mode.
1085
1101
  // per-turn requires BOTH SessionStart and UserPromptSubmit; stable requires SessionStart AND
@@ -1109,43 +1125,50 @@ async function cmdInstallHooks(flags) {
1109
1125
  console.log(`Automatic context already active for: ${labels}.`);
1110
1126
  }
1111
1127
  if (!pending.length) {
1112
- if (json) {
1113
- return output({
1114
- ok: true,
1115
- homeDir,
1116
- results: alreadyInstalled.map((target) => ({
1117
- key: target.key,
1118
- label: target.label,
1119
- installed: false,
1120
- reason: "already installed",
1121
- mode,
1122
- })),
1123
- }, true);
1124
- }
1125
- return output("Nothing to install.", false);
1128
+ const report = {
1129
+ ok: true,
1130
+ homeDir,
1131
+ results: alreadyInstalled.map((target) => ({
1132
+ key: target.key,
1133
+ label: target.label,
1134
+ installed: false,
1135
+ reason: "already installed",
1136
+ mode,
1137
+ })),
1138
+ };
1139
+ output(json ? report : "Nothing to install.", json);
1140
+ return report;
1126
1141
  }
1127
1142
  const selected = new Set();
1128
1143
  const applyAll = hasFlag(flags, "yes") || hasFlag(flags, "all") || !process.stdin.isTTY;
1129
1144
  const hookLabel = mode === "per-turn" ? "SessionStart + UserPromptSubmit hooks" : "SessionStart hook";
1130
1145
  for (const target of pending) {
1131
- if (applyAll || (await promptYesNo(`Install ${hookLabel} for ${target.label}?`, true)))
1146
+ // pi gets a real package install (`pi install npm:pi-memory`), not a config-file hook edit —
1147
+ // word the prompt accordingly so the confirmation matches what actually happens.
1148
+ const question = target.key === "pi"
1149
+ ? `Install pi-memory (native pi extension) for ${target.label}?`
1150
+ : `Install ${hookLabel} for ${target.label}?`;
1151
+ if (applyAll || (await promptYesNo(question, true)))
1132
1152
  selected.add(target.key);
1133
1153
  }
1134
1154
  if (!selected.size) {
1135
- if (json)
1136
- return output({ ok: true, homeDir, results: [] }, true);
1137
- return output("Nothing selected. Skipped.", false);
1155
+ const report = { ok: true, homeDir, results: [] };
1156
+ output(json ? report : "Nothing selected. Skipped.", json);
1157
+ return report;
1138
1158
  }
1139
1159
  const report = installHooks(selected, mode);
1140
1160
  if (!report.ok)
1141
1161
  exitError(report.error ?? "install failed", json);
1142
- if (json)
1143
- return output(report, true);
1162
+ if (json) {
1163
+ output(report, true);
1164
+ return report;
1165
+ }
1144
1166
  for (const result of report.results) {
1145
1167
  console.log(result.installed
1146
1168
  ? `Installed ${result.label} hook (${result.mode ?? mode}): ${result.path}`
1147
1169
  : `Skipped ${result.label} (${result.reason ?? "unknown"})`);
1148
1170
  }
1171
+ return report;
1149
1172
  }
1150
1173
  function cmdUninstallHooks(flags) {
1151
1174
  const json = hasFlag(flags, "json");
@@ -1431,15 +1454,14 @@ async function cmdSetup(flags) {
1431
1454
  const steps = [];
1432
1455
  const runQuiet = async (fn) => {
1433
1456
  if (!json) {
1434
- await fn();
1435
- return;
1457
+ return await fn();
1436
1458
  }
1437
1459
  const originalLog = console.log;
1438
1460
  const originalInfo = console.info;
1439
1461
  console.log = () => { };
1440
1462
  console.info = () => { };
1441
1463
  try {
1442
- await fn();
1464
+ return await fn();
1443
1465
  }
1444
1466
  finally {
1445
1467
  console.log = originalLog;
@@ -1470,8 +1492,31 @@ async function cmdSetup(flags) {
1470
1492
  // Step 3: hooks (uses the improved preflight — silent when all already installed)
1471
1493
  if (!skipHooks) {
1472
1494
  try {
1473
- await runQuiet(() => cmdInstallHooks(subFlags));
1474
- steps.push({ name: "hooks", ok: true });
1495
+ const userSuppliedYes = hasFlag(flags, "yes");
1496
+ // pi's install is a real network package fetch (`pi install npm:pi-memory`),
1497
+ // unlike every other agent here (local config-file edits) — setup's
1498
+ // manufactured `yes: true` above must not silently cover that unless the
1499
+ // user actually asked for --yes themselves.
1500
+ const { homeDir: detectedHomeDir, targets: detectedTargets } = detectHookAgents();
1501
+ const piTarget = detectedTargets.find((target) => target.key === "pi");
1502
+ const piDetected = Boolean(piTarget?.supported && piTarget?.detected);
1503
+ const piAlreadyActive = piDetected && detectedHomeDir ? isHookInstalled(detectedHomeDir, "pi") : false;
1504
+ const hooksFlags = userSuppliedYes ? subFlags : { ...subFlags, "_setup-defer-pi": true };
1505
+ const report = await runQuiet(() => cmdInstallHooks(hooksFlags));
1506
+ const piFailure = report?.results.find((result) => result.key === "pi" && !result.installed && result.reason && result.reason !== "already installed");
1507
+ if (piFailure) {
1508
+ steps.push({ name: "hooks", ok: false, detail: `pi-memory install failed: ${piFailure.reason}` });
1509
+ }
1510
+ else if (piDetected && !piAlreadyActive && !userSuppliedYes) {
1511
+ steps.push({
1512
+ name: "hooks",
1513
+ ok: true,
1514
+ detail: "pi-memory deferred — re-run with --yes, or agent-memory install-hooks --only pi",
1515
+ });
1516
+ }
1517
+ else {
1518
+ steps.push({ name: "hooks", ok: true });
1519
+ }
1475
1520
  }
1476
1521
  catch (error) {
1477
1522
  steps.push({ name: "hooks", ok: false, detail: error.message });
@@ -1662,6 +1707,138 @@ async function cmdSetup(flags) {
1662
1707
  }
1663
1708
  console.log("");
1664
1709
  }
1710
+ /**
1711
+ * Reverse of {@link cmdSetup}: removes every install artifact agent-memory
1712
+ * creates outside of this package — hooks, skills, MCP registrations, shell
1713
+ * completions, and the Pro plugin executables. Memory data under
1714
+ * `getMemoryDir()` (MEMORY.md, daily logs, scratchpad, topics, qmd index) is
1715
+ * left untouched unless `--data` is passed, since that's the one step a user
1716
+ * can't undo. Destructive by nature, so it always requires either an
1717
+ * interactive confirmation or `--yes`.
1718
+ */
1719
+ async function cmdUninstall(flags) {
1720
+ const json = hasFlag(flags, "json");
1721
+ const yes = hasFlag(flags, "yes");
1722
+ const wipeData = hasFlag(flags, "data");
1723
+ const interactive = !json && Boolean(process.stdin.isTTY && process.stdout.isTTY);
1724
+ if (!yes) {
1725
+ const message = wipeData
1726
+ ? "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)."
1727
+ : "Re-run with --yes to remove agent-memory's hooks, skills, MCP registrations, completions, and Pro plugin. Your memory data is left untouched.";
1728
+ if (interactive) {
1729
+ const question = wipeData
1730
+ ? "This will also permanently delete your memory data (MEMORY.md, daily logs, scratchpad). Continue?"
1731
+ : "Remove agent-memory's hooks, skills, MCP registrations, completions, and Pro plugin?";
1732
+ if (!(await promptYesNo(question, false))) {
1733
+ console.log("Aborted. Nothing was removed.");
1734
+ return;
1735
+ }
1736
+ }
1737
+ else {
1738
+ if (json)
1739
+ output({ ok: false, error: { code: "confirmation_required", message } }, true);
1740
+ else
1741
+ console.error(`Error: ${message}`);
1742
+ process.exitCode = 1;
1743
+ return;
1744
+ }
1745
+ }
1746
+ const steps = [];
1747
+ try {
1748
+ const report = uninstallSkills();
1749
+ if (!report.ok)
1750
+ throw new Error(report.error ?? "failed to remove skills");
1751
+ steps.push({
1752
+ name: "skills",
1753
+ ok: true,
1754
+ detail: report.removed.length ? `removed ${report.removed.length}` : "not installed",
1755
+ });
1756
+ }
1757
+ catch (error) {
1758
+ steps.push({ name: "skills", ok: false, detail: error.message });
1759
+ }
1760
+ try {
1761
+ const report = uninstallHooks();
1762
+ if (!report.ok)
1763
+ throw new Error(report.error ?? "failed to remove hooks");
1764
+ const removed = report.results.filter((r) => r.installed).length;
1765
+ // agent-memory never removes pi-memory itself (see uninstallPiMemoryDelegate) — make that
1766
+ // explicit here rather than letting a generic "removed N" detail imply everything is gone.
1767
+ const piResult = report.results.find((r) => r.key === "pi");
1768
+ const piNote = piResult?.reason?.startsWith("pi-memory left installed") ? `; ${piResult.reason}` : "";
1769
+ steps.push({
1770
+ name: "hooks",
1771
+ ok: true,
1772
+ detail: (removed ? `removed ${removed}` : "not installed") + piNote,
1773
+ });
1774
+ }
1775
+ catch (error) {
1776
+ steps.push({ name: "hooks", ok: false, detail: error.message });
1777
+ }
1778
+ try {
1779
+ const results = unregisterMcpFromAgents(null);
1780
+ const removed = results.filter((r) => r.status === "unregistered").length;
1781
+ steps.push({ name: "mcp", ok: true, detail: removed ? `unregistered ${removed}` : "not registered" });
1782
+ }
1783
+ catch (error) {
1784
+ steps.push({ name: "mcp", ok: false, detail: error.message });
1785
+ }
1786
+ try {
1787
+ const results = uninstallCompletion();
1788
+ const removed = results.filter((r) => r.removed || r.profileUpdated).length;
1789
+ steps.push({ name: "completions", ok: true, detail: removed ? `removed ${removed}` : "not installed" });
1790
+ }
1791
+ catch (error) {
1792
+ steps.push({ name: "completions", ok: false, detail: error.message });
1793
+ }
1794
+ try {
1795
+ const manager = createDefaultPluginBootstrap(VERSION);
1796
+ const pluginResult = await manager.uninstall();
1797
+ steps.push({ name: "plugin", ok: pluginResult.ok, detail: pluginResult.result });
1798
+ }
1799
+ catch (error) {
1800
+ steps.push({ name: "plugin", ok: false, detail: error.message });
1801
+ }
1802
+ if (wipeData) {
1803
+ try {
1804
+ const memoryDir = getMemoryDir();
1805
+ if (fs.existsSync(memoryDir))
1806
+ fs.rmSync(memoryDir, { recursive: true, force: true });
1807
+ const pluginRoot = getDefaultPluginInstallRoot();
1808
+ if (fs.existsSync(pluginRoot))
1809
+ fs.rmSync(pluginRoot, { recursive: true, force: true });
1810
+ steps.push({ name: "data", ok: true, detail: memoryDir });
1811
+ }
1812
+ catch (error) {
1813
+ steps.push({ name: "data", ok: false, detail: error.message });
1814
+ }
1815
+ }
1816
+ const allOk = steps.every((step) => step.ok);
1817
+ if (!allOk)
1818
+ process.exitCode = 1;
1819
+ if (json) {
1820
+ output({ ok: allOk, data: wipeData, steps }, true);
1821
+ return;
1822
+ }
1823
+ console.log("");
1824
+ console.log(colorize("agent-memory uninstall", "bold"));
1825
+ for (const step of steps) {
1826
+ const mark = step.ok ? MARK_OK : MARK_FAIL;
1827
+ const detail = step.detail ? colorize(` ${step.detail}`, "dim") : "";
1828
+ console.log(` ${mark} ${step.name}${detail}`);
1829
+ }
1830
+ console.log("");
1831
+ if (!allOk) {
1832
+ console.log(colorize("Some steps failed — see details above.", "yellow"));
1833
+ }
1834
+ else if (wipeData) {
1835
+ console.log(colorize("agent-memory has been fully removed, including your memory data.", "green"));
1836
+ }
1837
+ else {
1838
+ console.log(colorize("agent-memory's install artifacts have been removed.", "green"));
1839
+ console.log(colorize(`Your notes are untouched at ${getMemoryDir()}. Re-run with --data to remove them too.`, "dim"));
1840
+ }
1841
+ }
1665
1842
  /**
1666
1843
  * Explain the optional plugin without making a successful core setup feel
1667
1844
  * incomplete. Keep the quota line here in sync with `freeEntitlement()`.
@@ -1897,6 +2074,14 @@ async function cmdDoctor(flags) {
1897
2074
  });
1898
2075
  }
1899
2076
  }
2077
+ // Auto-upgrade policy
2078
+ const upgradePolicy = readUpgradePolicy();
2079
+ rows.push({
2080
+ status: "ok",
2081
+ label: "Auto-upgrade",
2082
+ detail: `CLI: ${upgradePolicy.cli}, Pro: ${upgradePolicy.plugin}`,
2083
+ fix: upgradePolicy.cli === "off" && upgradePolicy.plugin === "off" ? "agent-memory upgrade policy auto" : undefined,
2084
+ });
1900
2085
  // Skills + hooks per detected host
1901
2086
  const { homeDir, targets } = detectHookAgents();
1902
2087
  const detected = targets.filter((target) => target.detected);
@@ -1912,7 +2097,7 @@ async function cmdDoctor(flags) {
1912
2097
  rows.push({
1913
2098
  status: "warn",
1914
2099
  label: "Agent hosts",
1915
- detail: "no supported agents detected (Claude Code, Codex, Cursor, opencode)",
2100
+ detail: "no supported agents detected (Claude Code, Codex, Cursor, opencode, pi)",
1916
2101
  fix: "install one of the agents first, then: agent-memory install-skills",
1917
2102
  });
1918
2103
  }
@@ -1924,7 +2109,35 @@ async function cmdDoctor(flags) {
1924
2109
  });
1925
2110
  for (const target of detected) {
1926
2111
  if (!target.supported) {
1927
- // Skip skill/hook rows for hosts we don't yet integrate with (e.g. pi has its own extension).
2112
+ // Skip skill/hook rows for hosts we don't yet integrate with.
2113
+ continue;
2114
+ }
2115
+ if (target.key === "pi") {
2116
+ // pi has no SKILL.md — agent-memory delegates entirely to the pi-memory
2117
+ // extension, installed via `pi install npm:pi-memory` rather than a
2118
+ // config-file edit, so it gets its own row instead of the generic
2119
+ // Skill:/Hook: pair below.
2120
+ const installed = homeDir ? isHookInstalled(homeDir, "pi") : false;
2121
+ const state = getPiMemoryState();
2122
+ let detail;
2123
+ if (installed) {
2124
+ detail = "pi-memory extension active (github.com/jayzeng/pi-memory)";
2125
+ }
2126
+ else if (state && !state.ok) {
2127
+ detail = `pi-memory not detected — last install attempt (${state.lastAttemptAt}) failed: ${state.detail}`;
2128
+ }
2129
+ else if (state?.ok) {
2130
+ detail = `pi-memory installed (${state.lastAttemptAt}) but no session has run yet`;
2131
+ }
2132
+ else {
2133
+ detail = "pi-memory not detected — agent-memory can install it";
2134
+ }
2135
+ rows.push({
2136
+ status: installed ? "ok" : "warn",
2137
+ label: "Memory: pi",
2138
+ detail,
2139
+ fix: installed ? undefined : "agent-memory install-hooks --only pi",
2140
+ });
1928
2141
  continue;
1929
2142
  }
1930
2143
  const skillPath = homeDir ? `${target.homeMarker}/skills/agent-memory/SKILL.md` : null;
@@ -2479,16 +2692,108 @@ async function resolvePluginLatestHint() {
2479
2692
  return { latest: null, updateAvailable: false };
2480
2693
  }
2481
2694
  }
2482
- async function cmdUpgrade(flags) {
2695
+ async function cmdUpgradePolicy(flags, positional) {
2696
+ const json = hasFlag(flags, "json");
2697
+ const value = positional[0];
2698
+ if (!value) {
2699
+ const policy = readUpgradePolicy();
2700
+ if (json)
2701
+ output({ cli: policy.cli, plugin: policy.plugin }, true);
2702
+ else
2703
+ console.log(` cli: ${policy.cli}\n plugin: ${policy.plugin}`);
2704
+ return;
2705
+ }
2706
+ if (value !== "off" && value !== "notify" && value !== "auto") {
2707
+ exitError(`Invalid policy '${value}'. Expected one of: off, notify, auto.`, json);
2708
+ }
2709
+ const onlyCli = hasFlag(flags, "cli");
2710
+ const onlyPlugin = hasFlag(flags, "plugin");
2711
+ const patch = {};
2712
+ if (onlyCli || !onlyPlugin)
2713
+ patch.cli = value;
2714
+ if (onlyPlugin || !onlyCli)
2715
+ patch.plugin = value;
2716
+ const next = writeUpgradePolicy(patch);
2717
+ if (json)
2718
+ output(next, true);
2719
+ else
2720
+ console.log(` cli: ${next.cli}\n plugin: ${next.plugin}`);
2721
+ }
2722
+ /**
2723
+ * `--background`-mode install: only touches a target when its persisted policy
2724
+ * (see `readUpgradePolicy`) is `"auto"`. Always non-interactive — this path is
2725
+ * only ever reached from a detached, non-TTY child spawned by
2726
+ * `refreshUpgradeCacheBackground()`. Failures are recorded, never thrown.
2727
+ */
2728
+ async function runAutoUpgrade(status, policy, pluginCurrent, quiet) {
2729
+ const now = new Date().toISOString();
2730
+ let cliAuto;
2731
+ let pluginAuto;
2732
+ if (policy.cli === "auto" && status.cli.upgradeAvailable) {
2733
+ const method = detectInstallMethod();
2734
+ if (!quiet)
2735
+ console.log(`Auto-upgrading CLI via: ${method.command.join(" ")}`);
2736
+ try {
2737
+ const result = runInstaller(method);
2738
+ cliAuto = result.ok
2739
+ ? { at: now, ok: true, version: status.cli.latest }
2740
+ : {
2741
+ at: now,
2742
+ ok: false,
2743
+ version: status.cli.current,
2744
+ error: (result.stderr || result.stdout || `exit ${result.code}`).trim(),
2745
+ };
2746
+ }
2747
+ catch (error) {
2748
+ cliAuto = {
2749
+ at: now,
2750
+ ok: false,
2751
+ version: status.cli.current,
2752
+ error: error instanceof Error ? error.message : String(error),
2753
+ };
2754
+ }
2755
+ }
2756
+ if (policy.plugin === "auto" && status.plugin.upgradeAvailable) {
2757
+ if (!quiet)
2758
+ console.log("Auto-upgrading Pro plugin bundle…");
2759
+ try {
2760
+ const pluginResult = await cmdPlugin({ json: true }, ["update"]);
2761
+ const ok = Boolean(pluginResult?.ok);
2762
+ pluginAuto = ok
2763
+ ? { at: now, ok: true, version: status.plugin.latest }
2764
+ : {
2765
+ at: now,
2766
+ ok: false,
2767
+ version: pluginCurrent,
2768
+ error: pluginResult?.error?.message ?? "plugin update failed",
2769
+ };
2770
+ }
2771
+ catch (error) {
2772
+ pluginAuto = {
2773
+ at: now,
2774
+ ok: false,
2775
+ version: pluginCurrent,
2776
+ error: error instanceof Error ? error.message : String(error),
2777
+ };
2778
+ }
2779
+ }
2780
+ return { cliAuto, pluginAuto };
2781
+ }
2782
+ async function cmdUpgrade(flags, positional = []) {
2783
+ if (positional[0] === "policy") {
2784
+ await cmdUpgradePolicy(flags, positional.slice(1));
2785
+ return;
2786
+ }
2483
2787
  const json = hasFlag(flags, "json");
2484
2788
  const quiet = hasFlag(flags, "quiet");
2485
2789
  const checkOnly = hasFlag(flags, "check");
2790
+ const background = hasFlag(flags, "background");
2486
2791
  const refresh = hasFlag(flags, "refresh");
2487
2792
  const onlyCli = hasFlag(flags, "cli");
2488
2793
  const onlyPlugin = hasFlag(flags, "plugin");
2489
2794
  const targetCli = onlyCli || !onlyPlugin;
2490
2795
  const targetPlugin = onlyPlugin || !onlyCli;
2491
- const applyAll = hasFlag(flags, "yes") || !process.stdin.isTTY || !process.stdout.isTTY;
2796
+ const applyAll = hasFlag(flags, "yes") || background || !process.stdin.isTTY || !process.stdout.isTTY;
2492
2797
  const pluginCurrent = await readPluginCurrentVersion();
2493
2798
  const pluginProbe = targetPlugin ? await resolvePluginLatestHint() : { latest: null, updateAvailable: false };
2494
2799
  const status = await checkForUpgrades({
@@ -2505,6 +2810,30 @@ async function cmdUpgrade(flags) {
2505
2810
  printUpgradeStatus(status, { targetCli, targetPlugin });
2506
2811
  return;
2507
2812
  }
2813
+ if (background) {
2814
+ const policy = readUpgradePolicy();
2815
+ const { cliAuto, pluginAuto } = await runAutoUpgrade(status, policy, pluginCurrent, quiet);
2816
+ writeUpgradeCache({
2817
+ checkedAt: status.checkedAt,
2818
+ cliCurrent: VERSION,
2819
+ cliLatest: status.cli.latest,
2820
+ pluginCurrent,
2821
+ pluginLatest: status.plugin.latest,
2822
+ cliAuto,
2823
+ pluginAuto,
2824
+ });
2825
+ if (json)
2826
+ output({ ...status, cliAuto, pluginAuto }, true);
2827
+ else if (!quiet) {
2828
+ if (cliAuto)
2829
+ console.log(` ${cliAuto.ok ? MARK_OK : MARK_FAIL} CLI auto-upgrade: ${cliAuto.ok ? `→ ${cliAuto.version}` : cliAuto.error}`);
2830
+ if (pluginAuto)
2831
+ console.log(` ${pluginAuto.ok ? MARK_OK : MARK_FAIL} Pro auto-upgrade: ${pluginAuto.ok ? `→ ${pluginAuto.version}` : pluginAuto.error}`);
2832
+ }
2833
+ if ((cliAuto && !cliAuto.ok) || (pluginAuto && !pluginAuto.ok))
2834
+ process.exitCode = 1;
2835
+ return;
2836
+ }
2508
2837
  const cliNeedsUpgrade = targetCli && status.cli.upgradeAvailable;
2509
2838
  const pluginNeedsUpgrade = targetPlugin && status.plugin.upgradeAvailable;
2510
2839
  if (!cliNeedsUpgrade && !pluginNeedsUpgrade) {
@@ -2645,6 +2974,64 @@ function registerMcpInAgents(only) {
2645
2974
  }
2646
2975
  return results;
2647
2976
  }
2977
+ /**
2978
+ * Reverse of {@link registerMcpInAgents}: removes the `agent-memory` MCP server
2979
+ * entry from every supported local harness. Missing config files are reported
2980
+ * as `not-installed`, files that never had the entry as `not-registered`.
2981
+ */
2982
+ function unregisterMcpFromAgents(only) {
2983
+ const home = os.homedir();
2984
+ const want = (key) => !only || only.has(key);
2985
+ const results = [];
2986
+ const unregisterJson = (key, displayName, configFile) => {
2987
+ const p = path.join(home, configFile);
2988
+ if (!fs.existsSync(p)) {
2989
+ results.push({ key, displayName, path: p, status: "not-installed" });
2990
+ return;
2991
+ }
2992
+ let s = {};
2993
+ try {
2994
+ s = JSON.parse(fs.readFileSync(p, "utf8"));
2995
+ }
2996
+ catch { }
2997
+ const servers = (s.mcpServers ?? {});
2998
+ if (!servers["agent-memory"]) {
2999
+ results.push({ key, displayName, path: p, status: "not-registered" });
3000
+ return;
3001
+ }
3002
+ delete servers["agent-memory"];
3003
+ if (Object.keys(servers).length === 0)
3004
+ delete s.mcpServers;
3005
+ else
3006
+ s.mcpServers = servers;
3007
+ fs.writeFileSync(p, `${JSON.stringify(s, null, 2)}\n`);
3008
+ results.push({ key, displayName, path: p, status: "unregistered" });
3009
+ };
3010
+ if (want("claude"))
3011
+ unregisterJson("claude", "Claude Code", ".claude.json");
3012
+ if (want("cursor"))
3013
+ unregisterJson("cursor", "Cursor", ".cursor/mcp.json");
3014
+ if (want("windsurf"))
3015
+ unregisterJson("windsurf", "Windsurf", ".windsurf/mcp_settings.json");
3016
+ if (want("codex")) {
3017
+ const p = path.join(home, ".codex", "config.toml");
3018
+ if (!fs.existsSync(p)) {
3019
+ results.push({ key: "codex", displayName: "Codex", path: p, status: "not-installed" });
3020
+ }
3021
+ else {
3022
+ const existing = fs.readFileSync(p, "utf8");
3023
+ if (!existing.includes("[mcp_servers.agent-memory]")) {
3024
+ results.push({ key: "codex", displayName: "Codex", path: p, status: "not-registered" });
3025
+ }
3026
+ else {
3027
+ const pattern = /\n?\[mcp_servers\.agent-memory\]\n(?:(?!\[)[^\n]*\n?)*/;
3028
+ fs.writeFileSync(p, existing.replace(pattern, ""), "utf8");
3029
+ results.push({ key: "codex", displayName: "Codex", path: p, status: "unregistered" });
3030
+ }
3031
+ }
3032
+ }
3033
+ return results;
3034
+ }
2648
3035
  async function cmdServe(flags) {
2649
3036
  const isMcp = hasFlag(flags, "mcp");
2650
3037
  const isRegister = hasFlag(flags, "register");
@@ -2686,12 +3073,13 @@ async function cmdServe(flags) {
2686
3073
  // Core tools: free tier, available without Pro.
2687
3074
  server.addTool({
2688
3075
  name: "memory_read",
2689
- description: "Read the current long-term memory (MEMORY.md) and scratchpad checklist.",
3076
+ 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.",
2690
3077
  inputSchema: { type: "object", properties: {} },
2691
3078
  }, async () => {
2692
3079
  const memFile = getMemoryFile();
2693
3080
  const scratchFile = getScratchpadFile();
2694
- const memory = redactSecrets(readFileSafe(memFile) ?? "").content || "(empty)";
3081
+ const memory = redactSecrets(readFileSafe(memFile) ?? "").content ||
3082
+ '(empty — this only covers what was explicitly saved. For things said in prior chat sessions, try `agent-memory recall "<query>"` or the `session_recall`/`session_search` MCP tools, if AgentMemory Pro is installed.)';
2695
3083
  const scratchRaw = readFileSafe(scratchFile) ?? "";
2696
3084
  const scratchItems = parseScratchpad(scratchRaw)
2697
3085
  .filter((item) => !item.done)
@@ -2732,7 +3120,7 @@ async function cmdServe(flags) {
2732
3120
  try {
2733
3121
  await runtime.load();
2734
3122
  for (const tool of runtime.getMcpTools()) {
2735
- server.addTool({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }, (input) => tool.run(input));
3123
+ server.addTool({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }, (input) => runtime.runMcpTool(tool.name, input));
2736
3124
  }
2737
3125
  server.addStartupHook(() => runtime.runMcpStartup());
2738
3126
  }
@@ -2749,7 +3137,7 @@ function printUsage() {
2749
3137
  ["Do things", ["save", "note", "recall", "search"]],
2750
3138
  ["See things", ["status", "doctor", "dashboard"]],
2751
3139
  ["Advanced", ["write", "read", "context", "scratchpad", "distil", "sync"]],
2752
- ["Setup", ["setup", "install-skills", "install-hooks", "completion"]],
3140
+ ["Setup", ["setup", "install-skills", "install-hooks", "completion", "uninstall"]],
2753
3141
  ["Pro", ["pro", "learn"]],
2754
3142
  ];
2755
3143
  const knownGroupCommands = new Set(groups.flatMap(([, list]) => list));
@@ -2850,7 +3238,7 @@ async function main() {
2850
3238
  process.stdout.isTTY &&
2851
3239
  !json &&
2852
3240
  command &&
2853
- !["init", "help", "version", "doctor", "status", "completion", "hook", "serve"].includes(command) &&
3241
+ !["init", "help", "version", "doctor", "status", "completion", "hook", "serve", "uninstall"].includes(command) &&
2854
3242
  !fs.existsSync(getMemoryDir())) {
2855
3243
  console.log(colorize("It looks like this is your first run — no memory directory yet.", "yellow"));
2856
3244
  if (await promptYesNo("Run agent-memory init to set things up?", true)) {
@@ -2962,6 +3350,9 @@ async function main() {
2962
3350
  case "uninstall-hooks":
2963
3351
  cmdUninstallHooks(flags);
2964
3352
  break;
3353
+ case "uninstall":
3354
+ await cmdUninstall(flags);
3355
+ break;
2965
3356
  case "hook": {
2966
3357
  const sub = positional[0];
2967
3358
  if (sub !== "session-start" && sub !== "user-prompt-submit" && sub !== "stop") {
@@ -2984,6 +3375,13 @@ async function main() {
2984
3375
  const layer = readHookMode() === "per-turn" ? "stable" : undefined;
2985
3376
  await cmdContext(layer ? { "no-search": true, layer } : { "no-search": true });
2986
3377
  try {
3378
+ const policy = readUpgradePolicy();
3379
+ if (!policy.existed) {
3380
+ // Persist the (possibly env-overridden) defaults now, so this notice
3381
+ // only ever fires once — the file's mere existence is the "seen" flag.
3382
+ writeUpgradePolicy({ cli: policy.cli, plugin: policy.plugin });
3383
+ console.error("agent-memory: auto-upgrade is on by default (CLI + Pro plugin). Disable with: agent-memory upgrade policy off");
3384
+ }
2987
3385
  const cache = readUpgradeCache();
2988
3386
  if (cache) {
2989
3387
  const status = await checkForUpgrades({
@@ -2991,7 +3389,7 @@ async function main() {
2991
3389
  pluginCurrent: cache.pluginCurrent,
2992
3390
  cacheOnly: true,
2993
3391
  });
2994
- const notice = formatUpgradeNotice(status);
3392
+ const notice = formatUpgradeNotice(status, cache);
2995
3393
  if (notice)
2996
3394
  console.error(notice);
2997
3395
  }
@@ -3037,7 +3435,7 @@ async function main() {
3037
3435
  await cmdPro(flags, positional);
3038
3436
  break;
3039
3437
  case "upgrade":
3040
- await cmdUpgrade(flags);
3438
+ await cmdUpgrade(flags, positional);
3041
3439
  break;
3042
3440
  case "serve":
3043
3441
  await cmdServe(flags);