myagentmemory 0.4.16 → 0.5.1

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
@@ -23,11 +23,16 @@
23
23
  */
24
24
  import { spawn } from "node:child_process";
25
25
  import * as fs from "node:fs";
26
+ import * as os from "node:os";
27
+ import * as path from "node:path";
28
+ import { COMMAND_DESCRIPTIONS, COMMAND_OPTIONS, COMMANDS, GLOBAL_OPTIONS, optionTakesValue, PLUGIN_COMMAND_OPTIONS, renderCommandHelp, SCRATCHPAD_ACTION_OPTIONS, } from "./cli-spec.js";
26
29
  import { detectCompletionShell, generateCompletion, installCompletion } from "./completions.js";
27
- import { _setBaseDir, buildMemoryContext, checkCollection, dailyPath, detectQmd, distilMemories, ensureDirs, ensureQmdAvailableForSync, ensureQmdAvailableForUpdate, getCollectionName, getDailyDir, getMemoryDir, getMemoryFile, getQmdEmbedMode, getQmdHealth, getQmdResultPath, getQmdResultText, getScratchpadFile, getTopicsDir, installSkills, memoryWrite, nowTimestamp, parseScratchpad, probeEmbeddings, readFileSafe, redactSecrets, runQmdEmbedDetached, runQmdSearch, runQmdSync, runQmdUpdateNow, scheduleQmdUpdate, searchRelevantMemories, serializeScratchpad, setupQmdCollection, slugifyTopic, todayStr, topicPath, uninstallSkills, } from "./core.js";
28
- import { detectHookAgents, installHooks, uninstallHooks } from "./hooks.js";
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";
32
+ import { StdioMcpServer } from "./mcp-server.js";
29
33
  import { createDefaultPluginBootstrap, PluginBootstrapFailure, } from "./plugin-bootstrap.js";
30
34
  import { InstalledPluginRuntimeV1 } from "./plugin-runtime.js";
35
+ import { checkForUpgrades, detectInstallMethod, formatUpgradeNotice, isCacheFresh, readUpgradeCache, refreshUpgradeCacheBackground, runInstaller, } from "./upgrade.js";
31
36
  function readPackageVersion() {
32
37
  try {
33
38
  const packageJson = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
@@ -42,16 +47,38 @@ function parseArgs(argv) {
42
47
  const flags = {};
43
48
  const positional = [];
44
49
  let command = "";
45
- for (let i = 0; i < argv.length; i++) {
46
- const arg = argv[i];
50
+ // Normalize short flags to their long form so downstream code can rely on `flags.help`
51
+ // / `flags.version` without also checking `-h` / `-V`. Reserved shorthands only.
52
+ const SHORT_TO_LONG = { "-h": "--help", "-V": "--version" };
53
+ const normalized = argv.map((arg) => SHORT_TO_LONG[arg] ?? arg);
54
+ for (let i = 0; i < normalized.length; i++) {
55
+ const arg = normalized[i];
47
56
  if (!command && !arg.startsWith("-")) {
48
57
  command = arg;
49
58
  continue;
50
59
  }
51
60
  if (arg.startsWith("--")) {
61
+ // Support `--flag=value` in addition to `--flag value`.
62
+ const eqIdx = arg.indexOf("=");
63
+ if (eqIdx > 2) {
64
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
65
+ continue;
66
+ }
52
67
  const key = arg.slice(2);
53
- const next = argv[i + 1];
54
- if (next && !next.startsWith("--")) {
68
+ const next = normalized[i + 1];
69
+ // Use the option spec to decide whether this flag takes a value; that way boolean
70
+ // flags don't greedily consume the next positional argument. Unknown flags fall
71
+ // back to the "next token isn't a flag" heuristic.
72
+ const known = optionTakesValue(`--${key}`);
73
+ const looksLikeValue = next !== undefined && !next.startsWith("-");
74
+ if (known === true && looksLikeValue) {
75
+ flags[key] = next;
76
+ i++;
77
+ }
78
+ else if (known === false) {
79
+ flags[key] = true;
80
+ }
81
+ else if (looksLikeValue) {
55
82
  flags[key] = next;
56
83
  i++;
57
84
  }
@@ -73,6 +100,206 @@ function hasFlag(flags, key) {
73
100
  return key in flags;
74
101
  }
75
102
  // ---------------------------------------------------------------------------
103
+ // Fuzzy suggestions (levenshtein distance)
104
+ // ---------------------------------------------------------------------------
105
+ function levenshtein(a, b) {
106
+ if (a === b)
107
+ return 0;
108
+ if (!a.length)
109
+ return b.length;
110
+ if (!b.length)
111
+ return a.length;
112
+ const prev = new Array(b.length + 1);
113
+ const curr = new Array(b.length + 1);
114
+ for (let j = 0; j <= b.length; j++)
115
+ prev[j] = j;
116
+ for (let i = 1; i <= a.length; i++) {
117
+ curr[0] = i;
118
+ for (let j = 1; j <= b.length; j++) {
119
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
120
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
121
+ }
122
+ for (let j = 0; j <= b.length; j++)
123
+ prev[j] = curr[j];
124
+ }
125
+ return prev[b.length];
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // Pro plan / cap-exhausted UX
129
+ // ---------------------------------------------------------------------------
130
+ const UPGRADE_URL = "https://agentmemory.paperpilot.me/upgrade";
131
+ function detectCapExhausted(result) {
132
+ if (result.ok !== false)
133
+ return null;
134
+ const code = result.error?.code ?? "";
135
+ const message = result.error?.message ?? "";
136
+ // Authoritative signal: the plugin passed a decision through the data channel.
137
+ let decisionExhausted = false;
138
+ let resetAt;
139
+ let used;
140
+ let limit;
141
+ let remaining;
142
+ if (result.data && typeof result.data === "object") {
143
+ const decision = result.data.decision;
144
+ if (decision) {
145
+ decisionExhausted = decision.state === "exhausted";
146
+ if (typeof decision.resetAt === "string")
147
+ resetAt = decision.resetAt;
148
+ if (typeof decision.used === "number")
149
+ used = decision.used;
150
+ if (typeof decision.limit === "number")
151
+ limit = decision.limit;
152
+ if (typeof decision.remaining === "number")
153
+ remaining = decision.remaining;
154
+ }
155
+ }
156
+ // Only treat semantic exhaustion codes as cap-hits. Transient throttles
157
+ // (rate_limit, too_many_requests, HTTP 429) are NOT exhaustion — they must
158
+ // bubble as normal errors so we don't mislead the user with an upgrade prompt.
159
+ const hardExhaustion = /^(session_exhausted|quota_exceeded|preview_(limit|exhausted)|allowance_exhausted|session_capacity)$/i.test(code);
160
+ const messageExhaustion = /free preview (limit|allowance)|daily limit reached|no recalls remaining/i.test(message);
161
+ if (!decisionExhausted && !hardExhaustion && !messageExhaustion)
162
+ return null;
163
+ // Fallback: look for an ISO-ish timestamp in the message.
164
+ if (!resetAt) {
165
+ const match = /(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?)/.exec(message);
166
+ if (match)
167
+ resetAt = match[1];
168
+ }
169
+ return { code, message, resetAt, used, limit, remaining };
170
+ }
171
+ function formatResetTime(resetAt) {
172
+ if (!resetAt)
173
+ return "later today";
174
+ const parsed = Date.parse(resetAt);
175
+ if (!Number.isFinite(parsed))
176
+ return resetAt;
177
+ const now = Date.now();
178
+ const diffMs = parsed - now;
179
+ if (diffMs <= 0)
180
+ return "now";
181
+ const hours = Math.floor(diffMs / 3_600_000);
182
+ const minutes = Math.floor((diffMs % 3_600_000) / 60_000);
183
+ if (hours > 0)
184
+ return `${hours}h ${minutes}m`;
185
+ return `${minutes}m`;
186
+ }
187
+ function printCapExhaustedBox(command, info) {
188
+ const reset = formatResetTime(info.resetAt);
189
+ const usedLine = info.used !== undefined && info.limit !== undefined
190
+ ? ` You've used all ${info.used}/${info.limit} free '${command}' calls today (resets in ${reset}).`
191
+ : ` '${command}' free preview limit hit (resets in ${reset}).`;
192
+ const lines = [
193
+ "",
194
+ "─────────────────────────────────────────────────────────────",
195
+ usedLine,
196
+ "",
197
+ " Upgrade for unlimited recall + automatic capture:",
198
+ ` ${UPGRADE_URL}`,
199
+ "─────────────────────────────────────────────────────────────",
200
+ "",
201
+ ];
202
+ console.error(lines.join("\n"));
203
+ openExternalUrl(UPGRADE_URL);
204
+ }
205
+ // Persist the last usage decision to disk so `pro status` can show counters.
206
+ function cacheProUsage(decision) {
207
+ try {
208
+ const stateDir = `${getMemoryDir()}/state`;
209
+ fs.mkdirSync(stateDir, { recursive: true });
210
+ const path = `${stateDir}/pro-usage.json`;
211
+ let existing = {};
212
+ try {
213
+ existing = JSON.parse(fs.readFileSync(path, "utf-8"));
214
+ }
215
+ catch {
216
+ // missing or corrupt — start fresh
217
+ }
218
+ const capability = decision.capability ?? "session";
219
+ existing[capability] = {
220
+ used: decision.used,
221
+ limit: decision.limit,
222
+ remaining: decision.remaining,
223
+ resetAt: decision.resetAt,
224
+ state: decision.state,
225
+ recordedAt: new Date().toISOString(),
226
+ };
227
+ fs.writeFileSync(path, `${JSON.stringify(existing, null, 2)}\n`, { mode: 0o600 });
228
+ }
229
+ catch {
230
+ // caching is best-effort
231
+ }
232
+ }
233
+ function nearest(word, candidates, maxDistance = 2) {
234
+ let best;
235
+ for (const candidate of candidates) {
236
+ const distance = levenshtein(word, candidate);
237
+ if (distance <= maxDistance && (!best || distance < best.distance)) {
238
+ best = { word: candidate, distance };
239
+ }
240
+ }
241
+ return best?.word;
242
+ }
243
+ // ---------------------------------------------------------------------------
244
+ // Argument validation (whitelist from cli-spec)
245
+ // ---------------------------------------------------------------------------
246
+ // Internal / non-user commands that opt out of strict flag validation.
247
+ // `hook` is invoked by SessionStart wrappers with dynamic keys.
248
+ const UNVALIDATED_COMMANDS = new Set(["hook"]);
249
+ function allowedFlagsFor(command, positional) {
250
+ // `distill` (double-l) is an accepted alias for `distil` — share its flag whitelist.
251
+ const canonical = command === "distill" ? "distil" : command;
252
+ const allowed = new Set();
253
+ for (const opt of GLOBAL_OPTIONS)
254
+ allowed.add(opt.replace(/^-+/, ""));
255
+ for (const opt of COMMAND_OPTIONS[canonical] ?? [])
256
+ allowed.add(opt.replace(/^-+/, ""));
257
+ const sub = positional[0];
258
+ if (sub) {
259
+ if (canonical === "scratchpad") {
260
+ for (const opt of SCRATCHPAD_ACTION_OPTIONS[sub] ?? [])
261
+ allowed.add(opt.replace(/^-+/, ""));
262
+ }
263
+ if (canonical === "plugin" || canonical === "pro") {
264
+ const key = sub === "upgrade" ? "update" : sub;
265
+ for (const opt of PLUGIN_COMMAND_OPTIONS[key] ?? [])
266
+ allowed.add(opt.replace(/^-+/, ""));
267
+ }
268
+ }
269
+ return allowed;
270
+ }
271
+ function validateCommand(command) {
272
+ if (!command || UNVALIDATED_COMMANDS.has(command))
273
+ return;
274
+ const known = new Set([...COMMANDS, "distill", "hook"]);
275
+ if (known.has(command))
276
+ return;
277
+ // Unknown commands may still be plugin-provided; only warn when clearly a typo of a core command.
278
+ const suggestion = nearest(command, COMMANDS);
279
+ if (suggestion) {
280
+ console.error(`Error: Unknown command '${command}'. Did you mean '${suggestion}'?`);
281
+ console.error("Run 'agent-memory help' for the full list.");
282
+ process.exit(1);
283
+ }
284
+ // Fall through — plugin runtime will produce a definitive error if the command is truly unknown.
285
+ }
286
+ function validateFlags(command, positional, flags) {
287
+ if (!command || UNVALIDATED_COMMANDS.has(command))
288
+ return;
289
+ if (!COMMANDS.includes(command) && command !== "distill")
290
+ return; // plugin commands validate their own flags
291
+ const allowed = allowedFlagsFor(command, positional);
292
+ for (const key of Object.keys(flags)) {
293
+ if (allowed.has(key))
294
+ continue;
295
+ const suggestion = nearest(key, allowed);
296
+ const hint = suggestion ? `. Did you mean --${suggestion}?` : "";
297
+ console.error(`Error: Unknown flag --${key} for '${command}'${hint}`);
298
+ console.error(`Run 'agent-memory ${command} --help' for valid flags.`);
299
+ process.exit(1);
300
+ }
301
+ }
302
+ // ---------------------------------------------------------------------------
76
303
  // Output helpers
77
304
  // ---------------------------------------------------------------------------
78
305
  function output(data, json) {
@@ -118,24 +345,69 @@ function openExternalUrl(url) {
118
345
  return false;
119
346
  }
120
347
  }
348
+ // ---------------------------------------------------------------------------
349
+ // Color output (respects NO_COLOR and non-TTY)
350
+ // ---------------------------------------------------------------------------
351
+ const USE_COLOR = process.stdout.isTTY && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb";
352
+ const COLORS = {
353
+ reset: USE_COLOR ? "\x1b[0m" : "",
354
+ dim: USE_COLOR ? "\x1b[2m" : "",
355
+ bold: USE_COLOR ? "\x1b[1m" : "",
356
+ red: USE_COLOR ? "\x1b[31m" : "",
357
+ green: USE_COLOR ? "\x1b[32m" : "",
358
+ yellow: USE_COLOR ? "\x1b[33m" : "",
359
+ cyan: USE_COLOR ? "\x1b[36m" : "",
360
+ };
361
+ function colorize(text, color) {
362
+ return `${COLORS[color]}${text}${COLORS.reset}`;
363
+ }
364
+ const MARK_OK = colorize("✓", "green");
365
+ const MARK_WARN = colorize("⚠", "yellow");
366
+ const MARK_FAIL = colorize("✗", "red");
367
+ function readCachedProUsage() {
368
+ try {
369
+ const path = `${getMemoryDir()}/state/pro-usage.json`;
370
+ return JSON.parse(fs.readFileSync(path, "utf-8"));
371
+ }
372
+ catch {
373
+ return {};
374
+ }
375
+ }
376
+ function printProUsageCounters() {
377
+ const cache = readCachedProUsage();
378
+ const entries = Object.entries(cache);
379
+ if (entries.length === 0)
380
+ return;
381
+ console.log("Free preview usage today (local cache):");
382
+ for (const [capability, entry] of entries) {
383
+ const used = entry.used ?? 0;
384
+ const limit = entry.limit ?? 0;
385
+ const resetIn = formatResetTime(entry.resetAt);
386
+ const label = capability === "session" ? "Session captures" : capability;
387
+ const marker = entry.state === "exhausted" ? " (exhausted)" : "";
388
+ console.log(` ${label.padEnd(20)} ${used}/${limit} · resets in ${resetIn}${marker}`);
389
+ }
390
+ console.log("");
391
+ }
121
392
  function printProOverview(installed) {
122
393
  console.log("");
123
- console.log("AgentMemory Pro includes:");
124
- console.log(" Session Intelligence Recall decisions and context across Pi, Codex, and Claude Code sessions.");
125
- console.log(" Guided Learning Turn repeated corrections into reviewable, reversible memory.");
126
- console.log(" Local Web Console Inspect memories, activity, health, and settings in your browser.");
394
+ console.log("Core remembers what you save. Pro learns from what you do.");
127
395
  console.log("");
128
- console.log("Your session content stays on this device.");
396
+ console.log("AgentMemory Pro:");
397
+ console.log(' Remember past sessions Ask "what did we decide about auth?" across Claude Code, Codex, and Cursor.');
398
+ 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.");
129
400
  console.log("");
130
401
  if (installed) {
402
+ printProUsageCounters();
131
403
  console.log("Try it:");
132
404
  console.log(' agent-memory recall "what did we decide about authentication?"');
133
405
  console.log(" agent-memory learn");
134
- console.log(" agent-memory web");
406
+ console.log(" agent-memory dashboard");
135
407
  }
136
408
  else {
137
- console.log("Start your Pro beta:");
138
- console.log(" agent-memory plugin install");
409
+ console.log("No account. No email. Free preview starts now:");
410
+ console.log(" agent-memory pro install");
139
411
  }
140
412
  }
141
413
  function printPluginResult(result, json, allowBrowser) {
@@ -175,10 +447,10 @@ function printPluginResult(result, json, allowBrowser) {
175
447
  break;
176
448
  case "not_installed":
177
449
  console.log("AgentMemory Pro is not installed.");
178
- console.log("Run: agent-memory plugin install");
450
+ console.log("Run: agent-memory pro install");
179
451
  break;
180
452
  case "auth_required":
181
- console.log("Run this command in an interactive terminal to enter an email and activate free daily access.");
453
+ console.log("Run agent-memory pro install to activate the free preview.");
182
454
  break;
183
455
  case "renewal_required":
184
456
  console.log("Renew AgentMemory Pro to continue using paid capabilities.");
@@ -210,13 +482,32 @@ async function cmdContext(flags) {
210
482
  const json = hasFlag(flags, "json");
211
483
  const noSearch = hasFlag(flags, "no-search");
212
484
  const query = getFlag(flags, "query") ?? "";
485
+ const layer = getFlag(flags, "layer");
213
486
  ensureDirs();
214
487
  if (!noSearch && query)
215
488
  await ensureQmdAvailableForSync();
216
489
  const searchResults = noSearch ? "" : await searchRelevantMemories(query);
217
- const context = buildMemoryContext(searchResults);
490
+ const coreContext = layer === "stable"
491
+ ? buildStableContext()
492
+ : layer === "dynamic"
493
+ ? buildDynamicContext(searchResults, query)
494
+ : buildMemoryContext(searchResults);
495
+ let pluginSections = [];
496
+ try {
497
+ pluginSections = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).provideContext({
498
+ host: "agent-memory-cli",
499
+ cwd: process.cwd(),
500
+ query: query || undefined,
501
+ signal: new AbortController().signal,
502
+ });
503
+ }
504
+ catch {
505
+ // Optional Pro context must never make public-core context unavailable.
506
+ }
507
+ const pluginContext = pluginSections.map((section) => `${section.label}\n\n${section.content}`).join("\n\n");
508
+ const context = [coreContext, pluginContext].filter(Boolean).join("\n\n");
218
509
  if (json) {
219
- output({ context, directory: getMemoryDir() }, true);
510
+ output({ context, directory: getMemoryDir(), ...(pluginSections.length ? { pluginSections } : {}) }, true);
220
511
  }
221
512
  else {
222
513
  if (context) {
@@ -224,6 +515,193 @@ async function cmdContext(flags) {
224
515
  }
225
516
  }
226
517
  }
518
+ /**
519
+ * Read stdin (up to 1 MB) as JSON. Returns null on non-TTY stdin, oversized
520
+ * payload, or JSON parse errors — the caller emits empty stdout in those
521
+ * cases so a malformed harness hook payload never poisons the conversation.
522
+ */
523
+ async function readStdinJson() {
524
+ if (process.stdin.isTTY)
525
+ return null;
526
+ const chunks = [];
527
+ let total = 0;
528
+ try {
529
+ for await (const chunk of process.stdin) {
530
+ const buffer = chunk instanceof Buffer ? chunk : Buffer.from(chunk);
531
+ total += buffer.length;
532
+ if (total > 1_000_000)
533
+ return null;
534
+ chunks.push(buffer);
535
+ }
536
+ const text = Buffer.concat(chunks).toString("utf-8").trim();
537
+ if (!text)
538
+ return null;
539
+ return JSON.parse(text);
540
+ }
541
+ catch {
542
+ return null;
543
+ }
544
+ }
545
+ /**
546
+ * Sanitize a user prompt into a search query. Mirrors the discipline in
547
+ * `searchRelevantMemories`: strip control chars, cap length. Never throws.
548
+ */
549
+ function sanitizePromptQuery(prompt) {
550
+ if (typeof prompt !== "string")
551
+ return "";
552
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: intentionally stripping control chars.
553
+ const stripped = prompt.replace(/[\x00-\x1f\x7f]/g, " ");
554
+ return stripped.trim().slice(0, 200);
555
+ }
556
+ /**
557
+ * UserPromptSubmit hook handler — fires on every user prompt in per-turn mode.
558
+ * Reads the harness's JSON payload from stdin, emits the dynamic context layer
559
+ * (daily logs + qmd search + plugin context), and refreshes background workers
560
+ * un-metered. Silently degrades to empty stdout on any failure or timeout so
561
+ * a broken install never blocks the user.
562
+ */
563
+ async function cmdUserPromptSubmit(_flags) {
564
+ const TIMEOUT_MS = 3_000;
565
+ const controller = new AbortController();
566
+ let timer;
567
+ const timeout = new Promise((resolve) => {
568
+ timer = setTimeout(() => {
569
+ controller.abort();
570
+ resolve();
571
+ }, TIMEOUT_MS);
572
+ });
573
+ const work = (async () => {
574
+ const payload = await readStdinJson();
575
+ if (!payload)
576
+ return;
577
+ const query = sanitizePromptQuery(payload.user_input ?? payload.prompt);
578
+ ensureDirs();
579
+ if (query)
580
+ await ensureQmdAvailableForSync({ signal: controller.signal });
581
+ const searchResults = query ? await searchRelevantMemories(query, { signal: controller.signal }) : "";
582
+ const coreContext = buildDynamicContext(searchResults, query);
583
+ const runtime = new InstalledPluginRuntimeV1({ coreVersion: VERSION });
584
+ let pluginSections = [];
585
+ try {
586
+ pluginSections = await runtime.provideContext({
587
+ host: "agent-memory-cli",
588
+ cwd: process.cwd(),
589
+ query: query || undefined,
590
+ signal: controller.signal,
591
+ });
592
+ }
593
+ catch {
594
+ // Plugin context is optional — silent degradation.
595
+ }
596
+ try {
597
+ await runtime.refreshBackgroundWorkers({
598
+ host: "agent-memory-cli",
599
+ cwd: process.cwd(),
600
+ signal: controller.signal,
601
+ });
602
+ }
603
+ catch {
604
+ // Worker refresh must never block the user prompt.
605
+ }
606
+ const pluginContext = pluginSections.map((section) => `${section.label}\n\n${section.content}`).join("\n\n");
607
+ const context = [coreContext, pluginContext].filter(Boolean).join("\n\n");
608
+ if (context)
609
+ process.stdout.write(context);
610
+ })().catch(() => {
611
+ // Any failure in the per-turn hook must be swallowed — never emit an
612
+ // error message that would leak into the harness's context.
613
+ });
614
+ await Promise.race([work, timeout]);
615
+ if (timer)
616
+ clearTimeout(timer);
617
+ }
618
+ // How many Stop events must elapse (per session_id) before the periodic
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;
622
+ // Bound state/stop-hook.json so it can't grow unboundedly across many sessions.
623
+ const STOP_HOOK_MAX_SESSIONS = 50;
624
+ function stopHookStatePath() {
625
+ return `${getMemoryDir()}/state/stop-hook.json`;
626
+ }
627
+ function readStopHookState() {
628
+ try {
629
+ const raw = fs.readFileSync(stopHookStatePath(), "utf-8");
630
+ const parsed = JSON.parse(raw);
631
+ return { sessions: parsed.sessions && typeof parsed.sessions === "object" ? parsed.sessions : {} };
632
+ }
633
+ catch {
634
+ return { sessions: {} };
635
+ }
636
+ }
637
+ function writeStopHookState(state) {
638
+ const entries = Object.entries(state.sessions).sort((a, b) => b[1].lastSeenAt - a[1].lastSeenAt);
639
+ const pruned = Object.fromEntries(entries.slice(0, STOP_HOOK_MAX_SESSIONS));
640
+ const stateDir = `${getMemoryDir()}/state`;
641
+ fs.mkdirSync(stateDir, { recursive: true });
642
+ fs.writeFileSync(stopHookStatePath(), `${JSON.stringify({ sessions: pruned }, null, 2)}\n`, { mode: 0o600 });
643
+ }
644
+ /**
645
+ * Bump the Stop-event counter for `sessionId` and report whether the
646
+ * periodic memory-write nudge should fire this time. Never throws — a
647
+ * corrupt or unwritable state file just means the nudge falls back to
648
+ * "never fires" rather than breaking the Stop hook.
649
+ */
650
+ function shouldNagOnStop(sessionId, now) {
651
+ try {
652
+ const state = readStopHookState();
653
+ const existing = state.sessions[sessionId] ?? { count: 0, lastNagCount: 0, lastSeenAt: now };
654
+ const count = existing.count + 1;
655
+ const shouldNag = count - existing.lastNagCount >= STOP_NAG_INTERVAL;
656
+ state.sessions[sessionId] = {
657
+ count,
658
+ lastNagCount: shouldNag ? count : existing.lastNagCount,
659
+ lastSeenAt: now,
660
+ };
661
+ writeStopHookState(state);
662
+ return shouldNag;
663
+ }
664
+ catch {
665
+ return false;
666
+ }
667
+ }
668
+ const STOP_NAG_REASON = "Before stopping: if this session produced a durable fact, bug fix, or decision worth remembering, " +
669
+ 'capture it now — `agent-memory write --content "..."` for a daily note, or `--target long_term` for a ' +
670
+ "durable fact — and update the scratchpad with any open follow-ups. If there's nothing worth recording, " +
671
+ "ignore this and stop normally.";
672
+ /**
673
+ * Stop hook handler — fires at the end of every assistant turn (not once per
674
+ * session). Blocks at most once every STOP_NAG_INTERVAL turns per session_id
675
+ * to nudge a memory-write check without being disruptive. Always allows the
676
+ * stop (empty stdout) on missing session_id, `stop_hook_active` (Claude Code's
677
+ * own re-entrancy signal — never block twice in a row), or any internal error.
678
+ */
679
+ async function cmdStop(_flags) {
680
+ const TIMEOUT_MS = 3_000;
681
+ const controller = new AbortController();
682
+ let timer;
683
+ const timeout = new Promise((resolve) => {
684
+ timer = setTimeout(() => {
685
+ controller.abort();
686
+ resolve();
687
+ }, TIMEOUT_MS);
688
+ });
689
+ const work = (async () => {
690
+ const payload = await readStdinJson();
691
+ const sessionId = typeof payload?.session_id === "string" ? payload.session_id : "";
692
+ if (!sessionId || payload?.stop_hook_active === true)
693
+ return;
694
+ if (shouldNagOnStop(sessionId, Date.now())) {
695
+ process.stdout.write(JSON.stringify({ decision: "block", reason: STOP_NAG_REASON }));
696
+ }
697
+ })().catch(() => {
698
+ // Any failure in the Stop hook must be swallowed — never trap the user
699
+ // in a stuck session over a broken memory-write nudge.
700
+ });
701
+ await Promise.race([work, timeout]);
702
+ if (timer)
703
+ clearTimeout(timer);
704
+ }
227
705
  async function cmdWrite(flags) {
228
706
  const json = hasFlag(flags, "json");
229
707
  const target = getFlag(flags, "target") ?? "daily";
@@ -442,15 +920,42 @@ async function cmdSearch(flags) {
442
920
  const collName = getCollectionName();
443
921
  const hasCollection = await checkCollection(collName);
444
922
  if (!hasCollection) {
445
- exitError(`qmd collection '${collName}' not found. Run: agent-memory init`, json);
923
+ exitError(`qmd collection '${collName}' not found. Run: agent-memory setup`, json);
446
924
  }
447
925
  try {
448
926
  const { results, stderr } = await runQmdSearch(mode, query, limit);
927
+ let recallHits = [];
928
+ if (results.length === 0) {
929
+ try {
930
+ const runtime = new InstalledPluginRuntimeV1({ coreVersion: VERSION });
931
+ const recallResult = await runtime.run("recall", {
932
+ args: [query],
933
+ flags: { limit: String(limit) },
934
+ signal: new AbortController().signal,
935
+ });
936
+ if (recallResult?.ok && Array.isArray(recallResult.data) && recallResult.data.length > 0) {
937
+ recallHits = recallResult.data;
938
+ }
939
+ }
940
+ catch {
941
+ // Pro not installed or recall unavailable — fall through to normal empty result
942
+ }
943
+ }
449
944
  if (json) {
450
- output({ mode, query, count: results.length, results }, true);
945
+ if (results.length === 0 && recallHits.length > 0) {
946
+ output({ mode, query, source: "recall", count: recallHits.length, results: recallHits }, true);
947
+ }
948
+ else {
949
+ output({ mode, query, source: "qmd", count: results.length, results }, true);
950
+ }
451
951
  return;
452
952
  }
453
953
  if (results.length === 0) {
954
+ if (recallHits.length > 0) {
955
+ console.log(`No hits in local memory files for "${query}" — falling back to prior sessions via Pro recall (${recallHits.length} hit(s)):\n`);
956
+ console.log(JSON.stringify(recallHits, null, 2));
957
+ return;
958
+ }
454
959
  const needsEmbed = /need embeddings/i.test(stderr ?? "");
455
960
  if (needsEmbed && (mode === "semantic" || mode === "deep")) {
456
961
  console.log(`No results found. qmd reports missing embeddings — run: qmd embed`);
@@ -539,6 +1044,11 @@ function cmdInstallSkills(flags) {
539
1044
  console.log(`Skipped ${item.label} (${item.reason})`);
540
1045
  }
541
1046
  }
1047
+ if (report.installed.length > 0 && process.stdout.isTTY) {
1048
+ const first = report.installed[0].label;
1049
+ console.log("");
1050
+ console.log(`Next: open ${first} and ask ${colorize('"what do you remember about me?"', "cyan")} to verify.`);
1051
+ }
542
1052
  }
543
1053
  async function promptYesNo(question, defaultYes) {
544
1054
  const readline = await import("node:readline/promises");
@@ -557,26 +1067,83 @@ async function cmdInstallHooks(flags) {
557
1067
  const json = hasFlag(flags, "json");
558
1068
  const requested = getFlag(flags, "only");
559
1069
  const requestedKeys = requested ? new Set(requested.split(",").map((value) => value.trim())) : null;
1070
+ const modeFlag = getFlag(flags, "mode");
1071
+ if (modeFlag !== undefined && modeFlag !== "stable" && modeFlag !== "per-turn") {
1072
+ exitError(`--mode must be 'stable' or 'per-turn' (got ${modeFlag})`, json);
1073
+ }
1074
+ const mode = modeFlag ?? "per-turn";
560
1075
  const { homeDir, targets } = detectHookAgents();
561
1076
  if (!homeDir)
562
1077
  exitError("Home directory not found.", json);
563
1078
  const eligible = targets.filter((target) => target.supported && target.detected && (!requestedKeys || requestedKeys.has(target.key)));
1079
+ if (!eligible.length) {
1080
+ if (json)
1081
+ return output({ ok: true, homeDir, results: [] }, true);
1082
+ return output("No eligible agents. Nothing to install.", false);
1083
+ }
1084
+ // Consider an agent "already installed" only when the wiring matches the requested mode.
1085
+ // per-turn requires BOTH SessionStart and UserPromptSubmit; stable requires SessionStart AND
1086
+ // no UserPromptSubmit (so a downgrade correctly removes the per-turn hook).
1087
+ const isFullyInstalled = (target) => {
1088
+ if (!homeDir)
1089
+ return false;
1090
+ const session = isHookInstalled(homeDir, target.key);
1091
+ if (!session)
1092
+ return false;
1093
+ if (target.key !== "claude" && target.key !== "codex")
1094
+ return true; // cursor/opencode: static only
1095
+ const prompt = isUserPromptSubmitInstalled(homeDir, target.key);
1096
+ if (mode === "per-turn" ? !prompt : prompt)
1097
+ return false;
1098
+ // Stop (write-side nudge) is Claude Code only and mode-independent.
1099
+ if (target.key === "claude") {
1100
+ if (!isStopHookInstalled(homeDir, target.key))
1101
+ return false;
1102
+ }
1103
+ return true;
1104
+ };
1105
+ const alreadyInstalled = eligible.filter(isFullyInstalled);
1106
+ const pending = eligible.filter((target) => !alreadyInstalled.includes(target));
1107
+ if (!json && alreadyInstalled.length) {
1108
+ const labels = alreadyInstalled.map((target) => target.label).join(", ");
1109
+ console.log(`Automatic context already active for: ${labels}.`);
1110
+ }
1111
+ 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);
1126
+ }
564
1127
  const selected = new Set();
565
1128
  const applyAll = hasFlag(flags, "yes") || hasFlag(flags, "all") || !process.stdin.isTTY;
566
- for (const target of eligible) {
567
- if (applyAll || (await promptYesNo(`Install SessionStart hook for ${target.label}?`, true)))
1129
+ const hookLabel = mode === "per-turn" ? "SessionStart + UserPromptSubmit hooks" : "SessionStart hook";
1130
+ for (const target of pending) {
1131
+ if (applyAll || (await promptYesNo(`Install ${hookLabel} for ${target.label}?`, true)))
568
1132
  selected.add(target.key);
569
1133
  }
570
- const report = installHooks(selected);
1134
+ if (!selected.size) {
1135
+ if (json)
1136
+ return output({ ok: true, homeDir, results: [] }, true);
1137
+ return output("Nothing selected. Skipped.", false);
1138
+ }
1139
+ const report = installHooks(selected, mode);
571
1140
  if (!report.ok)
572
1141
  exitError(report.error ?? "install failed", json);
573
1142
  if (json)
574
1143
  return output(report, true);
575
- if (!report.results.length)
576
- return output("No eligible agents. Nothing to install.", false);
577
1144
  for (const result of report.results) {
578
1145
  console.log(result.installed
579
- ? `Installed ${result.label} hook: ${result.path}`
1146
+ ? `Installed ${result.label} hook (${result.mode ?? mode}): ${result.path}`
580
1147
  : `Skipped ${result.label} (${result.reason ?? "unknown"})`);
581
1148
  }
582
1149
  }
@@ -628,7 +1195,7 @@ async function cmdSync(flags) {
628
1195
  const collName = getCollectionName();
629
1196
  const hasCollection = await checkCollection(collName);
630
1197
  if (!hasCollection) {
631
- exitError(`qmd collection '${collName}' not found. Run: agent-memory init`, json);
1198
+ exitError(`qmd collection '${collName}' not found. Run: agent-memory setup`, json);
632
1199
  }
633
1200
  const result = await runQmdSync();
634
1201
  if (json) {
@@ -654,6 +1221,9 @@ async function cmdSync(flags) {
654
1221
  }
655
1222
  async function cmdInit(flags) {
656
1223
  const json = hasFlag(flags, "json");
1224
+ const nonInteractive = json || hasFlag(flags, "yes") || !process.stdin.isTTY || !process.stdout.isTTY;
1225
+ const skipSkills = hasFlag(flags, "skip-skills");
1226
+ const skipHooks = hasFlag(flags, "skip-hooks");
657
1227
  ensureDirs();
658
1228
  const dir = getMemoryDir();
659
1229
  const qmdFound = await detectQmd();
@@ -666,7 +1236,6 @@ async function cmdInit(flags) {
666
1236
  if (!hasCollection) {
667
1237
  collectionCreated = await setupQmdCollection();
668
1238
  }
669
- // Run initial index update + start background embed
670
1239
  await ensureQmdAvailableForUpdate();
671
1240
  await runQmdUpdateNow();
672
1241
  indexUpdated = true;
@@ -674,121 +1243,515 @@ async function cmdInit(flags) {
674
1243
  embedStarted = child !== null;
675
1244
  }
676
1245
  if (json) {
677
- output({
678
- ok: true,
679
- directory: dir,
680
- qmd: qmdFound,
681
- collectionCreated,
682
- indexUpdated,
683
- embedStarted,
684
- }, true);
1246
+ output({ ok: true, directory: dir, qmd: qmdFound, collectionCreated, indexUpdated, embedStarted }, true);
1247
+ return;
685
1248
  }
686
- else {
1249
+ // ---------------------------------------------------------------------
1250
+ // Non-interactive path (backward compatible with existing `--yes` / CI use)
1251
+ // ---------------------------------------------------------------------
1252
+ if (nonInteractive) {
687
1253
  console.log(`Memory directory: ${dir}`);
688
- console.log(` MEMORY.md, SCRATCHPAD.md, daily/, topics/ created.`);
1254
+ console.log(` MEMORY.md, SCRATCHPAD.md, daily/, topics/ ready.`);
689
1255
  if (qmdFound) {
690
- if (collectionCreated) {
691
- console.log(` qmd collection '${getCollectionName()}' created.`);
692
- }
693
- else {
694
- console.log(` qmd collection '${getCollectionName()}' already exists.`);
695
- }
696
- if (indexUpdated) {
697
- console.log(` Index updated.`);
698
- }
699
- if (embedStarted) {
700
- console.log(` Embedding started in background.`);
701
- }
1256
+ console.log(` Search index ready.`);
1257
+ if (embedStarted)
1258
+ console.log(` Semantic search is preparing in the background.`);
702
1259
  }
703
1260
  else {
704
- console.log(` qmd not found — search features unavailable.`);
705
- console.log(` Install: bun install -g https://github.com/tobi/qmd`);
1261
+ console.log(` Search index unavailablekeyword search remains available.`);
1262
+ console.log(` Optional: install qmd with bun install -g https://github.com/tobi/qmd`);
706
1263
  }
707
- if (process.stdout.isTTY) {
708
- try {
709
- const plugin = await createDefaultPluginBootstrap(VERSION).list();
710
- if (plugin.result === "not_installed") {
711
- console.log("");
712
- console.log("Optional: AgentMemory Pro adds session recall and a local Web Console.");
713
- console.log("Run: agent-memory plugin install");
714
- }
715
- }
716
- catch {
717
- // Commercial discovery must never make core initialization fail.
718
- }
1264
+ return;
1265
+ }
1266
+ // ---------------------------------------------------------------------
1267
+ // Interactive wizard
1268
+ // ---------------------------------------------------------------------
1269
+ console.log("");
1270
+ console.log(colorize("Welcome to AgentMemory.", "bold"));
1271
+ console.log(colorize("Persistent memory for Claude Code, Codex, Cursor, and other coding agents.", "dim"));
1272
+ console.log("");
1273
+ console.log(` ${MARK_OK} Memory directory: ${dir}`);
1274
+ if (qmdFound) {
1275
+ console.log(` ${MARK_OK} Search index: ${collectionCreated ? "created" : "ready"}`);
1276
+ }
1277
+ else {
1278
+ console.log(` ${MARK_WARN} qmd not installed — search will be limited.`);
1279
+ console.log(` ${colorize("Install: bun install -g https://github.com/tobi/qmd", "cyan")}`);
1280
+ }
1281
+ console.log("");
1282
+ // Detect which agent hosts are present
1283
+ const { targets } = detectHookAgents();
1284
+ const detected = targets.filter((target) => target.detected && target.supported);
1285
+ if (detected.length === 0) {
1286
+ console.log(colorize(" No supported agents detected on this machine.", "yellow"));
1287
+ console.log(colorize(" Install Claude Code, Codex, or Cursor, then re-run.", "dim"));
1288
+ }
1289
+ else {
1290
+ console.log(colorize("Detected agents:", "bold"));
1291
+ for (const target of detected)
1292
+ console.log(` ${MARK_OK} ${target.label}`);
1293
+ console.log("");
1294
+ }
1295
+ // Step 1: install skills
1296
+ if (!skipSkills && detected.length > 0) {
1297
+ if (await promptYesNo("Install the AgentMemory skill so your agents can use memory?", true)) {
1298
+ cmdInstallSkills({});
719
1299
  }
720
1300
  }
721
- }
722
- async function cmdStatus(flags) {
723
- const json = hasFlag(flags, "json");
724
- ensureDirs();
725
- const dir = getMemoryDir();
1301
+ console.log("");
1302
+ // Step 2: install SessionStart hooks (delegates to existing interactive path)
1303
+ if (!skipHooks && detected.length > 0) {
1304
+ if (await promptYesNo("Install SessionStart hooks so context loads automatically?", true)) {
1305
+ await cmdInstallHooks({});
1306
+ }
1307
+ }
1308
+ console.log("");
1309
+ // Step 3: seed MEMORY.md so the skill's cold start isn't empty
726
1310
  const memFile = getMemoryFile();
727
- const spFile = getScratchpadFile();
728
- const dailyDir = getDailyDir();
729
- const topicsDir = getTopicsDir();
730
- const memContent = readFileSafe(memFile);
731
- const spContent = readFileSafe(spFile);
732
- let dailyCount = 0;
1311
+ const existing = readFileSafe(memFile) ?? "";
1312
+ if (existing.trim().length === 0) {
1313
+ const platform = process.platform === "darwin" ? "macOS" : process.platform;
1314
+ const seed = `<!-- ${nowTimestamp()} [init] -->\nAgentMemory initialized on ${platform} · ${todayStr()}. First session: check the scratchpad and daily log for context.\n`;
1315
+ fs.writeFileSync(memFile, seed);
1316
+ console.log(` ${MARK_OK} Seeded MEMORY.md with a first entry so your agent has something to read.`);
1317
+ }
1318
+ // Step 4: offer Pro preview if not installed
733
1319
  try {
734
- dailyCount = fs.readdirSync(dailyDir).filter((f) => f.endsWith(".md")).length;
1320
+ const plugin = await createDefaultPluginBootstrap(VERSION).list();
1321
+ if (plugin.result === "not_installed") {
1322
+ printProPitch("first-run");
1323
+ if (await promptYesNo("Preview what Pro would find in your existing session history?", true)) {
1324
+ await cmdProPreview({});
1325
+ }
1326
+ }
735
1327
  }
736
1328
  catch {
737
- // directory may not exist
1329
+ // Commercial discovery must never make core initialization fail.
738
1330
  }
739
- let topicCount = 0;
1331
+ // Cheat sheet
1332
+ console.log("");
1333
+ console.log(colorize("You're set. Try these:", "bold"));
1334
+ console.log(' agent-memory save "made progress on <thing>" — quick note in today\'s log');
1335
+ console.log(' agent-memory note "follow up on <thing>" — persistent todo item');
1336
+ console.log(' agent-memory recall "what did we decide about X?" — search past sessions (Pro)');
1337
+ console.log(" agent-memory doctor — health check");
1338
+ console.log("");
1339
+ console.log(colorize(`Now open one of ${detected.map((target) => target.label).join(", ") || "your agents"} and ask: "what do you remember about me?"`, "cyan"));
1340
+ }
1341
+ function readProUsageCache() {
740
1342
  try {
741
- topicCount = fs.readdirSync(topicsDir).filter((f) => f.endsWith(".md")).length;
1343
+ return JSON.parse(fs.readFileSync(`${getMemoryDir()}/state/pro-usage.json`, "utf-8"));
742
1344
  }
743
1345
  catch {
744
- // directory may not exist
1346
+ return {};
745
1347
  }
746
- const qmdFound = await detectQmd();
747
- let hasCollection = false;
748
- let health = null;
749
- let embeddings = "n/a";
750
- if (qmdFound) {
751
- hasCollection = await checkCollection();
752
- if (hasCollection) {
753
- await ensureQmdAvailableForSync();
754
- health = await getQmdHealth();
755
- // A live semantic probe confirms embeddings are actually usable, but
756
- // it costs a real qmd query (and a possible model load), so it's
757
- // opt-in the cheap pending-embed count below covers the common case.
758
- if (hasFlag(flags, "probe")) {
759
- embeddings = await probeEmbeddings();
1348
+ }
1349
+ async function printSetupUsageStats(pluginInstalled) {
1350
+ if (!pluginInstalled)
1351
+ return;
1352
+ try {
1353
+ const usage = readProUsageCache();
1354
+ const recall = usage.recall;
1355
+ const learn = usage.learn;
1356
+ const recallStr = recall?.used !== undefined && recall?.limit !== undefined
1357
+ ? `${recall.used}/${recall.limit} recalls today`
1358
+ : `0/20 recalls today`;
1359
+ const learnStr = learn?.used !== undefined && learn?.limit !== undefined
1360
+ ? `${learn.used}/${learn.limit} learnings today`
1361
+ : `0/5 learnings today`;
1362
+ console.log(colorize(` Plan: Free · ${recallStr} · ${learnStr}`, "dim"));
1363
+ let sessionStats = null;
1364
+ try {
1365
+ const result = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).run("index", {
1366
+ args: [],
1367
+ flags: {},
1368
+ signal: new AbortController().signal,
1369
+ });
1370
+ if (result?.ok && result.data && typeof result.data === "object") {
1371
+ const stats = result.data.stats;
1372
+ if (stats?.discovered) {
1373
+ sessionStats = {
1374
+ claude: stats.discovered.claude ?? 0,
1375
+ codex: stats.discovered.codex ?? 0,
1376
+ pi: stats.discovered.pi ?? 0,
1377
+ };
1378
+ }
1379
+ }
1380
+ }
1381
+ catch {
1382
+ // best-effort
1383
+ }
1384
+ if (sessionStats !== null) {
1385
+ const total = sessionStats.claude + sessionStats.codex + sessionStats.pi;
1386
+ if (total > 0) {
1387
+ const parts = [
1388
+ sessionStats.claude > 0 ? `Claude Code ${sessionStats.claude.toLocaleString("en-US")}` : null,
1389
+ sessionStats.codex > 0 ? `Codex ${sessionStats.codex.toLocaleString("en-US")}` : null,
1390
+ sessionStats.pi > 0 ? `Pi ${sessionStats.pi.toLocaleString("en-US")}` : null,
1391
+ ]
1392
+ .filter(Boolean)
1393
+ .join(" · ");
1394
+ console.log(colorize(` Sessions indexed: ${total.toLocaleString("en-US")} (${parts})`, "dim"));
760
1395
  }
761
1396
  }
1397
+ console.log("");
762
1398
  }
763
- const embedMode = getQmdEmbedMode();
764
- let officialPlugin = {
765
- installed: false,
766
- result: "unavailable",
767
- entitlement: "missing",
1399
+ catch {
1400
+ // stats are best-effort; never block setup completion
1401
+ }
1402
+ }
1403
+ /**
1404
+ * One-shot idempotent installer. Runs init + install-skills + install-hooks +
1405
+ * plugin install (if a bundle is discoverable) and then prints a one-page status
1406
+ * summary. Each step is a no-op when the target is already good, so `setup` is
1407
+ * safe to re-run after upgrades.
1408
+ *
1409
+ * The point of `setup` is that a first-time user only has to remember ONE
1410
+ * command. `init`, `install-skills`, `install-hooks`, `plugin install` still
1411
+ * exist for scripts and for finer control, but no one needs them for the happy
1412
+ * path.
1413
+ *
1414
+ * Interactivity model: on an interactive TTY (and when not `--json`) setup
1415
+ * will prompt exactly once — to install AgentMemory Pro — because the benefits
1416
+ * (cross-session recall, learn-from-corrections) are invisible without it.
1417
+ * With `--yes` we auto-install Pro; with `--json`, `--skip-plugin`, or a
1418
+ * non-TTY stdin/stdout we fall back to the previous passive hint so scripted
1419
+ * setups keep working unchanged.
1420
+ */
1421
+ async function cmdSetup(flags) {
1422
+ const json = hasFlag(flags, "json");
1423
+ const skipSkills = hasFlag(flags, "skip-skills");
1424
+ const skipHooks = hasFlag(flags, "skip-hooks");
1425
+ const skipPlugin = hasFlag(flags, "skip-plugin");
1426
+ const skipMcp = hasFlag(flags, "skip-mcp");
1427
+ const interactive = !json && Boolean(process.stdin.isTTY && process.stdout.isTTY);
1428
+ // setup is non-interactive by design. Sub-steps that print progress noise get their
1429
+ // stdout captured when we're in json mode, so the whole command emits ONE envelope.
1430
+ const subFlags = { yes: true };
1431
+ const steps = [];
1432
+ const runQuiet = async (fn) => {
1433
+ if (!json) {
1434
+ await fn();
1435
+ return;
1436
+ }
1437
+ const originalLog = console.log;
1438
+ const originalInfo = console.info;
1439
+ console.log = () => { };
1440
+ console.info = () => { };
1441
+ try {
1442
+ await fn();
1443
+ }
1444
+ finally {
1445
+ console.log = originalLog;
1446
+ console.info = originalInfo;
1447
+ }
768
1448
  };
1449
+ // Step 1: memory dir + qmd
769
1450
  try {
770
- const plugin = await createDefaultPluginBootstrap(VERSION).status();
771
- officialPlugin = {
772
- installed: Boolean(plugin.bundle),
773
- result: plugin.result,
774
- entitlement: plugin.entitlement.state,
775
- };
1451
+ await runQuiet(() => cmdInit({ ...subFlags, "skip-skills": true, "skip-hooks": true }));
1452
+ steps.push({ name: "memory", ok: true, detail: getMemoryDir() });
776
1453
  }
777
- catch {
778
- // Commercial status must never make core status fail.
1454
+ catch (error) {
1455
+ steps.push({ name: "memory", ok: false, detail: error.message });
779
1456
  }
780
- if (json) {
781
- output({
782
- directory: dir,
783
- memoryFile: {
784
- exists: memContent !== null,
785
- chars: memContent?.length ?? 0,
786
- lines: memContent ? memContent.split("\n").length : 0,
787
- },
788
- scratchpadFile: {
789
- exists: spContent !== null,
790
- items: spContent ? parseScratchpad(spContent).length : 0,
791
- openItems: spContent ? parseScratchpad(spContent).filter((i) => !i.done).length : 0,
1457
+ // Step 2: skills for detected agents
1458
+ if (!skipSkills) {
1459
+ try {
1460
+ await runQuiet(() => cmdInstallSkills(subFlags));
1461
+ steps.push({ name: "skills", ok: true });
1462
+ }
1463
+ catch (error) {
1464
+ steps.push({ name: "skills", ok: false, detail: error.message });
1465
+ }
1466
+ }
1467
+ else {
1468
+ steps.push({ name: "skills", ok: true, detail: "skipped" });
1469
+ }
1470
+ // Step 3: hooks (uses the improved preflight — silent when all already installed)
1471
+ if (!skipHooks) {
1472
+ try {
1473
+ await runQuiet(() => cmdInstallHooks(subFlags));
1474
+ steps.push({ name: "hooks", ok: true });
1475
+ }
1476
+ catch (error) {
1477
+ steps.push({ name: "hooks", ok: false, detail: error.message });
1478
+ }
1479
+ }
1480
+ else {
1481
+ steps.push({ name: "hooks", ok: true, detail: "skipped" });
1482
+ }
1483
+ // Step 4: install the local session-intelligence plugin by default. The free
1484
+ // allowance is part of the product experience, not an opt-in gate: users
1485
+ // should feel recall and learning before they ever see an upgrade prompt.
1486
+ let pluginJustInstalled = false;
1487
+ if (!skipPlugin) {
1488
+ const receiptPath = path.join(process.env.HOME ?? "", ".agent-memory/system/plugins/receipts/agentmemory.pro.json");
1489
+ const readReceiptVersion = () => {
1490
+ try {
1491
+ const receipt = JSON.parse(fs.readFileSync(receiptPath, "utf-8"));
1492
+ return receipt.version;
1493
+ }
1494
+ catch {
1495
+ return undefined;
1496
+ }
1497
+ };
1498
+ if (fs.existsSync(receiptPath)) {
1499
+ const version = readReceiptVersion();
1500
+ steps.push({
1501
+ name: "plugin",
1502
+ ok: true,
1503
+ detail: version ? `${version} installed` : "installed",
1504
+ });
1505
+ }
1506
+ else {
1507
+ // The free allowance is enabled automatically. `--skip-plugin` is the
1508
+ // explicit escape hatch for users who want core-only setup.
1509
+ const manager = createDefaultPluginBootstrap(VERSION);
1510
+ // Run install, capture the structured result (don't drop it), and
1511
+ // auto-recover from `version_conflict` — that just means a stale
1512
+ // bundle from an earlier dev iteration is sitting in
1513
+ // ~/.agent-memory/system/plugins/bundles/... with a different SHA.
1514
+ // A manual `plugin uninstall && plugin install` fixes it, so we do
1515
+ // the same automatically once during setup.
1516
+ const runInstall = async () => {
1517
+ let outcome;
1518
+ await runQuiet(async () => {
1519
+ outcome = await manager.install({ channel: "stable", allowAuthentication: interactive });
1520
+ });
1521
+ return outcome;
1522
+ };
1523
+ try {
1524
+ let installResult = await runInstall();
1525
+ if (!installResult.ok && installResult.error?.code === "version_conflict") {
1526
+ // `manager.uninstall()` only wipes bundles referenced by a
1527
+ // receipt. On version_conflict there is no receipt (that's
1528
+ // the whole point — an orphan bundle from an earlier failed
1529
+ // install is on disk with a different SHA). Try the API path
1530
+ // first, then fall back to removing the specific version
1531
+ // directory under ~/.agent-memory/system/plugins/bundles/.
1532
+ await runQuiet(async () => {
1533
+ await manager.uninstall();
1534
+ });
1535
+ try {
1536
+ const bundlesRoot = path.join(process.env.HOME ?? "", ".agent-memory/system/plugins/bundles/agentmemory.pro");
1537
+ if (fs.existsSync(bundlesRoot)) {
1538
+ for (const entry of fs.readdirSync(bundlesRoot)) {
1539
+ // Only touch semver-looking dirs; keep sibling
1540
+ // artefacts like `*.backup` untouched.
1541
+ if (!/^\d+\.\d+\.\d+/.test(entry))
1542
+ continue;
1543
+ const target = path.join(bundlesRoot, entry);
1544
+ try {
1545
+ fs.rmSync(target, { recursive: true, force: true });
1546
+ }
1547
+ catch {
1548
+ // Best-effort; the retry below will surface a real error if this mattered.
1549
+ }
1550
+ }
1551
+ }
1552
+ }
1553
+ catch {
1554
+ // Non-fatal — we retry the install regardless.
1555
+ }
1556
+ installResult = await runInstall();
1557
+ }
1558
+ if (installResult.ok &&
1559
+ (installResult.result === "installed" ||
1560
+ installResult.result === "upgraded" ||
1561
+ installResult.result === "current")) {
1562
+ const version = installResult.bundle?.version ?? readReceiptVersion();
1563
+ steps.push({
1564
+ name: "plugin",
1565
+ ok: true,
1566
+ detail: version ? `${version} installed` : "installed",
1567
+ });
1568
+ pluginJustInstalled = installResult.result !== "current";
1569
+ }
1570
+ else {
1571
+ const reasonCode = installResult.error?.code;
1572
+ const reasonMessage = installResult.error?.message ?? installResult.result ?? "unknown reason";
1573
+ const hint = reasonCode === "auth_required" || reasonCode === "renewal_required"
1574
+ ? "run `agent-memory plugin install` and complete the browser sign-in"
1575
+ : "run `agent-memory plugin install --json` for details";
1576
+ steps.push({
1577
+ name: "plugin",
1578
+ ok: false,
1579
+ detail: `install failed (${reasonMessage}) — ${hint}`,
1580
+ });
1581
+ }
1582
+ }
1583
+ catch (error) {
1584
+ steps.push({
1585
+ name: "plugin",
1586
+ ok: false,
1587
+ detail: `install failed — ${error.message}`,
1588
+ });
1589
+ }
1590
+ }
1591
+ }
1592
+ if (skipPlugin) {
1593
+ steps.push({ name: "plugin", ok: true, detail: "skipped — core-only setup requested" });
1594
+ }
1595
+ // Step 5: register the MCP server in every detected local harness (Claude
1596
+ // Code, Cursor, Windsurf, Codex). Idempotent — re-running setup after adding
1597
+ // a new agent will pick it up. Hooks handle passive injection; MCP is what
1598
+ // lets the model pull memory on demand as tools.
1599
+ let mcpRegisteredKeys = [];
1600
+ if (!skipMcp) {
1601
+ try {
1602
+ const results = registerMcpInAgents(null);
1603
+ const registered = results.filter((r) => r.status === "registered").map((r) => r.key);
1604
+ const already = results.filter((r) => r.status === "already").map((r) => r.key);
1605
+ mcpRegisteredKeys = registered;
1606
+ let detail;
1607
+ if (registered.length === 0 && already.length === 0) {
1608
+ detail = "no supported agents detected";
1609
+ }
1610
+ else if (registered.length === 0) {
1611
+ detail = `already registered (${already.join(", ")})`;
1612
+ }
1613
+ else if (already.length === 0) {
1614
+ detail = `registered (${registered.join(", ")})`;
1615
+ }
1616
+ else {
1617
+ detail = `registered (${registered.join(", ")}); already (${already.join(", ")})`;
1618
+ }
1619
+ steps.push({ name: "mcp", ok: true, detail });
1620
+ }
1621
+ catch (error) {
1622
+ steps.push({ name: "mcp", ok: false, detail: error.message });
1623
+ }
1624
+ }
1625
+ else {
1626
+ steps.push({ name: "mcp", ok: true, detail: "skipped" });
1627
+ }
1628
+ if (json) {
1629
+ output({ ok: steps.every((step) => step.ok), directory: getMemoryDir(), steps }, true);
1630
+ return;
1631
+ }
1632
+ console.log("");
1633
+ console.log(colorize("agent-memory setup", "bold"));
1634
+ for (const step of steps) {
1635
+ const mark = step.ok ? MARK_OK : MARK_FAIL;
1636
+ const detail = step.detail ? colorize(` ${step.detail}`, "dim") : "";
1637
+ console.log(` ${mark} ${step.name}${detail}`);
1638
+ }
1639
+ console.log("");
1640
+ if (mcpRegisteredKeys.length > 0) {
1641
+ console.log(colorize(`Restart ${mcpRegisteredKeys.join(", ")} to pick up the new MCP server.`, "dim"));
1642
+ }
1643
+ await printSetupUsageStats(steps.some((s) => s.name === "plugin" && s.ok));
1644
+ console.log(colorize("Setup complete. Your agents will discover memory automatically next session.", "green"));
1645
+ console.log(colorize("Your notes stay in plain Markdown on this device — no account, no upload.", "dim"));
1646
+ console.log(colorize('Open your agent and ask: "What do you remember about me?"', "cyan"));
1647
+ console.log("");
1648
+ console.log("Try it now:");
1649
+ console.log(` ${colorize('agent-memory save "your first note"', "cyan")} — save a note you own`);
1650
+ console.log(` ${colorize("agent-memory status", "cyan")} — verify everything is healthy`);
1651
+ if (pluginJustInstalled) {
1652
+ console.log("");
1653
+ console.log(colorize("The local plugin is live. Feel the magic now:", "green"));
1654
+ console.log(` ${colorize('agent-memory recall "what did we decide about auth?"', "cyan")} — search past sessions`);
1655
+ console.log(` ${colorize("agent-memory learn", "cyan")} — surface repeated corrections`);
1656
+ console.log(` ${colorize("agent-memory worker start", "cyan")} — capture new sessions in real time`);
1657
+ console.log(` ${colorize("agent-memory dashboard", "cyan")} — private local dashboard`);
1658
+ }
1659
+ else if (skipPlugin) {
1660
+ console.log(colorize("Core-only setup selected. Enable session intelligence anytime:", "dim"));
1661
+ console.log(` ${colorize("agent-memory plugin install", "cyan")} — recall and learn with a free daily allowance`);
1662
+ }
1663
+ console.log("");
1664
+ }
1665
+ /**
1666
+ * Explain the optional plugin without making a successful core setup feel
1667
+ * incomplete. Keep the quota line here in sync with `freeEntitlement()`.
1668
+ */
1669
+ function printProPitch(mode) {
1670
+ console.log("");
1671
+ if (mode === "reinstall") {
1672
+ console.log(`${MARK_WARN} ${colorize("The optional local plugin is not enabled.", "yellow")}`);
1673
+ console.log(colorize(" Core memory is already ready. The plugin adds:", "dim"));
1674
+ }
1675
+ else {
1676
+ console.log(`${colorize("Optional: AgentMemory Pro", "bold")} — ${colorize("memory that learns from your work", "dim")}`);
1677
+ }
1678
+ console.log(` ${colorize("Recall across sessions", "cyan")} Ask "what did we decide about auth?" across Claude, Codex, Cursor.`);
1679
+ console.log(` ${colorize("Learn from corrections", "cyan")} Turn repeated fixes into memory you can inspect and undo.`);
1680
+ console.log(` ${colorize("Real-time capture", "cyan")} Local worker indexes new sessions as they happen.`);
1681
+ console.log(` ${colorize("Private by default", "cyan")} Memory and session content stay on this device — no account required.`);
1682
+ console.log(` ${colorize("Included at no cost:", "green")} ${colorize("20 recalls + 5 learning scans per day", "bold")}. Local indexing and dashboard remain free.`);
1683
+ console.log("");
1684
+ }
1685
+ async function cmdStatus(flags) {
1686
+ const json = hasFlag(flags, "json");
1687
+ ensureDirs();
1688
+ const dir = getMemoryDir();
1689
+ const memFile = getMemoryFile();
1690
+ const spFile = getScratchpadFile();
1691
+ const dailyDir = getDailyDir();
1692
+ const topicsDir = getTopicsDir();
1693
+ const memContent = readFileSafe(memFile);
1694
+ const spContent = readFileSafe(spFile);
1695
+ let dailyCount = 0;
1696
+ try {
1697
+ dailyCount = fs.readdirSync(dailyDir).filter((f) => f.endsWith(".md")).length;
1698
+ }
1699
+ catch {
1700
+ // directory may not exist
1701
+ }
1702
+ let topicCount = 0;
1703
+ try {
1704
+ topicCount = fs.readdirSync(topicsDir).filter((f) => f.endsWith(".md")).length;
1705
+ }
1706
+ catch {
1707
+ // directory may not exist
1708
+ }
1709
+ const qmdFound = await detectQmd();
1710
+ let hasCollection = false;
1711
+ let health = null;
1712
+ let embeddings = "n/a";
1713
+ if (qmdFound) {
1714
+ hasCollection = await checkCollection();
1715
+ if (hasCollection) {
1716
+ await ensureQmdAvailableForSync();
1717
+ health = await getQmdHealth();
1718
+ // A live semantic probe confirms embeddings are actually usable, but
1719
+ // it costs a real qmd query (and a possible model load), so it's
1720
+ // opt-in — the cheap pending-embed count below covers the common case.
1721
+ if (hasFlag(flags, "probe")) {
1722
+ embeddings = await probeEmbeddings();
1723
+ }
1724
+ }
1725
+ }
1726
+ const embedMode = getQmdEmbedMode();
1727
+ let officialPlugin = {
1728
+ installed: false,
1729
+ result: "unavailable",
1730
+ entitlement: "missing",
1731
+ };
1732
+ try {
1733
+ const plugin = await createDefaultPluginBootstrap(VERSION).status();
1734
+ officialPlugin = {
1735
+ installed: Boolean(plugin.bundle),
1736
+ result: plugin.result,
1737
+ entitlement: plugin.entitlement.state,
1738
+ };
1739
+ }
1740
+ catch {
1741
+ // Commercial status must never make core status fail.
1742
+ }
1743
+ if (json) {
1744
+ output({
1745
+ directory: dir,
1746
+ memoryFile: {
1747
+ exists: memContent !== null,
1748
+ chars: memContent?.length ?? 0,
1749
+ lines: memContent ? memContent.split("\n").length : 0,
1750
+ },
1751
+ scratchpadFile: {
1752
+ exists: spContent !== null,
1753
+ items: spContent ? parseScratchpad(spContent).length : 0,
1754
+ openItems: spContent ? parseScratchpad(spContent).filter((i) => !i.done).length : 0,
792
1755
  },
793
1756
  dailyLogs: dailyCount,
794
1757
  topics: topicCount,
@@ -825,7 +1788,7 @@ async function cmdStatus(flags) {
825
1788
  console.log("");
826
1789
  if (qmdFound) {
827
1790
  console.log(`qmd: available`);
828
- console.log(`Collection '${getCollectionName()}': ${hasCollection ? "configured" : "not configured — run: agent-memory init"}`);
1791
+ console.log(`Collection '${getCollectionName()}': ${hasCollection ? "configured" : "not configured — run: agent-memory setup"}`);
829
1792
  console.log(`Embed mode: ${embedMode}`);
830
1793
  if (hasCollection && embeddings !== "n/a") {
831
1794
  const embLabel = embeddings === "ready"
@@ -853,11 +1816,325 @@ async function cmdStatus(flags) {
853
1816
  }
854
1817
  if (!officialPlugin.installed) {
855
1818
  console.log("");
856
- console.log("Optional official plugins: not installed");
857
- console.log(" run: agent-memory plugin install");
1819
+ console.log("AgentMemory Pro: not installed");
1820
+ console.log(" try without an account: agent-memory pro install");
858
1821
  }
859
1822
  }
860
1823
  }
1824
+ function printDoctorRow(row) {
1825
+ const mark = row.status === "ok" ? MARK_OK : row.status === "warn" ? MARK_WARN : MARK_FAIL;
1826
+ console.log(` ${mark} ${row.label.padEnd(28)} ${colorize(row.detail, "dim")}`);
1827
+ if (row.fix)
1828
+ console.log(` ${colorize(`↳ fix: ${row.fix}`, "cyan")}`);
1829
+ }
1830
+ async function cmdDoctor(flags) {
1831
+ const json = hasFlag(flags, "json");
1832
+ ensureDirs();
1833
+ const rows = [];
1834
+ // Memory dir
1835
+ const dir = getMemoryDir();
1836
+ let dirWritable = false;
1837
+ try {
1838
+ fs.accessSync(dir, fs.constants.W_OK);
1839
+ dirWritable = true;
1840
+ }
1841
+ catch {
1842
+ // noop
1843
+ }
1844
+ rows.push({
1845
+ status: dirWritable ? "ok" : "fail",
1846
+ label: "Memory directory",
1847
+ detail: dirWritable ? dir : `${dir} — not writable`,
1848
+ fix: dirWritable ? undefined : `chmod u+w ${dir}`,
1849
+ });
1850
+ // MEMORY.md seeded?
1851
+ const memContent = readFileSafe(getMemoryFile());
1852
+ rows.push({
1853
+ status: memContent && memContent.length > 0 ? "ok" : "warn",
1854
+ label: "MEMORY.md",
1855
+ detail: memContent && memContent.length > 0
1856
+ ? `${memContent.split("\n").length} lines, ${memContent.length} chars`
1857
+ : "empty — new sessions will see no long-term context",
1858
+ fix: memContent && memContent.length > 0
1859
+ ? undefined
1860
+ : 'agent-memory write --target long_term --content "…first fact…"',
1861
+ });
1862
+ // qmd
1863
+ const qmdFound = await detectQmd();
1864
+ if (!qmdFound) {
1865
+ rows.push({
1866
+ status: "warn",
1867
+ label: "qmd search index",
1868
+ detail: "not installed — keyword/semantic search unavailable",
1869
+ fix: "bun install -g https://github.com/tobi/qmd",
1870
+ });
1871
+ }
1872
+ else {
1873
+ const hasCollection = await checkCollection();
1874
+ if (!hasCollection) {
1875
+ rows.push({
1876
+ status: "warn",
1877
+ label: "qmd collection",
1878
+ detail: `'${getCollectionName()}' not configured`,
1879
+ fix: "agent-memory setup",
1880
+ });
1881
+ }
1882
+ else {
1883
+ try {
1884
+ await ensureQmdAvailableForSync();
1885
+ }
1886
+ catch {
1887
+ // noop
1888
+ }
1889
+ const health = await getQmdHealth();
1890
+ const files = health?.totalFiles ?? 0;
1891
+ const pending = health?.pendingEmbed ?? 0;
1892
+ rows.push({
1893
+ status: pending > 0 ? "warn" : "ok",
1894
+ label: "qmd search index",
1895
+ detail: `${files} files indexed${pending > 0 ? `, ${pending} pending embeds` : ""}`,
1896
+ fix: pending > 0 ? "agent-memory sync" : undefined,
1897
+ });
1898
+ }
1899
+ }
1900
+ // Skills + hooks per detected host
1901
+ const { homeDir, targets } = detectHookAgents();
1902
+ const detected = targets.filter((target) => target.detected);
1903
+ const hookMode = readHookMode();
1904
+ rows.push({
1905
+ status: "ok",
1906
+ label: "Hook mode",
1907
+ detail: hookMode === "per-turn"
1908
+ ? "per-turn (SessionStart stable snapshot + UserPromptSubmit query-scoped recall)"
1909
+ : "stable (SessionStart-only, full snapshot every session)",
1910
+ });
1911
+ if (detected.length === 0) {
1912
+ rows.push({
1913
+ status: "warn",
1914
+ label: "Agent hosts",
1915
+ detail: "no supported agents detected (Claude Code, Codex, Cursor, opencode)",
1916
+ fix: "install one of the agents first, then: agent-memory install-skills",
1917
+ });
1918
+ }
1919
+ else {
1920
+ rows.push({
1921
+ status: "ok",
1922
+ label: "Agent hosts detected",
1923
+ detail: detected.map((target) => target.label).join(", "),
1924
+ });
1925
+ for (const target of detected) {
1926
+ if (!target.supported) {
1927
+ // Skip skill/hook rows for hosts we don't yet integrate with (e.g. pi has its own extension).
1928
+ continue;
1929
+ }
1930
+ const skillPath = homeDir ? `${target.homeMarker}/skills/agent-memory/SKILL.md` : null;
1931
+ const skillInstalled = skillPath ? fs.existsSync(skillPath) : false;
1932
+ rows.push({
1933
+ status: skillInstalled ? "ok" : "warn",
1934
+ label: `Skill: ${target.label}`,
1935
+ detail: skillInstalled ? "SKILL.md installed" : "SKILL.md missing — agent cannot call memory",
1936
+ fix: skillInstalled ? undefined : "agent-memory install-skills",
1937
+ });
1938
+ const sessionInstalled = homeDir ? isHookInstalled(homeDir, target.key) : false;
1939
+ const supportsPerTurn = target.key === "claude" || target.key === "codex";
1940
+ // opencode only gets a static instructions file (no command-execution hook API in its
1941
+ // plugin surface we could verify) — never report it as a guaranteed-automatic hook.
1942
+ const guaranteedAutomatic = target.key !== "opencode";
1943
+ const promptInstalled = homeDir && supportsPerTurn ? isUserPromptSubmitInstalled(homeDir, target.key) : false;
1944
+ const wantsPerTurn = hookMode === "per-turn" && supportsPerTurn;
1945
+ // Stop backs the write side with a periodic memory-write nudge. Claude
1946
+ // Code only for now, mode-independent — always wanted when supported.
1947
+ const wantsWriteHooks = target.key === "claude";
1948
+ const stopInstalled = homeDir && wantsWriteHooks ? isStopHookInstalled(homeDir, target.key) : false;
1949
+ const writeHooksOk = !wantsWriteHooks || stopInstalled;
1950
+ const ok = sessionInstalled && guaranteedAutomatic && (wantsPerTurn ? promptInstalled : true) && writeHooksOk;
1951
+ let detail;
1952
+ if (!sessionInstalled) {
1953
+ detail = "not installed — no automatic context";
1954
+ }
1955
+ else if (!guaranteedAutomatic) {
1956
+ detail = "static instructions installed — model must run context manually, not guaranteed";
1957
+ }
1958
+ else if (!supportsPerTurn) {
1959
+ detail = "SessionStart hook active";
1960
+ }
1961
+ else if (wantsPerTurn && !promptInstalled) {
1962
+ detail = "SessionStart active, UserPromptSubmit missing — per-turn recall disabled";
1963
+ }
1964
+ else if (wantsPerTurn) {
1965
+ detail = "SessionStart + UserPromptSubmit hooks active";
1966
+ }
1967
+ else {
1968
+ detail = "SessionStart hook active";
1969
+ }
1970
+ if (wantsWriteHooks) {
1971
+ detail += stopInstalled ? "; Stop memory-write nudge active" : "; Stop memory-write nudge missing";
1972
+ }
1973
+ rows.push({
1974
+ status: ok ? "ok" : "warn",
1975
+ label: `Hook: ${target.label}`,
1976
+ detail,
1977
+ fix: ok ? undefined : `agent-memory install-hooks --mode ${hookMode}`,
1978
+ });
1979
+ }
1980
+ }
1981
+ // Pro plugin — emit exactly one row per outcome (bootstrap failure, not installed, or installed).
1982
+ let proBootstrapError;
1983
+ let proStatus;
1984
+ try {
1985
+ const plugin = await createDefaultPluginBootstrap(VERSION).status();
1986
+ proStatus = {
1987
+ installed: Boolean(plugin.bundle),
1988
+ result: plugin.result,
1989
+ entitlement: plugin.entitlement.state,
1990
+ };
1991
+ }
1992
+ catch (error) {
1993
+ proBootstrapError = error instanceof Error ? error.message : String(error);
1994
+ }
1995
+ if (proBootstrapError) {
1996
+ rows.push({
1997
+ status: "fail",
1998
+ label: "AgentMemory Pro",
1999
+ detail: `bootstrap failed: ${proBootstrapError}`,
2000
+ fix: "agent-memory pro install",
2001
+ });
2002
+ }
2003
+ else if (!proStatus?.installed) {
2004
+ rows.push({
2005
+ status: "warn",
2006
+ label: "AgentMemory Pro",
2007
+ detail: "not installed — recall + learn + dashboard unavailable",
2008
+ fix: "agent-memory pro install",
2009
+ });
2010
+ }
2011
+ else {
2012
+ rows.push({
2013
+ status: "ok",
2014
+ label: "AgentMemory Pro",
2015
+ detail: `installed (${proStatus.result}, entitlement=${proStatus.entitlement})`,
2016
+ });
2017
+ }
2018
+ if (json) {
2019
+ output({ rows }, true);
2020
+ if (rows.some((row) => row.status === "fail"))
2021
+ process.exitCode = 1;
2022
+ return;
2023
+ }
2024
+ console.log("");
2025
+ console.log(colorize("AgentMemory diagnostic", "bold"));
2026
+ console.log("");
2027
+ for (const row of rows)
2028
+ printDoctorRow(row);
2029
+ console.log("");
2030
+ const failed = rows.filter((row) => row.status === "fail").length;
2031
+ const warned = rows.filter((row) => row.status === "warn").length;
2032
+ if (failed > 0) {
2033
+ console.log(colorize(`${failed} issue(s) need attention.`, "red"));
2034
+ process.exitCode = 1;
2035
+ }
2036
+ else if (warned > 0) {
2037
+ console.log(colorize(`${warned} optional improvement(s) available.`, "yellow"));
2038
+ }
2039
+ else {
2040
+ console.log(colorize("Everything looks healthy.", "green"));
2041
+ }
2042
+ }
2043
+ async function cmdTutorial(flags) {
2044
+ const json = hasFlag(flags, "json");
2045
+ if (json) {
2046
+ exitError("tutorial is interactive — remove --json to run", true);
2047
+ }
2048
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
2049
+ exitError("tutorial requires an interactive terminal", false);
2050
+ }
2051
+ const os = await import("node:os");
2052
+ const originalDir = getMemoryDir();
2053
+ const sandboxDir = fs.mkdtempSync(`${os.tmpdir()}/agent-memory-tutorial-`);
2054
+ _setBaseDir(sandboxDir);
2055
+ try {
2056
+ console.log("");
2057
+ console.log(colorize("AgentMemory Tutorial (3 minutes)", "bold"));
2058
+ console.log(colorize("Everything below runs in a throwaway sandbox — your real memory is untouched.", "dim"));
2059
+ console.log(colorize(`Sandbox: ${sandboxDir}`, "dim"));
2060
+ console.log("");
2061
+ console.log(colorize("Real memory dir stays at:", "dim"), originalDir);
2062
+ console.log("");
2063
+ await promptEnter("Press Enter to begin.");
2064
+ // Step 1: init
2065
+ console.log("");
2066
+ console.log(colorize("Step 1/4 — init", "bold"));
2067
+ console.log("This creates MEMORY.md, SCRATCHPAD.md, daily/ and topics/ in the sandbox.");
2068
+ console.log("");
2069
+ await promptEnter("Ready? Press Enter to run `agent-memory init --yes`.");
2070
+ ensureDirs();
2071
+ console.log(` ${MARK_OK} Sandbox ready at ${sandboxDir}`);
2072
+ // Step 2: save
2073
+ console.log("");
2074
+ console.log(colorize("Step 2/4 — save a memory", "bold"));
2075
+ console.log("`save` appends a line to today's log. Try it:");
2076
+ console.log("");
2077
+ await promptEnter('Press Enter to run `agent-memory save "tutorial: first save"`.');
2078
+ await memoryWrite({ target: "daily", content: "tutorial: first save" });
2079
+ console.log(` ${MARK_OK} Saved. Look inside: ${sandboxDir}/daily/`);
2080
+ // Step 3: note (scratchpad)
2081
+ console.log("");
2082
+ console.log(colorize("Step 3/4 — track a follow-up", "bold"));
2083
+ console.log("`note` adds a persistent checklist item.");
2084
+ console.log("");
2085
+ await promptEnter('Press Enter to run `agent-memory note "tutorial: try recall"`.');
2086
+ // Direct scratchpad manipulation to keep the demo dep-free
2087
+ const spFile = getScratchpadFile();
2088
+ const existing = readFileSafe(spFile) ?? "# Scratchpad\n";
2089
+ fs.writeFileSync(spFile, `${existing}\n<!-- ${nowTimestamp()} [cli] -->\n- [ ] tutorial: try recall\n`);
2090
+ console.log(` ${MARK_OK} Added a scratchpad item you can complete later.`);
2091
+ // Step 4: context
2092
+ console.log("");
2093
+ console.log(colorize("Step 4/4 — inspect what the agent sees", "bold"));
2094
+ console.log("`context` builds the injected context block your agent reads at session start.");
2095
+ console.log("");
2096
+ await promptEnter("Press Enter to preview the context.");
2097
+ const context = buildMemoryContext("");
2098
+ const preview = context.split("\n").slice(0, 20).join("\n");
2099
+ console.log(colorize("--- context preview (first 20 lines) ---", "dim"));
2100
+ console.log(preview);
2101
+ console.log(colorize("--- end preview ---", "dim"));
2102
+ console.log("");
2103
+ console.log(colorize("You're done.", "bold"));
2104
+ console.log("");
2105
+ console.log("In your real setup:");
2106
+ console.log(" agent-memory setup — one-shot install: memory dir, skills, hooks, MCP");
2107
+ console.log(' agent-memory save "your note" — quick save');
2108
+ console.log(' agent-memory note "your todo" — scratchpad item');
2109
+ console.log(" agent-memory doctor — health check");
2110
+ console.log("");
2111
+ if (await promptYesNo("Remove the sandbox directory now?", true)) {
2112
+ try {
2113
+ fs.rmSync(sandboxDir, { recursive: true, force: true });
2114
+ console.log(` ${MARK_OK} Sandbox removed.`);
2115
+ }
2116
+ catch (error) {
2117
+ console.log(colorize(` Could not remove sandbox: ${error instanceof Error ? error.message : String(error)}`, "yellow"));
2118
+ }
2119
+ }
2120
+ else {
2121
+ console.log(` Sandbox preserved: ${sandboxDir}`);
2122
+ }
2123
+ }
2124
+ finally {
2125
+ _setBaseDir(originalDir);
2126
+ }
2127
+ }
2128
+ async function promptEnter(prompt) {
2129
+ const readline = await import("node:readline/promises");
2130
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
2131
+ try {
2132
+ await rl.question(`${colorize(prompt, "cyan")} `);
2133
+ }
2134
+ finally {
2135
+ rl.close();
2136
+ }
2137
+ }
861
2138
  async function cmdDistil(flags) {
862
2139
  const json = hasFlag(flags, "json");
863
2140
  const dryRun = hasFlag(flags, "dry-run");
@@ -892,9 +2169,10 @@ Usage:
892
2169
  agent-memory plugin uninstall --yes
893
2170
  agent-memory plugin manage [--no-browser]
894
2171
 
895
- The public core remains fully usable without AgentMemory Pro. Interactive install
896
- opens a loopback website for email activation and a configurable free daily
897
- agent-session allowance. Memory and session content stay on this device.`);
2172
+ The public core remains fully usable without AgentMemory Pro. Install uses a random
2173
+ installation identifier and requires no account or email. The free tier includes
2174
+ 20 recalls and 5 learning scans per local day; indexing and the Memory Dashboard
2175
+ remain available. Memory and session content stay on this device.`);
898
2176
  }
899
2177
  function pluginCommandFailure(command, error) {
900
2178
  return {
@@ -922,12 +2200,13 @@ async function cmdPlugin(flags, positional) {
922
2200
  const subcommand = positional[0] ?? "list";
923
2201
  if (subcommand === "help" || hasFlag(flags, "help")) {
924
2202
  printPluginUsage();
925
- return;
2203
+ return null;
926
2204
  }
927
2205
  const channel = getFlag(flags, "channel") ?? "stable";
928
2206
  if (channel !== "stable") {
929
- printPluginResult(pluginCommandFailure(subcommand, new PluginBootstrapFailure("channel_invalid", "--channel supports only 'stable'")), json, false);
930
- return;
2207
+ const failure = pluginCommandFailure(subcommand, new PluginBootstrapFailure("channel_invalid", "--channel supports only 'stable'"));
2208
+ printPluginResult(failure, json, false);
2209
+ return failure;
931
2210
  }
932
2211
  const allowBrowser = !json && !hasFlag(flags, "no-browser") && Boolean(process.stdin.isTTY && process.stdout.isTTY);
933
2212
  const manager = createDefaultPluginBootstrap(VERSION);
@@ -974,77 +2253,640 @@ async function cmdPlugin(flags, positional) {
974
2253
  result = pluginCommandFailure(subcommand, error);
975
2254
  }
976
2255
  printPluginResult(result, json, allowBrowser && (subcommand === "install" || subcommand === "manage"));
2256
+ return result;
2257
+ }
2258
+ async function printFirstRunProof() {
2259
+ try {
2260
+ const result = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).run("index", {
2261
+ args: [],
2262
+ flags: {},
2263
+ signal: new AbortController().signal,
2264
+ });
2265
+ if (!result?.ok || !result.data || typeof result.data !== "object")
2266
+ return;
2267
+ const stats = result.data.stats;
2268
+ if (!stats?.discovered)
2269
+ return;
2270
+ const hosts = [
2271
+ ["Claude Code", stats.discovered.claude ?? 0],
2272
+ ["Codex", stats.discovered.codex ?? 0],
2273
+ ["Pi", stats.discovered.pi ?? 0],
2274
+ ];
2275
+ console.log("");
2276
+ console.log("Found local coding history:");
2277
+ for (const [label, count] of hosts)
2278
+ console.log(` ${label.padEnd(13)} ${count} sessions`);
2279
+ console.log("");
2280
+ console.log(`${stats.selected ?? 0} sessions available for local recall. Nothing was uploaded.`);
2281
+ console.log("");
2282
+ console.log('Try: agent-memory recall "what did we decide about authentication?"');
2283
+ console.log("Open: agent-memory dashboard");
2284
+ }
2285
+ catch {
2286
+ // Personalized proof is helpful but must never turn a successful install into a failure.
2287
+ }
2288
+ }
2289
+ const PRO_PREVIEW_DAILY_SESSION_CAP = 50;
2290
+ const PRO_PREVIEW_DISCOVERY_FILE_CAP = 10_000;
2291
+ function previewSessionRoots() {
2292
+ return {
2293
+ claude: process.env.AGENT_MEMORY_CLAUDE_SESSION_ROOT ?? path.join(os.homedir(), ".claude", "projects"),
2294
+ codex: process.env.AGENT_MEMORY_CODEX_SESSION_ROOT ?? path.join(os.homedir(), ".codex", "sessions"),
2295
+ pi: process.env.AGENT_MEMORY_PI_SESSION_ROOT ?? path.join(os.homedir(), ".pi", "agent", "sessions"),
2296
+ };
2297
+ }
2298
+ function localDayKey(now = new Date()) {
2299
+ const year = now.getFullYear();
2300
+ const month = String(now.getMonth() + 1).padStart(2, "0");
2301
+ const day = String(now.getDate()).padStart(2, "0");
2302
+ return `${year}-${month}-${day}`;
2303
+ }
2304
+ function nextLocalMidnight(now = new Date()) {
2305
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).toISOString();
2306
+ }
2307
+ function previewUsagePath() {
2308
+ return path.join(process.env.AGENT_MEMORY_PLUGIN_DIR ?? path.join(os.homedir(), ".agent-memory", "system", "plugins"), "state", "pro-preview-usage.json");
2309
+ }
2310
+ function countSessionFiles(root, cap) {
2311
+ if (!fs.existsSync(root))
2312
+ return 0;
2313
+ let count = 0;
2314
+ const stack = [root];
2315
+ while (stack.length && count < cap) {
2316
+ const directory = stack.pop();
2317
+ let entries;
2318
+ try {
2319
+ entries = fs.readdirSync(directory, { withFileTypes: true });
2320
+ }
2321
+ catch {
2322
+ continue;
2323
+ }
2324
+ for (const entry of entries) {
2325
+ if (entry.name.startsWith("."))
2326
+ continue;
2327
+ const fullPath = path.join(directory, entry.name);
2328
+ if (entry.isDirectory()) {
2329
+ stack.push(fullPath);
2330
+ }
2331
+ else if (entry.isFile()) {
2332
+ count++;
2333
+ if (count >= cap)
2334
+ break;
2335
+ }
2336
+ }
2337
+ }
2338
+ return count;
2339
+ }
2340
+ function reserveProPreviewSessions(discovered) {
2341
+ const now = new Date();
2342
+ const date = localDayKey(now);
2343
+ const filePath = previewUsagePath();
2344
+ let used = 0;
2345
+ try {
2346
+ const existing = JSON.parse(fs.readFileSync(filePath, "utf-8"));
2347
+ if (existing.date === date && Number.isSafeInteger(existing.used))
2348
+ used = Math.max(0, Number(existing.used));
2349
+ }
2350
+ catch {
2351
+ // Missing or corrupt preview usage starts a fresh local day window.
2352
+ }
2353
+ const remainingBefore = Math.max(0, PRO_PREVIEW_DAILY_SESSION_CAP - used);
2354
+ const consumed = Math.min(discovered, remainingBefore);
2355
+ const nextUsed = used + consumed;
2356
+ try {
2357
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
2358
+ fs.writeFileSync(filePath, `${JSON.stringify({ schemaVersion: 1, date, used: nextUsed, updatedAt: now.toISOString() }, null, 2)}\n`, { mode: 0o600 });
2359
+ }
2360
+ catch {
2361
+ // Preview metering is best-effort and local only; never block the preview.
2362
+ }
2363
+ return {
2364
+ consumed,
2365
+ cap: {
2366
+ limit: PRO_PREVIEW_DAILY_SESSION_CAP,
2367
+ used: nextUsed,
2368
+ remaining: Math.max(0, PRO_PREVIEW_DAILY_SESSION_CAP - nextUsed),
2369
+ resetAt: nextLocalMidnight(now),
2370
+ exhausted: remainingBefore === 0 && discovered > 0,
2371
+ },
2372
+ };
2373
+ }
2374
+ function scanProPreviewSessions() {
2375
+ const roots = previewSessionRoots();
2376
+ const sessions = { claude: 0, codex: 0, pi: 0 };
2377
+ for (const host of Object.keys(roots)) {
2378
+ sessions[host] = countSessionFiles(roots[host], PRO_PREVIEW_DISCOVERY_FILE_CAP);
2379
+ }
2380
+ const discovered = sessions.claude + sessions.codex + sessions.pi;
2381
+ const { cap, consumed } = reserveProPreviewSessions(discovered);
2382
+ const previewed = Math.min(discovered, consumed);
2383
+ return { available: discovered > 0 && previewed > 0, sessions, discovered, previewed, cap };
2384
+ }
2385
+ async function cmdProPreview(flags) {
2386
+ const json = hasFlag(flags, "json");
2387
+ const preview = scanProPreviewSessions();
2388
+ if (json)
2389
+ output(preview, true);
2390
+ else
2391
+ renderProPreview(preview);
2392
+ }
2393
+ function renderProPreview(preview) {
2394
+ if (!preview.discovered) {
2395
+ console.log("No local coding history found yet. Try running a session in Claude Code or Codex first.");
2396
+ console.log("Then re-run: agent-memory pro preview");
2397
+ return;
2398
+ }
2399
+ if (preview.cap.exhausted) {
2400
+ console.log("");
2401
+ console.log(`Daily preview cap reached (${preview.cap.used}/${preview.cap.limit} sessions).`);
2402
+ console.log(`Resets at ${preview.cap.resetAt}.`);
2403
+ console.log("");
2404
+ console.log("Install to make your local history searchable:");
2405
+ console.log(" agent-memory pro install");
2406
+ return;
2407
+ }
2408
+ console.log("");
2409
+ console.log(colorize(` ${preview.previewed.toLocaleString("en-US")} of ${preview.discovered.toLocaleString("en-US")} local sessions previewed (nothing uploaded).`, "bold"));
2410
+ console.log("");
2411
+ console.log(` ${MARK_OK} Claude Code ${preview.sessions.claude.toLocaleString("en-US")}`);
2412
+ console.log(` ${MARK_OK} Codex ${preview.sessions.codex.toLocaleString("en-US")}`);
2413
+ if (preview.sessions.pi > 0) {
2414
+ console.log(` ${MARK_OK} Pi ${preview.sessions.pi.toLocaleString("en-US")}`);
2415
+ }
2416
+ console.log("");
2417
+ console.log(`Daily preview cap: ${preview.cap.used}/${preview.cap.limit} sessions used, ${preview.cap.remaining} remaining.`);
2418
+ console.log("");
2419
+ console.log("Install to make all of it searchable:");
2420
+ console.log(" agent-memory pro install");
2421
+ console.log("");
2422
+ console.log(colorize("No account. No email. Free preview starts now.", "dim"));
2423
+ }
2424
+ async function cmdPro(flags, positional) {
2425
+ const subcommand = positional[0];
2426
+ if (!subcommand) {
2427
+ await cmdPlugin(flags, ["list"]);
2428
+ return;
2429
+ }
2430
+ if (subcommand === "preview") {
2431
+ await cmdProPreview(flags);
2432
+ return;
2433
+ }
2434
+ const mapped = subcommand === "upgrade" ? "update" : subcommand;
2435
+ if (!["install", "status", "update", "manage"].includes(mapped)) {
2436
+ const json = hasFlag(flags, "json");
2437
+ const message = `Unknown Pro command: ${subcommand}. Available commands: install, preview, status, upgrade, manage.`;
2438
+ if (json)
2439
+ console.log(JSON.stringify({ error: message }));
2440
+ else
2441
+ console.error(`Error: ${message}`);
2442
+ process.exitCode = 1;
2443
+ return;
2444
+ }
2445
+ const result = await cmdPlugin(flags, [mapped]);
2446
+ if (!hasFlag(flags, "json") && mapped === "install" && ["installed", "upgraded"].includes(result?.result ?? ""))
2447
+ await printFirstRunProof();
2448
+ }
2449
+ // ---------------------------------------------------------------------------
2450
+ // Upgrade (CLI + Pro plugin bundle)
2451
+ // ---------------------------------------------------------------------------
2452
+ async function readPluginCurrentVersion() {
2453
+ try {
2454
+ const manager = createDefaultPluginBootstrap(VERSION);
2455
+ const status = await manager.status("stable");
2456
+ return status.bundle?.version ?? null;
2457
+ }
2458
+ catch {
2459
+ return null;
2460
+ }
2461
+ }
2462
+ async function resolvePluginLatestHint() {
2463
+ try {
2464
+ const manager = createDefaultPluginBootstrap(VERSION);
2465
+ const status = await manager.status("stable");
2466
+ // `status.result === "update_available"` means the release feed advertises
2467
+ // a newer signed release. `bundle.version` always reflects the *installed*
2468
+ // version, so we cannot report the new version number from this call —
2469
+ // only the fact that an upgrade exists. `checkForUpgrades` treats
2470
+ // `latest == null` + explicit `pluginUpgradeAvailable` accordingly.
2471
+ const updateAvailable = status.result === "update_available";
2472
+ return {
2473
+ latest: updateAvailable ? null : (status.bundle?.version ?? null),
2474
+ updateAvailable,
2475
+ result: status,
2476
+ };
2477
+ }
2478
+ catch {
2479
+ return { latest: null, updateAvailable: false };
2480
+ }
2481
+ }
2482
+ async function cmdUpgrade(flags) {
2483
+ const json = hasFlag(flags, "json");
2484
+ const quiet = hasFlag(flags, "quiet");
2485
+ const checkOnly = hasFlag(flags, "check");
2486
+ const refresh = hasFlag(flags, "refresh");
2487
+ const onlyCli = hasFlag(flags, "cli");
2488
+ const onlyPlugin = hasFlag(flags, "plugin");
2489
+ const targetCli = onlyCli || !onlyPlugin;
2490
+ const targetPlugin = onlyPlugin || !onlyCli;
2491
+ const applyAll = hasFlag(flags, "yes") || !process.stdin.isTTY || !process.stdout.isTTY;
2492
+ const pluginCurrent = await readPluginCurrentVersion();
2493
+ const pluginProbe = targetPlugin ? await resolvePluginLatestHint() : { latest: null, updateAvailable: false };
2494
+ const status = await checkForUpgrades({
2495
+ cliCurrent: VERSION,
2496
+ pluginCurrent,
2497
+ refresh,
2498
+ pluginLatestHint: pluginProbe.latest,
2499
+ pluginUpgradeAvailable: pluginProbe.updateAvailable,
2500
+ });
2501
+ if (checkOnly) {
2502
+ if (json)
2503
+ output(status, true);
2504
+ else if (!quiet)
2505
+ printUpgradeStatus(status, { targetCli, targetPlugin });
2506
+ return;
2507
+ }
2508
+ const cliNeedsUpgrade = targetCli && status.cli.upgradeAvailable;
2509
+ const pluginNeedsUpgrade = targetPlugin && status.plugin.upgradeAvailable;
2510
+ if (!cliNeedsUpgrade && !pluginNeedsUpgrade) {
2511
+ if (json)
2512
+ output({ ...status, action: "noop" }, true);
2513
+ else if (!quiet) {
2514
+ console.log(`agent-memory ${VERSION} is up to date${pluginCurrent ? ` (Pro ${pluginCurrent})` : ""}.`);
2515
+ }
2516
+ return;
2517
+ }
2518
+ if (!applyAll) {
2519
+ if (!quiet)
2520
+ printUpgradeStatus(status, { targetCli, targetPlugin });
2521
+ const confirmed = await promptYesNo("Install available upgrades now?", true);
2522
+ if (!confirmed) {
2523
+ if (json)
2524
+ output({ ...status, action: "aborted" }, true);
2525
+ else
2526
+ console.log("Upgrade cancelled.");
2527
+ return;
2528
+ }
2529
+ }
2530
+ const actions = [];
2531
+ if (cliNeedsUpgrade) {
2532
+ const method = detectInstallMethod();
2533
+ if (!quiet)
2534
+ console.log(`Upgrading CLI via: ${method.command.join(" ")}`);
2535
+ const result = runInstaller(method);
2536
+ actions.push({
2537
+ target: "cli",
2538
+ ok: result.ok,
2539
+ detail: result.ok
2540
+ ? `CLI upgraded to ${status.cli.latest ?? "latest"}`
2541
+ : (result.stderr || result.stdout || `exit ${result.code}`).trim(),
2542
+ });
2543
+ }
2544
+ if (pluginNeedsUpgrade) {
2545
+ if (!quiet)
2546
+ console.log("Upgrading Pro plugin bundle…");
2547
+ const pluginResult = await cmdPlugin({ json: json ? true : "" }, ["update"]);
2548
+ const ok = Boolean(pluginResult?.ok);
2549
+ actions.push({
2550
+ target: "plugin",
2551
+ ok,
2552
+ detail: ok
2553
+ ? `Pro upgraded to ${status.plugin.latest ?? "latest"}`
2554
+ : (pluginResult?.error?.message ?? "plugin update failed"),
2555
+ });
2556
+ }
2557
+ const allOk = actions.every((entry) => entry.ok);
2558
+ if (json)
2559
+ output({ ...status, actions, ok: allOk }, true);
2560
+ else if (!quiet) {
2561
+ for (const entry of actions)
2562
+ console.log(` ${entry.ok ? MARK_OK : MARK_FAIL} ${entry.detail}`);
2563
+ }
2564
+ if (!allOk)
2565
+ process.exitCode = 1;
2566
+ }
2567
+ function printUpgradeStatus(status, scope) {
2568
+ const lines = [];
2569
+ if (scope.targetCli) {
2570
+ const marker = status.cli.upgradeAvailable ? MARK_WARN : MARK_OK;
2571
+ const target = status.cli.latest ?? "unknown";
2572
+ lines.push(` ${marker} CLI ${status.cli.current} → ${target}${status.cli.upgradeAvailable ? " (upgrade available)" : ""}`);
2573
+ }
2574
+ if (scope.targetPlugin) {
2575
+ const current = status.plugin.current ?? "not installed";
2576
+ const target = status.plugin.latest ?? "unknown";
2577
+ const marker = status.plugin.upgradeAvailable ? MARK_WARN : MARK_OK;
2578
+ lines.push(` ${marker} Pro ${current} → ${target}${status.plugin.upgradeAvailable ? " (upgrade available)" : ""}`);
2579
+ }
2580
+ console.log(`Checked ${status.checkedAt}${status.fromCache ? " (cached)" : ""}`);
2581
+ for (const line of lines)
2582
+ console.log(line);
2583
+ }
2584
+ /**
2585
+ * Register the `agent-memory` MCP server across every supported local harness.
2586
+ * Idempotent: existing entries are preserved, missing agent dirs are reported
2587
+ * as `not-installed` rather than treated as an error. Returns a structured
2588
+ * result so both `serve --register` and `setup` can format it their own way.
2589
+ */
2590
+ function registerMcpInAgents(only) {
2591
+ const home = os.homedir();
2592
+ const mcpEntry = { type: "stdio", command: "agent-memory", args: ["serve", "--mcp"] };
2593
+ const want = (key) => !only || only.has(key);
2594
+ const results = [];
2595
+ const registerJson = (key, displayName, detectPaths, configFile) => {
2596
+ const p = path.join(home, configFile);
2597
+ const detected = detectPaths.some((rel) => fs.existsSync(path.join(home, rel)));
2598
+ if (!detected) {
2599
+ results.push({ key, displayName, path: p, status: "not-installed" });
2600
+ return;
2601
+ }
2602
+ let s = {};
2603
+ try {
2604
+ s = JSON.parse(fs.readFileSync(p, "utf8"));
2605
+ }
2606
+ catch { }
2607
+ const servers = (s.mcpServers ?? {});
2608
+ if (servers["agent-memory"]) {
2609
+ results.push({ key, displayName, path: p, status: "already" });
2610
+ return;
2611
+ }
2612
+ servers["agent-memory"] = mcpEntry;
2613
+ s.mcpServers = servers;
2614
+ fs.mkdirSync(path.dirname(p), { recursive: true });
2615
+ fs.writeFileSync(p, `${JSON.stringify(s, null, 2)}\n`);
2616
+ results.push({ key, displayName, path: p, status: "registered" });
2617
+ };
2618
+ // Claude Code stores user-level MCP servers in ~/.claude.json (top-level
2619
+ // `mcpServers` key), NOT in ~/.claude/settings.json. The settings file
2620
+ // holds hooks/permissions/env — Claude Code does not read MCP config from
2621
+ // there. Detect either the `.claude/` dir or `.claude.json` file so we
2622
+ // register whenever the user has Claude Code installed at all.
2623
+ if (want("claude"))
2624
+ registerJson("claude", "Claude Code", [".claude", ".claude.json"], ".claude.json");
2625
+ if (want("cursor"))
2626
+ registerJson("cursor", "Cursor", [".cursor"], ".cursor/mcp.json");
2627
+ if (want("windsurf"))
2628
+ registerJson("windsurf", "Windsurf", [".windsurf"], ".windsurf/mcp_settings.json");
2629
+ if (want("codex")) {
2630
+ const p = path.join(home, ".codex", "config.toml");
2631
+ if (!fs.existsSync(p)) {
2632
+ results.push({ key: "codex", displayName: "Codex", path: p, status: "not-installed" });
2633
+ }
2634
+ else {
2635
+ const existing = fs.readFileSync(p, "utf8");
2636
+ if (existing.includes("[mcp_servers.agent-memory]")) {
2637
+ results.push({ key: "codex", displayName: "Codex", path: p, status: "already" });
2638
+ }
2639
+ else {
2640
+ const block = '\n[mcp_servers.agent-memory]\ncommand = "agent-memory"\nargs = ["serve", "--mcp"]\n';
2641
+ fs.writeFileSync(p, existing.trimEnd() + block, "utf8");
2642
+ results.push({ key: "codex", displayName: "Codex", path: p, status: "registered" });
2643
+ }
2644
+ }
2645
+ }
2646
+ return results;
2647
+ }
2648
+ async function cmdServe(flags) {
2649
+ const isMcp = hasFlag(flags, "mcp");
2650
+ const isRegister = hasFlag(flags, "register");
2651
+ if (isRegister) {
2652
+ const only = getFlag(flags, "only");
2653
+ const onlySet = only ? new Set(only.split(",").map((s) => s.trim())) : null;
2654
+ const results = registerMcpInAgents(onlySet);
2655
+ const shown = onlySet ? results : results.filter((r) => r.status !== "not-installed");
2656
+ for (const r of shown) {
2657
+ if (r.status === "already")
2658
+ console.log(` ${r.displayName}: already registered`);
2659
+ else if (r.status === "registered")
2660
+ console.log(` ${r.displayName}: registered (${r.path})`);
2661
+ else
2662
+ console.log(` ${r.displayName}: not installed`);
2663
+ }
2664
+ const anyRegistered = results.some((r) => r.status === "registered");
2665
+ const anyDetected = results.some((r) => r.status !== "not-installed");
2666
+ if (anyRegistered) {
2667
+ console.log("");
2668
+ console.log("Restart the agent(s) above for the change to take effect.");
2669
+ }
2670
+ else if (!onlySet && !anyDetected) {
2671
+ console.log("No supported agents detected (Claude Code, Cursor, Windsurf, Codex).");
2672
+ console.log("Use --only <agent> to force: --only claude,cursor,windsurf,codex");
2673
+ }
2674
+ return;
2675
+ }
2676
+ if (!isMcp) {
2677
+ console.log("Usage: agent-memory serve --mcp");
2678
+ console.log(" agent-memory serve --register (register in all detected agents)");
2679
+ console.log(" agent-memory serve --register --only claude,cursor,windsurf,codex");
2680
+ console.log("");
2681
+ console.log("Supported agents: Claude Code, Cursor, Windsurf, Codex");
2682
+ console.log("Tip: run --register once, then restart the agent(s).");
2683
+ return;
2684
+ }
2685
+ const server = new StdioMcpServer(VERSION);
2686
+ // Core tools: free tier, available without Pro.
2687
+ server.addTool({
2688
+ name: "memory_read",
2689
+ description: "Read the current long-term memory (MEMORY.md) and scratchpad checklist.",
2690
+ inputSchema: { type: "object", properties: {} },
2691
+ }, async () => {
2692
+ const memFile = getMemoryFile();
2693
+ const scratchFile = getScratchpadFile();
2694
+ const memory = redactSecrets(readFileSafe(memFile) ?? "").content || "(empty)";
2695
+ const scratchRaw = readFileSafe(scratchFile) ?? "";
2696
+ const scratchItems = parseScratchpad(scratchRaw)
2697
+ .filter((item) => !item.done)
2698
+ .map((item) => `- [ ] ${redactSecrets(item.text).content}`);
2699
+ return [
2700
+ "## Long-term memory (MEMORY.md)",
2701
+ memory,
2702
+ "",
2703
+ "## Open scratchpad items",
2704
+ scratchItems.length ? scratchItems.join("\n") : "(none)",
2705
+ ].join("\n");
2706
+ });
2707
+ server.addTool({
2708
+ name: "memory_write",
2709
+ description: "Append a new entry to memory. Use target='daily' for session notes, 'long_term' for durable facts.",
2710
+ inputSchema: {
2711
+ type: "object",
2712
+ properties: {
2713
+ content: { type: "string", description: "Text to store" },
2714
+ target: { type: "string", description: "Where to write", enum: ["daily", "long_term", "scratchpad"] },
2715
+ },
2716
+ required: ["content"],
2717
+ },
2718
+ }, async (input) => {
2719
+ const content = typeof input.content === "string" ? input.content.trim() : "";
2720
+ const target = typeof input.target === "string" ? input.target : "daily";
2721
+ if (!content)
2722
+ return "Error: content is required";
2723
+ if (target === "scratchpad") {
2724
+ const result = await scratchpadAction({ action: "add", text: content, sessionId: "mcp-serve" });
2725
+ return result.text;
2726
+ }
2727
+ await memoryWrite({ target: target, content, sessionId: "mcp-serve" });
2728
+ return `Written to ${target} memory.`;
2729
+ });
2730
+ // Load Pro plugin and let it register additional MCP tools + startup hooks.
2731
+ const runtime = new InstalledPluginRuntimeV1({ coreVersion: VERSION });
2732
+ try {
2733
+ await runtime.load();
2734
+ for (const tool of runtime.getMcpTools()) {
2735
+ server.addTool({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }, (input) => tool.run(input));
2736
+ }
2737
+ server.addStartupHook(() => runtime.runMcpStartup());
2738
+ }
2739
+ catch {
2740
+ // Pro not installed or failed to load — serve with core tools only.
2741
+ }
2742
+ await server.start();
977
2743
  }
978
2744
  // ---------------------------------------------------------------------------
979
2745
  // Usage
980
2746
  // ---------------------------------------------------------------------------
981
2747
  function printUsage() {
2748
+ const groups = [
2749
+ ["Do things", ["save", "note", "recall", "search"]],
2750
+ ["See things", ["status", "doctor", "dashboard"]],
2751
+ ["Advanced", ["write", "read", "context", "scratchpad", "distil", "sync"]],
2752
+ ["Setup", ["setup", "install-skills", "install-hooks", "completion"]],
2753
+ ["Pro", ["pro", "learn"]],
2754
+ ];
2755
+ const knownGroupCommands = new Set(groups.flatMap(([, list]) => list));
2756
+ const otherCommands = COMMANDS.filter((command) => !knownGroupCommands.has(command));
2757
+ if (otherCommands.length > 0)
2758
+ groups.push(["Other", [...otherCommands]]);
2759
+ const allCommands = groups.flatMap(([, list]) => list);
2760
+ const width = Math.max(...allCommands.map((command) => command.length));
2761
+ const sections = groups
2762
+ .map(([title, list]) => {
2763
+ const rows = list
2764
+ .filter((command) => COMMAND_DESCRIPTIONS[command])
2765
+ .map((command) => {
2766
+ const description = COMMAND_DESCRIPTIONS[command];
2767
+ return ` ${command.padEnd(width)} ${description}`;
2768
+ })
2769
+ .join("\n");
2770
+ return `${title}:\n${rows}`;
2771
+ })
2772
+ .join("\n\n");
982
2773
  console.log(`agent-memory — persistent memory for coding agents
983
2774
 
984
2775
  Usage:
985
2776
  agent-memory <command> [options]
986
2777
 
987
- Commands:
988
- version Show binary version
989
- install-skills Install (or --uninstall) bundled skills
990
- uninstall-skills Uninstall bundled skills
991
- context Build context; optionally retrieve memories with --query
992
- write Write to memory files (default: daily; optional --source-uri)
993
- read Read memory files
994
- scratchpad Manage checklist items
995
- search Search across memory files (requires qmd)
996
- distil Generate compact MEMORY.md index from daily logs + topics
997
- sync Re-index and embed all files (requires qmd)
998
- init Initialize memory directory and qmd collection
999
- status Show configuration and status (--probe for a live embeddings check)
1000
- completion Install or print shell completion
1001
- install-hooks Install managed SessionStart hooks
1002
- uninstall-hooks Remove only managed SessionStart hooks
1003
- plugin Discover, install, update, or remove optional official plugins
2778
+ ${sections}
1004
2779
 
1005
2780
  Global flags:
1006
2781
  --dir <path> Override memory directory
1007
2782
  --json Machine-readable JSON output
1008
2783
 
1009
- Examples:
1010
- agent-memory init
1011
- agent-memory write --content "Fixed auth bug in login flow"
1012
- agent-memory write --target long_term --content "User prefers dark mode" --source-uri "session://agent/turn/12"
1013
- agent-memory write --target topic --topic "auth" --content "Rolled JWT refresh to edge"
1014
- agent-memory read --target long_term
1015
- agent-memory read --target daily --date 2026-02-15
1016
- agent-memory read --target list
1017
- agent-memory read --target topic --topic "auth"
1018
- agent-memory read --target topics
1019
- agent-memory scratchpad add --text "Review PR #42"
1020
- agent-memory scratchpad list
1021
- agent-memory scratchpad done --text "PR #42"
1022
- agent-memory search --query "database choice" --mode keyword
1023
- agent-memory distil --dry-run
1024
- agent-memory context --query "database choice"
1025
- agent-memory sync
1026
- agent-memory status --json
1027
- agent-memory completion zsh
1028
- agent-memory install-hooks --yes
1029
- agent-memory plugin status
1030
- agent-memory plugin install`);
2784
+ Get started:
2785
+ agent-memory setup — one-shot idempotent installer (recommended)
2786
+ agent-memory save "your note" quick save to today's log
2787
+ agent-memory recall "what did we decide about X?" search prior sessions
2788
+ agent-memory status — one-page health readout
2789
+
2790
+ New here? Run: ${colorize("agent-memory setup", "cyan")}`);
1031
2791
  }
1032
2792
  // ---------------------------------------------------------------------------
1033
2793
  // Main
1034
2794
  // ---------------------------------------------------------------------------
2795
+ // Pre-parse content-taking aliases (`save`, `note`) before the general flag parser
2796
+ // consumes tokens that were meant to be literal user text. Extracts only known
2797
+ // infrastructure flags — `--json`, `--dir <path>`, and `--target <val>` for `save` —
2798
+ // and treats the rest of argv as the content string.
2799
+ function extractRawContentAlias(argv) {
2800
+ if (argv.length === 0)
2801
+ return null;
2802
+ const command = argv[0];
2803
+ if (command !== "save" && command !== "note")
2804
+ return null;
2805
+ // Never treat reserved flags as content — they must fall through to the normal
2806
+ // parser so `save --help` renders help and `save --json` sets JSON mode without
2807
+ // accidentally saving "--help" or "--json" as a daily-log entry.
2808
+ if (argv.slice(1).some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-V")) {
2809
+ return null;
2810
+ }
2811
+ const flags = {};
2812
+ const contentParts = [];
2813
+ const validValueFlags = new Set(["--dir", "--target"]);
2814
+ for (let i = 1; i < argv.length; i++) {
2815
+ const arg = argv[i];
2816
+ if (arg === "--json") {
2817
+ flags.json = true;
2818
+ continue;
2819
+ }
2820
+ if (validValueFlags.has(arg) && i + 1 < argv.length) {
2821
+ flags[arg.slice(2)] = argv[i + 1];
2822
+ i++;
2823
+ continue;
2824
+ }
2825
+ contentParts.push(arg);
2826
+ }
2827
+ return { command, content: contentParts.join(" ").trim(), flags };
2828
+ }
1035
2829
  async function main() {
1036
- const { command, flags, positional } = parseArgs(process.argv.slice(2));
2830
+ const rawArgv = process.argv.slice(2);
2831
+ const rawAlias = extractRawContentAlias(rawArgv);
2832
+ const { command, flags, positional } = rawAlias
2833
+ ? { command: rawAlias.command, flags: rawAlias.flags, positional: [rawAlias.content].filter(Boolean) }
2834
+ : parseArgs(rawArgv);
1037
2835
  const json = hasFlag(flags, "json");
2836
+ // Reject unknown commands / flags with a helpful "did you mean" hint.
2837
+ // Plugin-provided commands fall through and are validated by the runtime.
2838
+ validateCommand(command);
2839
+ validateFlags(command, positional, flags);
1038
2840
  // Apply --dir override
1039
2841
  const dir = getFlag(flags, "dir");
1040
2842
  if (dir) {
1041
2843
  _setBaseDir(dir);
1042
2844
  }
2845
+ // First-run detection: nudge new users toward `init` before they run anything
2846
+ // destructive/creative. Gated on the memory *directory* (not MEMORY.md) so a
2847
+ // user who intentionally cleared their memory files doesn't get the wizard on
2848
+ // every subsequent command. TTY-only; skip introspection commands.
2849
+ if (process.stdin.isTTY &&
2850
+ process.stdout.isTTY &&
2851
+ !json &&
2852
+ command &&
2853
+ !["init", "help", "version", "doctor", "status", "completion", "hook", "serve"].includes(command) &&
2854
+ !fs.existsSync(getMemoryDir())) {
2855
+ console.log(colorize("It looks like this is your first run — no memory directory yet.", "yellow"));
2856
+ if (await promptYesNo("Run agent-memory init to set things up?", true)) {
2857
+ await cmdInit({});
2858
+ console.log("");
2859
+ console.log(colorize(`Now continuing with: agent-memory ${command}`, "dim"));
2860
+ console.log("");
2861
+ }
2862
+ }
1043
2863
  if (command === "version" || hasFlag(flags, "version")) {
1044
2864
  output(json ? { version: VERSION } : VERSION, json);
1045
2865
  return;
1046
2866
  }
1047
- if (!command || command === "help" || (hasFlag(flags, "help") && command !== "plugin")) {
2867
+ // Per-command help: `agent-memory <cmd> --help` shows only that command's usage,
2868
+ // flags, and examples. Falls back to the top-level overview when no command was
2869
+ // specified. Plugin subcommands render their own help via the plugin runtime.
2870
+ if (command === "help") {
2871
+ const target = positional[0];
2872
+ if (target) {
2873
+ console.log(renderCommandHelp(target));
2874
+ }
2875
+ else {
2876
+ printUsage();
2877
+ }
2878
+ return;
2879
+ }
2880
+ if (hasFlag(flags, "help") && command !== "plugin") {
2881
+ if (!command) {
2882
+ printUsage();
2883
+ }
2884
+ else {
2885
+ console.log(renderCommandHelp(command));
2886
+ }
2887
+ return;
2888
+ }
2889
+ if (!command) {
1048
2890
  printUsage();
1049
2891
  return;
1050
2892
  }
@@ -1052,9 +2894,28 @@ async function main() {
1052
2894
  case "context":
1053
2895
  await cmdContext(flags);
1054
2896
  break;
1055
- case "write":
1056
- await cmdWrite(flags);
2897
+ case "write": {
2898
+ // Accept content as the first positional so `write "text"` mirrors
2899
+ // `save "text"` / `note "text"`. `--content` still works for scripts
2900
+ // and existing callers.
2901
+ const positionalContent = positional[0];
2902
+ await cmdWrite(positionalContent && !getFlag(flags, "content") ? { ...flags, content: positionalContent } : flags);
2903
+ break;
2904
+ }
2905
+ case "save": {
2906
+ const text = positional[0] ?? "";
2907
+ if (!text)
2908
+ exitError('save requires content: agent-memory save "your note here"', json);
2909
+ await cmdWrite({ ...flags, content: text, target: getFlag(flags, "target") ?? "daily" });
1057
2910
  break;
2911
+ }
2912
+ case "note": {
2913
+ const text = positional[0] ?? "";
2914
+ if (!text)
2915
+ exitError('note requires text: agent-memory note "your item"', json);
2916
+ await cmdScratchpad({ ...flags, text }, ["add"]);
2917
+ break;
2918
+ }
1058
2919
  case "read":
1059
2920
  await cmdRead(flags);
1060
2921
  break;
@@ -1080,9 +2941,18 @@ async function main() {
1080
2941
  case "init":
1081
2942
  await cmdInit(flags);
1082
2943
  break;
2944
+ case "setup":
2945
+ await cmdSetup(flags);
2946
+ break;
1083
2947
  case "status":
1084
2948
  await cmdStatus(flags);
1085
2949
  break;
2950
+ case "doctor":
2951
+ await cmdDoctor(flags);
2952
+ break;
2953
+ case "tutorial":
2954
+ await cmdTutorial(flags);
2955
+ break;
1086
2956
  case "completion":
1087
2957
  cmdCompletion(flags, positional);
1088
2958
  break;
@@ -1093,43 +2963,131 @@ async function main() {
1093
2963
  cmdUninstallHooks(flags);
1094
2964
  break;
1095
2965
  case "hook": {
1096
- if (positional[0] !== "session-start")
1097
- exitError("hook requires 'session-start'", json);
2966
+ const sub = positional[0];
2967
+ if (sub !== "session-start" && sub !== "user-prompt-submit" && sub !== "stop") {
2968
+ exitError("hook requires 'session-start', 'user-prompt-submit', or 'stop'", json);
2969
+ }
1098
2970
  const agent = getFlag(flags, "agent");
1099
2971
  if (!agent)
1100
- exitError("hook session-start requires --agent", json);
1101
- await cmdContext({ "no-search": true });
2972
+ exitError(`hook ${sub} requires --agent`, json);
2973
+ if (sub === "user-prompt-submit") {
2974
+ await cmdUserPromptSubmit(flags);
2975
+ break;
2976
+ }
2977
+ if (sub === "stop") {
2978
+ await cmdStop(flags);
2979
+ break;
2980
+ }
2981
+ // In per-turn mode, SessionStart emits only the stable layer (MEMORY.md + scratchpad).
2982
+ // The dynamic layer (daily logs + search) is emitted per-turn by UserPromptSubmit.
2983
+ // In stable mode, SessionStart emits the full context (current behavior).
2984
+ const layer = readHookMode() === "per-turn" ? "stable" : undefined;
2985
+ await cmdContext(layer ? { "no-search": true, layer } : { "no-search": true });
2986
+ try {
2987
+ const cache = readUpgradeCache();
2988
+ if (cache) {
2989
+ const status = await checkForUpgrades({
2990
+ cliCurrent: VERSION,
2991
+ pluginCurrent: cache.pluginCurrent,
2992
+ cacheOnly: true,
2993
+ });
2994
+ const notice = formatUpgradeNotice(status);
2995
+ if (notice)
2996
+ console.error(notice);
2997
+ }
2998
+ if (!isCacheFresh(cache) && !process.env.AGENT_MEMORY_UPGRADE_BACKGROUND) {
2999
+ refreshUpgradeCacheBackground();
3000
+ }
3001
+ }
3002
+ catch {
3003
+ // Passive upgrade notice must never break session start.
3004
+ }
1102
3005
  try {
1103
3006
  const decision = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).runSessionStart({
1104
3007
  host: agent,
1105
3008
  cwd: process.cwd(),
1106
3009
  signal: new AbortController().signal,
1107
3010
  });
1108
- if (decision?.state === "exhausted")
1109
- console.error(`AgentMemory free session allowance resets at ${decision.resetAt}`);
3011
+ if (decision) {
3012
+ cacheProUsage({
3013
+ used: decision.used,
3014
+ limit: decision.limit,
3015
+ remaining: decision.remaining,
3016
+ resetAt: decision.resetAt,
3017
+ state: decision.state,
3018
+ capability: "session",
3019
+ });
3020
+ if (decision.state === "exhausted") {
3021
+ console.error(`AgentMemory free session allowance resets in ${formatResetTime(decision.resetAt)}. Upgrade: ${UPGRADE_URL}`);
3022
+ }
3023
+ }
1110
3024
  }
1111
- catch {
1112
- // Paid SessionStart work must never make public-core context unavailable.
3025
+ catch (error) {
3026
+ // Paid SessionStart work must never make public-core context unavailable,
3027
+ // but a completely silent failure hides broken installs from users and `doctor`.
3028
+ const message = error instanceof Error ? error.message : String(error);
3029
+ console.error(`AgentMemory Pro session hook failed: ${message}`);
1113
3030
  }
1114
3031
  break;
1115
3032
  }
1116
3033
  case "plugin":
1117
3034
  await cmdPlugin(flags, positional);
1118
3035
  break;
3036
+ case "pro":
3037
+ await cmdPro(flags, positional);
3038
+ break;
3039
+ case "upgrade":
3040
+ await cmdUpgrade(flags);
3041
+ break;
3042
+ case "serve":
3043
+ await cmdServe(flags);
3044
+ break;
1119
3045
  default: {
1120
3046
  const controller = new AbortController();
1121
3047
  const abort = () => controller.abort();
1122
3048
  process.once("SIGINT", abort);
1123
3049
  try {
1124
- const result = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).run(command, {
3050
+ const pluginCommand = command === "dashboard" ? "web" : command;
3051
+ // Dev-only: AGENT_MEMORY_FORCE_QUOTA_EXHAUSTED=recall (or "learn") injects an
3052
+ // exhaustion result so the upgrade-prompt flow can be tested locally without
3053
+ // burning through real server quota.
3054
+ const forceExhausted = process.env.AGENT_MEMORY_FORCE_QUOTA_EXHAUSTED;
3055
+ if (!json && forceExhausted && forceExhausted.split(",").includes(command)) {
3056
+ const resetAt = new Date(Date.now() + 18 * 3_600_000).toISOString();
3057
+ const forced = {
3058
+ code: "quota_exceeded",
3059
+ used: 20,
3060
+ limit: 20,
3061
+ remaining: 0,
3062
+ resetAt,
3063
+ };
3064
+ cacheProUsage({ used: 20, limit: 20, remaining: 0, resetAt, state: "exhausted", capability: command });
3065
+ printCapExhaustedBox(command, forced);
3066
+ process.exit(1);
3067
+ }
3068
+ const result = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).run(pluginCommand, {
1125
3069
  args: positional,
1126
3070
  flags,
1127
3071
  signal: controller.signal,
1128
3072
  });
1129
3073
  if (!result)
1130
3074
  exitError(`Unknown command: ${command}. Run 'agent-memory help' for usage.`, json);
1131
- if (!result.ok)
1132
- exitError(result.error?.message ?? `Plugin command ${command} failed`, json);
3075
+ if (!result.ok) {
3076
+ const capInfo = detectCapExhausted(result);
3077
+ if (capInfo && !json) {
3078
+ cacheProUsage({
3079
+ used: capInfo.used,
3080
+ limit: capInfo.limit,
3081
+ remaining: capInfo.remaining ?? 0,
3082
+ resetAt: capInfo.resetAt,
3083
+ state: "exhausted",
3084
+ capability: command,
3085
+ });
3086
+ printCapExhaustedBox(command, capInfo);
3087
+ process.exit(1);
3088
+ }
3089
+ exitError(result.error?.message ?? `Plugin command ${pluginCommand} failed`, json);
3090
+ }
1133
3091
  output(result.data ?? { ok: true }, json);
1134
3092
  }
1135
3093
  finally {