myagentmemory 0.4.17 → 0.5.2
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/README.md +68 -71
- package/dist/cli-spec.d.ts +7 -1
- package/dist/cli-spec.js +214 -12
- package/dist/cli.js +2053 -156
- package/dist/completions.js +24 -18
- package/dist/core.d.ts +42 -4
- package/dist/core.js +242 -68
- package/dist/hooks.d.ts +21 -1
- package/dist/hooks.js +382 -87
- package/dist/mcp-server.d.ts +27 -0
- package/dist/mcp-server.js +106 -0
- package/dist/plugin-bootstrap.js +4 -4
- package/dist/plugin-host.d.ts +34 -0
- package/dist/plugin-runtime.d.ts +22 -1
- package/dist/plugin-runtime.js +65 -2
- package/dist/plugin-service.d.ts +10 -4
- package/dist/plugin-service.js +52 -8
- package/dist/upgrade.d.ts +80 -0
- package/dist/upgrade.js +243 -0
- package/docs/official-plugin-bootstrap.md +3 -3
- package/package.json +24 -4
- package/scripts/install-skills.sh +1 -1
- package/skills/agent/SKILL.md +12 -2
- package/skills/claude-code/SKILL.md +17 -2
- package/skills/codex/SKILL.md +14 -2
- package/skills/cursor/SKILL.md +14 -2
- package/src/cli-spec.ts +218 -12
- package/src/completions.ts +26 -18
- package/src/core.ts +312 -123
- package/src/hooks.ts +395 -85
- package/src/plugin-bootstrap.ts +4 -4
- package/src/plugin-host.ts +34 -0
- package/src/cli.ts +0 -1332
- package/src/plugin-runtime.ts +0 -390
- package/src/plugin-service.ts +0 -627
package/dist/cli.js
CHANGED
|
@@ -23,12 +23,16 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { spawn } from "node:child_process";
|
|
25
25
|
import * as fs from "node:fs";
|
|
26
|
-
import
|
|
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";
|
|
27
29
|
import { detectCompletionShell, generateCompletion, installCompletion } from "./completions.js";
|
|
28
|
-
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";
|
|
29
|
-
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";
|
|
30
33
|
import { createDefaultPluginBootstrap, PluginBootstrapFailure, } from "./plugin-bootstrap.js";
|
|
31
34
|
import { InstalledPluginRuntimeV1 } from "./plugin-runtime.js";
|
|
35
|
+
import { checkForUpgrades, detectInstallMethod, formatUpgradeNotice, isCacheFresh, readUpgradeCache, refreshUpgradeCacheBackground, runInstaller, } from "./upgrade.js";
|
|
32
36
|
function readPackageVersion() {
|
|
33
37
|
try {
|
|
34
38
|
const packageJson = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
@@ -43,16 +47,38 @@ function parseArgs(argv) {
|
|
|
43
47
|
const flags = {};
|
|
44
48
|
const positional = [];
|
|
45
49
|
let command = "";
|
|
46
|
-
|
|
47
|
-
|
|
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];
|
|
48
56
|
if (!command && !arg.startsWith("-")) {
|
|
49
57
|
command = arg;
|
|
50
58
|
continue;
|
|
51
59
|
}
|
|
52
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
|
+
}
|
|
53
67
|
const key = arg.slice(2);
|
|
54
|
-
const next =
|
|
55
|
-
|
|
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) {
|
|
56
82
|
flags[key] = next;
|
|
57
83
|
i++;
|
|
58
84
|
}
|
|
@@ -74,6 +100,206 @@ function hasFlag(flags, key) {
|
|
|
74
100
|
return key in flags;
|
|
75
101
|
}
|
|
76
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
|
+
// ---------------------------------------------------------------------------
|
|
77
303
|
// Output helpers
|
|
78
304
|
// ---------------------------------------------------------------------------
|
|
79
305
|
function output(data, json) {
|
|
@@ -119,25 +345,68 @@ function openExternalUrl(url) {
|
|
|
119
345
|
return false;
|
|
120
346
|
}
|
|
121
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
|
+
}
|
|
122
392
|
function printProOverview(installed) {
|
|
123
393
|
console.log("");
|
|
124
394
|
console.log("Core remembers what you save. Pro learns from what you do.");
|
|
125
395
|
console.log("");
|
|
126
396
|
console.log("AgentMemory Pro:");
|
|
127
|
-
console.log(
|
|
128
|
-
console.log(" Learn from
|
|
129
|
-
console.log("
|
|
130
|
-
console.log("");
|
|
131
|
-
console.log("No account is required for the free preview. Your coding history stays on this device.");
|
|
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.");
|
|
132
400
|
console.log("");
|
|
133
401
|
if (installed) {
|
|
402
|
+
printProUsageCounters();
|
|
134
403
|
console.log("Try it:");
|
|
135
404
|
console.log(' agent-memory recall "what did we decide about authentication?"');
|
|
136
405
|
console.log(" agent-memory learn");
|
|
137
406
|
console.log(" agent-memory dashboard");
|
|
138
407
|
}
|
|
139
408
|
else {
|
|
140
|
-
console.log("
|
|
409
|
+
console.log("No account. No email. Free preview starts now:");
|
|
141
410
|
console.log(" agent-memory pro install");
|
|
142
411
|
}
|
|
143
412
|
}
|
|
@@ -213,11 +482,16 @@ async function cmdContext(flags) {
|
|
|
213
482
|
const json = hasFlag(flags, "json");
|
|
214
483
|
const noSearch = hasFlag(flags, "no-search");
|
|
215
484
|
const query = getFlag(flags, "query") ?? "";
|
|
485
|
+
const layer = getFlag(flags, "layer");
|
|
216
486
|
ensureDirs();
|
|
217
487
|
if (!noSearch && query)
|
|
218
488
|
await ensureQmdAvailableForSync();
|
|
219
489
|
const searchResults = noSearch ? "" : await searchRelevantMemories(query);
|
|
220
|
-
const coreContext =
|
|
490
|
+
const coreContext = layer === "stable"
|
|
491
|
+
? buildStableContext()
|
|
492
|
+
: layer === "dynamic"
|
|
493
|
+
? buildDynamicContext(searchResults, query)
|
|
494
|
+
: buildMemoryContext(searchResults);
|
|
221
495
|
let pluginSections = [];
|
|
222
496
|
try {
|
|
223
497
|
pluginSections = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).provideContext({
|
|
@@ -241,6 +515,196 @@ async function cmdContext(flags) {
|
|
|
241
515
|
}
|
|
242
516
|
}
|
|
243
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". Deliberately short —
|
|
621
|
+
// most real sessions are well under a dozen turns, so a wider interval meant
|
|
622
|
+
// the nudge rarely fired in practice (see stop-hook.json in the wild: sessions
|
|
623
|
+
// topping out around 7 turns, zero nags ever recorded).
|
|
624
|
+
const STOP_NAG_INTERVAL = 6;
|
|
625
|
+
// Bound state/stop-hook.json so it can't grow unboundedly across many sessions.
|
|
626
|
+
const STOP_HOOK_MAX_SESSIONS = 50;
|
|
627
|
+
function stopHookStatePath() {
|
|
628
|
+
return `${getMemoryDir()}/state/stop-hook.json`;
|
|
629
|
+
}
|
|
630
|
+
function readStopHookState() {
|
|
631
|
+
try {
|
|
632
|
+
const raw = fs.readFileSync(stopHookStatePath(), "utf-8");
|
|
633
|
+
const parsed = JSON.parse(raw);
|
|
634
|
+
return { sessions: parsed.sessions && typeof parsed.sessions === "object" ? parsed.sessions : {} };
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
return { sessions: {} };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function writeStopHookState(state) {
|
|
641
|
+
const entries = Object.entries(state.sessions).sort((a, b) => b[1].lastSeenAt - a[1].lastSeenAt);
|
|
642
|
+
const pruned = Object.fromEntries(entries.slice(0, STOP_HOOK_MAX_SESSIONS));
|
|
643
|
+
const stateDir = `${getMemoryDir()}/state`;
|
|
644
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
645
|
+
fs.writeFileSync(stopHookStatePath(), `${JSON.stringify({ sessions: pruned }, null, 2)}\n`, { mode: 0o600 });
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Bump the Stop-event counter for `sessionId` and report whether the
|
|
649
|
+
* periodic memory-write nudge should fire this time. Never throws — a
|
|
650
|
+
* corrupt or unwritable state file just means the nudge falls back to
|
|
651
|
+
* "never fires" rather than breaking the Stop hook.
|
|
652
|
+
*/
|
|
653
|
+
function shouldNagOnStop(sessionId, now) {
|
|
654
|
+
try {
|
|
655
|
+
const state = readStopHookState();
|
|
656
|
+
const existing = state.sessions[sessionId] ?? { count: 0, lastNagCount: 0, lastSeenAt: now };
|
|
657
|
+
const count = existing.count + 1;
|
|
658
|
+
const shouldNag = count - existing.lastNagCount >= STOP_NAG_INTERVAL;
|
|
659
|
+
state.sessions[sessionId] = {
|
|
660
|
+
count,
|
|
661
|
+
lastNagCount: shouldNag ? count : existing.lastNagCount,
|
|
662
|
+
lastSeenAt: now,
|
|
663
|
+
};
|
|
664
|
+
writeStopHookState(state);
|
|
665
|
+
return shouldNag;
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const STOP_NAG_REASON = "Before stopping: if this session produced a durable fact, bug fix, or decision worth remembering, " +
|
|
672
|
+
'capture it now — `agent-memory write --content "..."` for a daily note, or `--target long_term` for a ' +
|
|
673
|
+
"durable fact — and update the scratchpad with any open follow-ups. If there's nothing worth recording, " +
|
|
674
|
+
"ignore this and stop normally.";
|
|
675
|
+
/**
|
|
676
|
+
* Stop hook handler — fires at the end of every assistant turn (not once per
|
|
677
|
+
* session). Blocks at most once every STOP_NAG_INTERVAL turns per session_id
|
|
678
|
+
* to nudge a memory-write check without being disruptive. Always allows the
|
|
679
|
+
* stop (empty stdout) on missing session_id, `stop_hook_active` (Claude Code's
|
|
680
|
+
* own re-entrancy signal — never block twice in a row), or any internal error.
|
|
681
|
+
*/
|
|
682
|
+
async function cmdStop(_flags) {
|
|
683
|
+
const TIMEOUT_MS = 3_000;
|
|
684
|
+
const controller = new AbortController();
|
|
685
|
+
let timer;
|
|
686
|
+
const timeout = new Promise((resolve) => {
|
|
687
|
+
timer = setTimeout(() => {
|
|
688
|
+
controller.abort();
|
|
689
|
+
resolve();
|
|
690
|
+
}, TIMEOUT_MS);
|
|
691
|
+
});
|
|
692
|
+
const work = (async () => {
|
|
693
|
+
const payload = await readStdinJson();
|
|
694
|
+
const sessionId = typeof payload?.session_id === "string" ? payload.session_id : "";
|
|
695
|
+
if (!sessionId || payload?.stop_hook_active === true)
|
|
696
|
+
return;
|
|
697
|
+
if (shouldNagOnStop(sessionId, Date.now())) {
|
|
698
|
+
process.stdout.write(JSON.stringify({ decision: "block", reason: STOP_NAG_REASON }));
|
|
699
|
+
}
|
|
700
|
+
})().catch(() => {
|
|
701
|
+
// Any failure in the Stop hook must be swallowed — never trap the user
|
|
702
|
+
// in a stuck session over a broken memory-write nudge.
|
|
703
|
+
});
|
|
704
|
+
await Promise.race([work, timeout]);
|
|
705
|
+
if (timer)
|
|
706
|
+
clearTimeout(timer);
|
|
707
|
+
}
|
|
244
708
|
async function cmdWrite(flags) {
|
|
245
709
|
const json = hasFlag(flags, "json");
|
|
246
710
|
const target = getFlag(flags, "target") ?? "daily";
|
|
@@ -459,15 +923,42 @@ async function cmdSearch(flags) {
|
|
|
459
923
|
const collName = getCollectionName();
|
|
460
924
|
const hasCollection = await checkCollection(collName);
|
|
461
925
|
if (!hasCollection) {
|
|
462
|
-
exitError(`qmd collection '${collName}' not found. Run: agent-memory
|
|
926
|
+
exitError(`qmd collection '${collName}' not found. Run: agent-memory setup`, json);
|
|
463
927
|
}
|
|
464
928
|
try {
|
|
465
929
|
const { results, stderr } = await runQmdSearch(mode, query, limit);
|
|
930
|
+
let recallHits = [];
|
|
931
|
+
if (results.length === 0) {
|
|
932
|
+
try {
|
|
933
|
+
const runtime = new InstalledPluginRuntimeV1({ coreVersion: VERSION });
|
|
934
|
+
const recallResult = await runtime.run("recall", {
|
|
935
|
+
args: [query],
|
|
936
|
+
flags: { limit: String(limit) },
|
|
937
|
+
signal: new AbortController().signal,
|
|
938
|
+
});
|
|
939
|
+
if (recallResult?.ok && Array.isArray(recallResult.data) && recallResult.data.length > 0) {
|
|
940
|
+
recallHits = recallResult.data;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
catch {
|
|
944
|
+
// Pro not installed or recall unavailable — fall through to normal empty result
|
|
945
|
+
}
|
|
946
|
+
}
|
|
466
947
|
if (json) {
|
|
467
|
-
|
|
948
|
+
if (results.length === 0 && recallHits.length > 0) {
|
|
949
|
+
output({ mode, query, source: "recall", count: recallHits.length, results: recallHits }, true);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
output({ mode, query, source: "qmd", count: results.length, results }, true);
|
|
953
|
+
}
|
|
468
954
|
return;
|
|
469
955
|
}
|
|
470
956
|
if (results.length === 0) {
|
|
957
|
+
if (recallHits.length > 0) {
|
|
958
|
+
console.log(`No hits in local memory files for "${query}" — falling back to prior sessions via Pro recall (${recallHits.length} hit(s)):\n`);
|
|
959
|
+
console.log(JSON.stringify(recallHits, null, 2));
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
471
962
|
const needsEmbed = /need embeddings/i.test(stderr ?? "");
|
|
472
963
|
if (needsEmbed && (mode === "semantic" || mode === "deep")) {
|
|
473
964
|
console.log(`No results found. qmd reports missing embeddings — run: qmd embed`);
|
|
@@ -556,6 +1047,11 @@ function cmdInstallSkills(flags) {
|
|
|
556
1047
|
console.log(`Skipped ${item.label} (${item.reason})`);
|
|
557
1048
|
}
|
|
558
1049
|
}
|
|
1050
|
+
if (report.installed.length > 0 && process.stdout.isTTY) {
|
|
1051
|
+
const first = report.installed[0].label;
|
|
1052
|
+
console.log("");
|
|
1053
|
+
console.log(`Next: open ${first} and ask ${colorize('"what do you remember about me?"', "cyan")} to verify.`);
|
|
1054
|
+
}
|
|
559
1055
|
}
|
|
560
1056
|
async function promptYesNo(question, defaultYes) {
|
|
561
1057
|
const readline = await import("node:readline/promises");
|
|
@@ -574,26 +1070,83 @@ async function cmdInstallHooks(flags) {
|
|
|
574
1070
|
const json = hasFlag(flags, "json");
|
|
575
1071
|
const requested = getFlag(flags, "only");
|
|
576
1072
|
const requestedKeys = requested ? new Set(requested.split(",").map((value) => value.trim())) : null;
|
|
1073
|
+
const modeFlag = getFlag(flags, "mode");
|
|
1074
|
+
if (modeFlag !== undefined && modeFlag !== "stable" && modeFlag !== "per-turn") {
|
|
1075
|
+
exitError(`--mode must be 'stable' or 'per-turn' (got ${modeFlag})`, json);
|
|
1076
|
+
}
|
|
1077
|
+
const mode = modeFlag ?? "per-turn";
|
|
577
1078
|
const { homeDir, targets } = detectHookAgents();
|
|
578
1079
|
if (!homeDir)
|
|
579
1080
|
exitError("Home directory not found.", json);
|
|
580
1081
|
const eligible = targets.filter((target) => target.supported && target.detected && (!requestedKeys || requestedKeys.has(target.key)));
|
|
1082
|
+
if (!eligible.length) {
|
|
1083
|
+
if (json)
|
|
1084
|
+
return output({ ok: true, homeDir, results: [] }, true);
|
|
1085
|
+
return output("No eligible agents. Nothing to install.", false);
|
|
1086
|
+
}
|
|
1087
|
+
// Consider an agent "already installed" only when the wiring matches the requested mode.
|
|
1088
|
+
// per-turn requires BOTH SessionStart and UserPromptSubmit; stable requires SessionStart AND
|
|
1089
|
+
// no UserPromptSubmit (so a downgrade correctly removes the per-turn hook).
|
|
1090
|
+
const isFullyInstalled = (target) => {
|
|
1091
|
+
if (!homeDir)
|
|
1092
|
+
return false;
|
|
1093
|
+
const session = isHookInstalled(homeDir, target.key);
|
|
1094
|
+
if (!session)
|
|
1095
|
+
return false;
|
|
1096
|
+
if (target.key !== "claude" && target.key !== "codex")
|
|
1097
|
+
return true; // cursor/opencode: static only
|
|
1098
|
+
const prompt = isUserPromptSubmitInstalled(homeDir, target.key);
|
|
1099
|
+
if (mode === "per-turn" ? !prompt : prompt)
|
|
1100
|
+
return false;
|
|
1101
|
+
// Stop (write-side nudge) is Claude Code only and mode-independent.
|
|
1102
|
+
if (target.key === "claude") {
|
|
1103
|
+
if (!isStopHookInstalled(homeDir, target.key))
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
return true;
|
|
1107
|
+
};
|
|
1108
|
+
const alreadyInstalled = eligible.filter(isFullyInstalled);
|
|
1109
|
+
const pending = eligible.filter((target) => !alreadyInstalled.includes(target));
|
|
1110
|
+
if (!json && alreadyInstalled.length) {
|
|
1111
|
+
const labels = alreadyInstalled.map((target) => target.label).join(", ");
|
|
1112
|
+
console.log(`Automatic context already active for: ${labels}.`);
|
|
1113
|
+
}
|
|
1114
|
+
if (!pending.length) {
|
|
1115
|
+
if (json) {
|
|
1116
|
+
return output({
|
|
1117
|
+
ok: true,
|
|
1118
|
+
homeDir,
|
|
1119
|
+
results: alreadyInstalled.map((target) => ({
|
|
1120
|
+
key: target.key,
|
|
1121
|
+
label: target.label,
|
|
1122
|
+
installed: false,
|
|
1123
|
+
reason: "already installed",
|
|
1124
|
+
mode,
|
|
1125
|
+
})),
|
|
1126
|
+
}, true);
|
|
1127
|
+
}
|
|
1128
|
+
return output("Nothing to install.", false);
|
|
1129
|
+
}
|
|
581
1130
|
const selected = new Set();
|
|
582
1131
|
const applyAll = hasFlag(flags, "yes") || hasFlag(flags, "all") || !process.stdin.isTTY;
|
|
583
|
-
|
|
584
|
-
|
|
1132
|
+
const hookLabel = mode === "per-turn" ? "SessionStart + UserPromptSubmit hooks" : "SessionStart hook";
|
|
1133
|
+
for (const target of pending) {
|
|
1134
|
+
if (applyAll || (await promptYesNo(`Install ${hookLabel} for ${target.label}?`, true)))
|
|
585
1135
|
selected.add(target.key);
|
|
586
1136
|
}
|
|
587
|
-
|
|
1137
|
+
if (!selected.size) {
|
|
1138
|
+
if (json)
|
|
1139
|
+
return output({ ok: true, homeDir, results: [] }, true);
|
|
1140
|
+
return output("Nothing selected. Skipped.", false);
|
|
1141
|
+
}
|
|
1142
|
+
const report = installHooks(selected, mode);
|
|
588
1143
|
if (!report.ok)
|
|
589
1144
|
exitError(report.error ?? "install failed", json);
|
|
590
1145
|
if (json)
|
|
591
1146
|
return output(report, true);
|
|
592
|
-
if (!report.results.length)
|
|
593
|
-
return output("No eligible agents. Nothing to install.", false);
|
|
594
1147
|
for (const result of report.results) {
|
|
595
1148
|
console.log(result.installed
|
|
596
|
-
? `Installed ${result.label} hook: ${result.path}`
|
|
1149
|
+
? `Installed ${result.label} hook (${result.mode ?? mode}): ${result.path}`
|
|
597
1150
|
: `Skipped ${result.label} (${result.reason ?? "unknown"})`);
|
|
598
1151
|
}
|
|
599
1152
|
}
|
|
@@ -645,7 +1198,7 @@ async function cmdSync(flags) {
|
|
|
645
1198
|
const collName = getCollectionName();
|
|
646
1199
|
const hasCollection = await checkCollection(collName);
|
|
647
1200
|
if (!hasCollection) {
|
|
648
|
-
exitError(`qmd collection '${collName}' not found. Run: agent-memory
|
|
1201
|
+
exitError(`qmd collection '${collName}' not found. Run: agent-memory setup`, json);
|
|
649
1202
|
}
|
|
650
1203
|
const result = await runQmdSync();
|
|
651
1204
|
if (json) {
|
|
@@ -671,6 +1224,9 @@ async function cmdSync(flags) {
|
|
|
671
1224
|
}
|
|
672
1225
|
async function cmdInit(flags) {
|
|
673
1226
|
const json = hasFlag(flags, "json");
|
|
1227
|
+
const nonInteractive = json || hasFlag(flags, "yes") || !process.stdin.isTTY || !process.stdout.isTTY;
|
|
1228
|
+
const skipSkills = hasFlag(flags, "skip-skills");
|
|
1229
|
+
const skipHooks = hasFlag(flags, "skip-hooks");
|
|
674
1230
|
ensureDirs();
|
|
675
1231
|
const dir = getMemoryDir();
|
|
676
1232
|
const qmdFound = await detectQmd();
|
|
@@ -683,7 +1239,6 @@ async function cmdInit(flags) {
|
|
|
683
1239
|
if (!hasCollection) {
|
|
684
1240
|
collectionCreated = await setupQmdCollection();
|
|
685
1241
|
}
|
|
686
|
-
// Run initial index update + start background embed
|
|
687
1242
|
await ensureQmdAvailableForUpdate();
|
|
688
1243
|
await runQmdUpdateNow();
|
|
689
1244
|
indexUpdated = true;
|
|
@@ -691,117 +1246,511 @@ async function cmdInit(flags) {
|
|
|
691
1246
|
embedStarted = child !== null;
|
|
692
1247
|
}
|
|
693
1248
|
if (json) {
|
|
694
|
-
output({
|
|
695
|
-
|
|
696
|
-
directory: dir,
|
|
697
|
-
qmd: qmdFound,
|
|
698
|
-
collectionCreated,
|
|
699
|
-
indexUpdated,
|
|
700
|
-
embedStarted,
|
|
701
|
-
}, true);
|
|
1249
|
+
output({ ok: true, directory: dir, qmd: qmdFound, collectionCreated, indexUpdated, embedStarted }, true);
|
|
1250
|
+
return;
|
|
702
1251
|
}
|
|
703
|
-
|
|
1252
|
+
// ---------------------------------------------------------------------
|
|
1253
|
+
// Non-interactive path (backward compatible with existing `--yes` / CI use)
|
|
1254
|
+
// ---------------------------------------------------------------------
|
|
1255
|
+
if (nonInteractive) {
|
|
704
1256
|
console.log(`Memory directory: ${dir}`);
|
|
705
|
-
console.log(` MEMORY.md, SCRATCHPAD.md, daily/, topics/
|
|
1257
|
+
console.log(` MEMORY.md, SCRATCHPAD.md, daily/, topics/ ready.`);
|
|
706
1258
|
if (qmdFound) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
else {
|
|
711
|
-
console.log(` qmd collection '${getCollectionName()}' already exists.`);
|
|
712
|
-
}
|
|
713
|
-
if (indexUpdated) {
|
|
714
|
-
console.log(` Index updated.`);
|
|
715
|
-
}
|
|
716
|
-
if (embedStarted) {
|
|
717
|
-
console.log(` Embedding started in background.`);
|
|
718
|
-
}
|
|
1259
|
+
console.log(` Search index ready.`);
|
|
1260
|
+
if (embedStarted)
|
|
1261
|
+
console.log(` Semantic search is preparing in the background.`);
|
|
719
1262
|
}
|
|
720
1263
|
else {
|
|
721
|
-
console.log(`
|
|
722
|
-
console.log(`
|
|
1264
|
+
console.log(` Search index unavailable — keyword search remains available.`);
|
|
1265
|
+
console.log(` Optional: install qmd with bun install -g https://github.com/tobi/qmd`);
|
|
723
1266
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
// ---------------------------------------------------------------------
|
|
1270
|
+
// Interactive wizard
|
|
1271
|
+
// ---------------------------------------------------------------------
|
|
1272
|
+
console.log("");
|
|
1273
|
+
console.log(colorize("Welcome to AgentMemory.", "bold"));
|
|
1274
|
+
console.log(colorize("Persistent memory for Claude Code, Codex, Cursor, and other coding agents.", "dim"));
|
|
1275
|
+
console.log("");
|
|
1276
|
+
console.log(` ${MARK_OK} Memory directory: ${dir}`);
|
|
1277
|
+
if (qmdFound) {
|
|
1278
|
+
console.log(` ${MARK_OK} Search index: ${collectionCreated ? "created" : "ready"}`);
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
console.log(` ${MARK_WARN} qmd not installed — search will be limited.`);
|
|
1282
|
+
console.log(` ${colorize("Install: bun install -g https://github.com/tobi/qmd", "cyan")}`);
|
|
1283
|
+
}
|
|
1284
|
+
console.log("");
|
|
1285
|
+
// Detect which agent hosts are present
|
|
1286
|
+
const { targets } = detectHookAgents();
|
|
1287
|
+
const detected = targets.filter((target) => target.detected && target.supported);
|
|
1288
|
+
if (detected.length === 0) {
|
|
1289
|
+
console.log(colorize(" No supported agents detected on this machine.", "yellow"));
|
|
1290
|
+
console.log(colorize(" Install Claude Code, Codex, or Cursor, then re-run.", "dim"));
|
|
1291
|
+
}
|
|
1292
|
+
else {
|
|
1293
|
+
console.log(colorize("Detected agents:", "bold"));
|
|
1294
|
+
for (const target of detected)
|
|
1295
|
+
console.log(` ${MARK_OK} ${target.label}`);
|
|
1296
|
+
console.log("");
|
|
1297
|
+
}
|
|
1298
|
+
// Step 1: install skills
|
|
1299
|
+
if (!skipSkills && detected.length > 0) {
|
|
1300
|
+
if (await promptYesNo("Install the AgentMemory skill so your agents can use memory?", true)) {
|
|
1301
|
+
cmdInstallSkills({});
|
|
736
1302
|
}
|
|
737
1303
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1304
|
+
console.log("");
|
|
1305
|
+
// Step 2: install SessionStart hooks (delegates to existing interactive path)
|
|
1306
|
+
if (!skipHooks && detected.length > 0) {
|
|
1307
|
+
if (await promptYesNo("Install SessionStart hooks so context loads automatically?", true)) {
|
|
1308
|
+
await cmdInstallHooks({});
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
console.log("");
|
|
1312
|
+
// Step 3: seed MEMORY.md so the skill's cold start isn't empty
|
|
743
1313
|
const memFile = getMemoryFile();
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1314
|
+
const existing = readFileSafe(memFile) ?? "";
|
|
1315
|
+
if (existing.trim().length === 0) {
|
|
1316
|
+
const platform = process.platform === "darwin" ? "macOS" : process.platform;
|
|
1317
|
+
const seed = `<!-- ${nowTimestamp()} [init] -->\nAgentMemory initialized on ${platform} · ${todayStr()}. First session: check the scratchpad and daily log for context.\n`;
|
|
1318
|
+
fs.writeFileSync(memFile, seed);
|
|
1319
|
+
console.log(` ${MARK_OK} Seeded MEMORY.md with a first entry so your agent has something to read.`);
|
|
1320
|
+
}
|
|
1321
|
+
// Step 4: offer Pro preview if not installed
|
|
750
1322
|
try {
|
|
751
|
-
|
|
1323
|
+
const plugin = await createDefaultPluginBootstrap(VERSION).list();
|
|
1324
|
+
if (plugin.result === "not_installed") {
|
|
1325
|
+
printProPitch("first-run");
|
|
1326
|
+
if (await promptYesNo("Preview what Pro would find in your existing session history?", true)) {
|
|
1327
|
+
await cmdProPreview({});
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
752
1330
|
}
|
|
753
1331
|
catch {
|
|
754
|
-
//
|
|
1332
|
+
// Commercial discovery must never make core initialization fail.
|
|
755
1333
|
}
|
|
756
|
-
|
|
1334
|
+
// Cheat sheet
|
|
1335
|
+
console.log("");
|
|
1336
|
+
console.log(colorize("You're set. Try these:", "bold"));
|
|
1337
|
+
console.log(' agent-memory save "made progress on <thing>" — quick note in today\'s log');
|
|
1338
|
+
console.log(' agent-memory note "follow up on <thing>" — persistent todo item');
|
|
1339
|
+
console.log(' agent-memory recall "what did we decide about X?" — search past sessions (Pro)');
|
|
1340
|
+
console.log(" agent-memory doctor — health check");
|
|
1341
|
+
console.log("");
|
|
1342
|
+
console.log(colorize(`Now open one of ${detected.map((target) => target.label).join(", ") || "your agents"} and ask: "what do you remember about me?"`, "cyan"));
|
|
1343
|
+
}
|
|
1344
|
+
function readProUsageCache() {
|
|
757
1345
|
try {
|
|
758
|
-
|
|
1346
|
+
return JSON.parse(fs.readFileSync(`${getMemoryDir()}/state/pro-usage.json`, "utf-8"));
|
|
759
1347
|
}
|
|
760
1348
|
catch {
|
|
761
|
-
|
|
1349
|
+
return {};
|
|
762
1350
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1351
|
+
}
|
|
1352
|
+
async function printSetupUsageStats(pluginInstalled) {
|
|
1353
|
+
if (!pluginInstalled)
|
|
1354
|
+
return;
|
|
1355
|
+
try {
|
|
1356
|
+
const usage = readProUsageCache();
|
|
1357
|
+
const recall = usage.recall;
|
|
1358
|
+
const learn = usage.learn;
|
|
1359
|
+
const recallStr = recall?.used !== undefined && recall?.limit !== undefined
|
|
1360
|
+
? `${recall.used}/${recall.limit} recalls today`
|
|
1361
|
+
: `0/20 recalls today`;
|
|
1362
|
+
const learnStr = learn?.used !== undefined && learn?.limit !== undefined
|
|
1363
|
+
? `${learn.used}/${learn.limit} learnings today`
|
|
1364
|
+
: `0/5 learnings today`;
|
|
1365
|
+
console.log(colorize(` Plan: Free · ${recallStr} · ${learnStr}`, "dim"));
|
|
1366
|
+
let sessionStats = null;
|
|
1367
|
+
try {
|
|
1368
|
+
const result = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).run("index", {
|
|
1369
|
+
args: [],
|
|
1370
|
+
flags: {},
|
|
1371
|
+
signal: new AbortController().signal,
|
|
1372
|
+
});
|
|
1373
|
+
if (result?.ok && result.data && typeof result.data === "object") {
|
|
1374
|
+
const stats = result.data.stats;
|
|
1375
|
+
if (stats?.discovered) {
|
|
1376
|
+
sessionStats = {
|
|
1377
|
+
claude: stats.discovered.claude ?? 0,
|
|
1378
|
+
codex: stats.discovered.codex ?? 0,
|
|
1379
|
+
pi: stats.discovered.pi ?? 0,
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
777
1382
|
}
|
|
778
1383
|
}
|
|
1384
|
+
catch {
|
|
1385
|
+
// best-effort
|
|
1386
|
+
}
|
|
1387
|
+
if (sessionStats !== null) {
|
|
1388
|
+
const total = sessionStats.claude + sessionStats.codex + sessionStats.pi;
|
|
1389
|
+
if (total > 0) {
|
|
1390
|
+
const parts = [
|
|
1391
|
+
sessionStats.claude > 0 ? `Claude Code ${sessionStats.claude.toLocaleString("en-US")}` : null,
|
|
1392
|
+
sessionStats.codex > 0 ? `Codex ${sessionStats.codex.toLocaleString("en-US")}` : null,
|
|
1393
|
+
sessionStats.pi > 0 ? `Pi ${sessionStats.pi.toLocaleString("en-US")}` : null,
|
|
1394
|
+
]
|
|
1395
|
+
.filter(Boolean)
|
|
1396
|
+
.join(" · ");
|
|
1397
|
+
console.log(colorize(` Sessions indexed: ${total.toLocaleString("en-US")} (${parts})`, "dim"));
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
console.log("");
|
|
779
1401
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1402
|
+
catch {
|
|
1403
|
+
// stats are best-effort; never block setup completion
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* One-shot idempotent installer. Runs init + install-skills + install-hooks +
|
|
1408
|
+
* plugin install (if a bundle is discoverable) and then prints a one-page status
|
|
1409
|
+
* summary. Each step is a no-op when the target is already good, so `setup` is
|
|
1410
|
+
* safe to re-run after upgrades.
|
|
1411
|
+
*
|
|
1412
|
+
* The point of `setup` is that a first-time user only has to remember ONE
|
|
1413
|
+
* command. `init`, `install-skills`, `install-hooks`, `plugin install` still
|
|
1414
|
+
* exist for scripts and for finer control, but no one needs them for the happy
|
|
1415
|
+
* path.
|
|
1416
|
+
*
|
|
1417
|
+
* Interactivity model: on an interactive TTY (and when not `--json`) setup
|
|
1418
|
+
* will prompt exactly once — to install AgentMemory Pro — because the benefits
|
|
1419
|
+
* (cross-session recall, learn-from-corrections) are invisible without it.
|
|
1420
|
+
* With `--yes` we auto-install Pro; with `--json`, `--skip-plugin`, or a
|
|
1421
|
+
* non-TTY stdin/stdout we fall back to the previous passive hint so scripted
|
|
1422
|
+
* setups keep working unchanged.
|
|
1423
|
+
*/
|
|
1424
|
+
async function cmdSetup(flags) {
|
|
1425
|
+
const json = hasFlag(flags, "json");
|
|
1426
|
+
const skipSkills = hasFlag(flags, "skip-skills");
|
|
1427
|
+
const skipHooks = hasFlag(flags, "skip-hooks");
|
|
1428
|
+
const skipPlugin = hasFlag(flags, "skip-plugin");
|
|
1429
|
+
const skipMcp = hasFlag(flags, "skip-mcp");
|
|
1430
|
+
const interactive = !json && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1431
|
+
// setup is non-interactive by design. Sub-steps that print progress noise get their
|
|
1432
|
+
// stdout captured when we're in json mode, so the whole command emits ONE envelope.
|
|
1433
|
+
const subFlags = { yes: true };
|
|
1434
|
+
const steps = [];
|
|
1435
|
+
const runQuiet = async (fn) => {
|
|
1436
|
+
if (!json) {
|
|
1437
|
+
await fn();
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
const originalLog = console.log;
|
|
1441
|
+
const originalInfo = console.info;
|
|
1442
|
+
console.log = () => { };
|
|
1443
|
+
console.info = () => { };
|
|
1444
|
+
try {
|
|
1445
|
+
await fn();
|
|
1446
|
+
}
|
|
1447
|
+
finally {
|
|
1448
|
+
console.log = originalLog;
|
|
1449
|
+
console.info = originalInfo;
|
|
1450
|
+
}
|
|
785
1451
|
};
|
|
1452
|
+
// Step 1: memory dir + qmd
|
|
786
1453
|
try {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
installed: Boolean(plugin.bundle),
|
|
790
|
-
result: plugin.result,
|
|
791
|
-
entitlement: plugin.entitlement.state,
|
|
792
|
-
};
|
|
1454
|
+
await runQuiet(() => cmdInit({ ...subFlags, "skip-skills": true, "skip-hooks": true }));
|
|
1455
|
+
steps.push({ name: "memory", ok: true, detail: getMemoryDir() });
|
|
793
1456
|
}
|
|
794
|
-
catch {
|
|
795
|
-
|
|
1457
|
+
catch (error) {
|
|
1458
|
+
steps.push({ name: "memory", ok: false, detail: error.message });
|
|
796
1459
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
1460
|
+
// Step 2: skills for detected agents
|
|
1461
|
+
if (!skipSkills) {
|
|
1462
|
+
try {
|
|
1463
|
+
await runQuiet(() => cmdInstallSkills(subFlags));
|
|
1464
|
+
steps.push({ name: "skills", ok: true });
|
|
1465
|
+
}
|
|
1466
|
+
catch (error) {
|
|
1467
|
+
steps.push({ name: "skills", ok: false, detail: error.message });
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
else {
|
|
1471
|
+
steps.push({ name: "skills", ok: true, detail: "skipped" });
|
|
1472
|
+
}
|
|
1473
|
+
// Step 3: hooks (uses the improved preflight — silent when all already installed)
|
|
1474
|
+
if (!skipHooks) {
|
|
1475
|
+
try {
|
|
1476
|
+
await runQuiet(() => cmdInstallHooks(subFlags));
|
|
1477
|
+
steps.push({ name: "hooks", ok: true });
|
|
1478
|
+
}
|
|
1479
|
+
catch (error) {
|
|
1480
|
+
steps.push({ name: "hooks", ok: false, detail: error.message });
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
else {
|
|
1484
|
+
steps.push({ name: "hooks", ok: true, detail: "skipped" });
|
|
1485
|
+
}
|
|
1486
|
+
// Step 4: install the local session-intelligence plugin by default. The free
|
|
1487
|
+
// allowance is part of the product experience, not an opt-in gate: users
|
|
1488
|
+
// should feel recall and learning before they ever see an upgrade prompt.
|
|
1489
|
+
let pluginJustInstalled = false;
|
|
1490
|
+
if (!skipPlugin) {
|
|
1491
|
+
const receiptPath = path.join(process.env.HOME ?? "", ".agent-memory/system/plugins/receipts/agentmemory.pro.json");
|
|
1492
|
+
const readReceiptVersion = () => {
|
|
1493
|
+
try {
|
|
1494
|
+
const receipt = JSON.parse(fs.readFileSync(receiptPath, "utf-8"));
|
|
1495
|
+
return receipt.version;
|
|
1496
|
+
}
|
|
1497
|
+
catch {
|
|
1498
|
+
return undefined;
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
if (fs.existsSync(receiptPath)) {
|
|
1502
|
+
const version = readReceiptVersion();
|
|
1503
|
+
steps.push({
|
|
1504
|
+
name: "plugin",
|
|
1505
|
+
ok: true,
|
|
1506
|
+
detail: version ? `${version} installed` : "installed",
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
else {
|
|
1510
|
+
// The free allowance is enabled automatically. `--skip-plugin` is the
|
|
1511
|
+
// explicit escape hatch for users who want core-only setup.
|
|
1512
|
+
const manager = createDefaultPluginBootstrap(VERSION);
|
|
1513
|
+
// Run install, capture the structured result (don't drop it), and
|
|
1514
|
+
// auto-recover from `version_conflict` — that just means a stale
|
|
1515
|
+
// bundle from an earlier dev iteration is sitting in
|
|
1516
|
+
// ~/.agent-memory/system/plugins/bundles/... with a different SHA.
|
|
1517
|
+
// A manual `plugin uninstall && plugin install` fixes it, so we do
|
|
1518
|
+
// the same automatically once during setup.
|
|
1519
|
+
const runInstall = async () => {
|
|
1520
|
+
let outcome;
|
|
1521
|
+
await runQuiet(async () => {
|
|
1522
|
+
outcome = await manager.install({ channel: "stable", allowAuthentication: interactive });
|
|
1523
|
+
});
|
|
1524
|
+
return outcome;
|
|
1525
|
+
};
|
|
1526
|
+
try {
|
|
1527
|
+
let installResult = await runInstall();
|
|
1528
|
+
if (!installResult.ok && installResult.error?.code === "version_conflict") {
|
|
1529
|
+
// `manager.uninstall()` only wipes bundles referenced by a
|
|
1530
|
+
// receipt. On version_conflict there is no receipt (that's
|
|
1531
|
+
// the whole point — an orphan bundle from an earlier failed
|
|
1532
|
+
// install is on disk with a different SHA). Try the API path
|
|
1533
|
+
// first, then fall back to removing the specific version
|
|
1534
|
+
// directory under ~/.agent-memory/system/plugins/bundles/.
|
|
1535
|
+
await runQuiet(async () => {
|
|
1536
|
+
await manager.uninstall();
|
|
1537
|
+
});
|
|
1538
|
+
try {
|
|
1539
|
+
const bundlesRoot = path.join(process.env.HOME ?? "", ".agent-memory/system/plugins/bundles/agentmemory.pro");
|
|
1540
|
+
if (fs.existsSync(bundlesRoot)) {
|
|
1541
|
+
for (const entry of fs.readdirSync(bundlesRoot)) {
|
|
1542
|
+
// Only touch semver-looking dirs; keep sibling
|
|
1543
|
+
// artefacts like `*.backup` untouched.
|
|
1544
|
+
if (!/^\d+\.\d+\.\d+/.test(entry))
|
|
1545
|
+
continue;
|
|
1546
|
+
const target = path.join(bundlesRoot, entry);
|
|
1547
|
+
try {
|
|
1548
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
1549
|
+
}
|
|
1550
|
+
catch {
|
|
1551
|
+
// Best-effort; the retry below will surface a real error if this mattered.
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
catch {
|
|
1557
|
+
// Non-fatal — we retry the install regardless.
|
|
1558
|
+
}
|
|
1559
|
+
installResult = await runInstall();
|
|
1560
|
+
}
|
|
1561
|
+
if (installResult.ok &&
|
|
1562
|
+
(installResult.result === "installed" ||
|
|
1563
|
+
installResult.result === "upgraded" ||
|
|
1564
|
+
installResult.result === "current")) {
|
|
1565
|
+
const version = installResult.bundle?.version ?? readReceiptVersion();
|
|
1566
|
+
steps.push({
|
|
1567
|
+
name: "plugin",
|
|
1568
|
+
ok: true,
|
|
1569
|
+
detail: version ? `${version} installed` : "installed",
|
|
1570
|
+
});
|
|
1571
|
+
pluginJustInstalled = installResult.result !== "current";
|
|
1572
|
+
}
|
|
1573
|
+
else {
|
|
1574
|
+
const reasonCode = installResult.error?.code;
|
|
1575
|
+
const reasonMessage = installResult.error?.message ?? installResult.result ?? "unknown reason";
|
|
1576
|
+
const hint = reasonCode === "auth_required" || reasonCode === "renewal_required"
|
|
1577
|
+
? "run `agent-memory plugin install` and complete the browser sign-in"
|
|
1578
|
+
: "run `agent-memory plugin install --json` for details";
|
|
1579
|
+
steps.push({
|
|
1580
|
+
name: "plugin",
|
|
1581
|
+
ok: false,
|
|
1582
|
+
detail: `install failed (${reasonMessage}) — ${hint}`,
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
catch (error) {
|
|
1587
|
+
steps.push({
|
|
1588
|
+
name: "plugin",
|
|
1589
|
+
ok: false,
|
|
1590
|
+
detail: `install failed — ${error.message}`,
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (skipPlugin) {
|
|
1596
|
+
steps.push({ name: "plugin", ok: true, detail: "skipped — core-only setup requested" });
|
|
1597
|
+
}
|
|
1598
|
+
// Step 5: register the MCP server in every detected local harness (Claude
|
|
1599
|
+
// Code, Cursor, Windsurf, Codex). Idempotent — re-running setup after adding
|
|
1600
|
+
// a new agent will pick it up. Hooks handle passive injection; MCP is what
|
|
1601
|
+
// lets the model pull memory on demand as tools.
|
|
1602
|
+
let mcpRegisteredKeys = [];
|
|
1603
|
+
if (!skipMcp) {
|
|
1604
|
+
try {
|
|
1605
|
+
const results = registerMcpInAgents(null);
|
|
1606
|
+
const registered = results.filter((r) => r.status === "registered").map((r) => r.key);
|
|
1607
|
+
const already = results.filter((r) => r.status === "already").map((r) => r.key);
|
|
1608
|
+
mcpRegisteredKeys = registered;
|
|
1609
|
+
let detail;
|
|
1610
|
+
if (registered.length === 0 && already.length === 0) {
|
|
1611
|
+
detail = "no supported agents detected";
|
|
1612
|
+
}
|
|
1613
|
+
else if (registered.length === 0) {
|
|
1614
|
+
detail = `already registered (${already.join(", ")})`;
|
|
1615
|
+
}
|
|
1616
|
+
else if (already.length === 0) {
|
|
1617
|
+
detail = `registered (${registered.join(", ")})`;
|
|
1618
|
+
}
|
|
1619
|
+
else {
|
|
1620
|
+
detail = `registered (${registered.join(", ")}); already (${already.join(", ")})`;
|
|
1621
|
+
}
|
|
1622
|
+
steps.push({ name: "mcp", ok: true, detail });
|
|
1623
|
+
}
|
|
1624
|
+
catch (error) {
|
|
1625
|
+
steps.push({ name: "mcp", ok: false, detail: error.message });
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
else {
|
|
1629
|
+
steps.push({ name: "mcp", ok: true, detail: "skipped" });
|
|
1630
|
+
}
|
|
1631
|
+
if (json) {
|
|
1632
|
+
output({ ok: steps.every((step) => step.ok), directory: getMemoryDir(), steps }, true);
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
console.log("");
|
|
1636
|
+
console.log(colorize("agent-memory setup", "bold"));
|
|
1637
|
+
for (const step of steps) {
|
|
1638
|
+
const mark = step.ok ? MARK_OK : MARK_FAIL;
|
|
1639
|
+
const detail = step.detail ? colorize(` ${step.detail}`, "dim") : "";
|
|
1640
|
+
console.log(` ${mark} ${step.name}${detail}`);
|
|
1641
|
+
}
|
|
1642
|
+
console.log("");
|
|
1643
|
+
if (mcpRegisteredKeys.length > 0) {
|
|
1644
|
+
console.log(colorize(`Restart ${mcpRegisteredKeys.join(", ")} to pick up the new MCP server.`, "dim"));
|
|
1645
|
+
}
|
|
1646
|
+
await printSetupUsageStats(steps.some((s) => s.name === "plugin" && s.ok));
|
|
1647
|
+
console.log(colorize("Setup complete. Your agents will discover memory automatically next session.", "green"));
|
|
1648
|
+
console.log(colorize("Your notes stay in plain Markdown on this device — no account, no upload.", "dim"));
|
|
1649
|
+
console.log(colorize('Open your agent and ask: "What do you remember about me?"', "cyan"));
|
|
1650
|
+
console.log("");
|
|
1651
|
+
console.log("Try it now:");
|
|
1652
|
+
console.log(` ${colorize('agent-memory save "your first note"', "cyan")} — save a note you own`);
|
|
1653
|
+
console.log(` ${colorize("agent-memory status", "cyan")} — verify everything is healthy`);
|
|
1654
|
+
if (pluginJustInstalled) {
|
|
1655
|
+
console.log("");
|
|
1656
|
+
console.log(colorize("The local plugin is live. Feel the magic now:", "green"));
|
|
1657
|
+
console.log(` ${colorize('agent-memory recall "what did we decide about auth?"', "cyan")} — search past sessions`);
|
|
1658
|
+
console.log(` ${colorize("agent-memory learn", "cyan")} — surface repeated corrections`);
|
|
1659
|
+
console.log(` ${colorize("agent-memory worker start", "cyan")} — capture new sessions in real time`);
|
|
1660
|
+
console.log(` ${colorize("agent-memory dashboard", "cyan")} — private local dashboard`);
|
|
1661
|
+
}
|
|
1662
|
+
else if (skipPlugin) {
|
|
1663
|
+
console.log(colorize("Core-only setup selected. Enable session intelligence anytime:", "dim"));
|
|
1664
|
+
console.log(` ${colorize("agent-memory plugin install", "cyan")} — recall and learn with a free daily allowance`);
|
|
1665
|
+
}
|
|
1666
|
+
console.log("");
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Explain the optional plugin without making a successful core setup feel
|
|
1670
|
+
* incomplete. Keep the quota line here in sync with `freeEntitlement()`.
|
|
1671
|
+
*/
|
|
1672
|
+
function printProPitch(mode) {
|
|
1673
|
+
console.log("");
|
|
1674
|
+
if (mode === "reinstall") {
|
|
1675
|
+
console.log(`${MARK_WARN} ${colorize("The optional local plugin is not enabled.", "yellow")}`);
|
|
1676
|
+
console.log(colorize(" Core memory is already ready. The plugin adds:", "dim"));
|
|
1677
|
+
}
|
|
1678
|
+
else {
|
|
1679
|
+
console.log(`${colorize("Optional: AgentMemory Pro", "bold")} — ${colorize("memory that learns from your work", "dim")}`);
|
|
1680
|
+
}
|
|
1681
|
+
console.log(` ${colorize("Recall across sessions", "cyan")} Ask "what did we decide about auth?" across Claude, Codex, Cursor.`);
|
|
1682
|
+
console.log(` ${colorize("Learn from corrections", "cyan")} Turn repeated fixes into memory you can inspect and undo.`);
|
|
1683
|
+
console.log(` ${colorize("Real-time capture", "cyan")} Local worker indexes new sessions as they happen.`);
|
|
1684
|
+
console.log(` ${colorize("Private by default", "cyan")} Memory and session content stay on this device — no account required.`);
|
|
1685
|
+
console.log(` ${colorize("Included at no cost:", "green")} ${colorize("20 recalls + 5 learning scans per day", "bold")}. Local indexing and dashboard remain free.`);
|
|
1686
|
+
console.log("");
|
|
1687
|
+
}
|
|
1688
|
+
async function cmdStatus(flags) {
|
|
1689
|
+
const json = hasFlag(flags, "json");
|
|
1690
|
+
ensureDirs();
|
|
1691
|
+
const dir = getMemoryDir();
|
|
1692
|
+
const memFile = getMemoryFile();
|
|
1693
|
+
const spFile = getScratchpadFile();
|
|
1694
|
+
const dailyDir = getDailyDir();
|
|
1695
|
+
const topicsDir = getTopicsDir();
|
|
1696
|
+
const memContent = readFileSafe(memFile);
|
|
1697
|
+
const spContent = readFileSafe(spFile);
|
|
1698
|
+
let dailyCount = 0;
|
|
1699
|
+
try {
|
|
1700
|
+
dailyCount = fs.readdirSync(dailyDir).filter((f) => f.endsWith(".md")).length;
|
|
1701
|
+
}
|
|
1702
|
+
catch {
|
|
1703
|
+
// directory may not exist
|
|
1704
|
+
}
|
|
1705
|
+
let topicCount = 0;
|
|
1706
|
+
try {
|
|
1707
|
+
topicCount = fs.readdirSync(topicsDir).filter((f) => f.endsWith(".md")).length;
|
|
1708
|
+
}
|
|
1709
|
+
catch {
|
|
1710
|
+
// directory may not exist
|
|
1711
|
+
}
|
|
1712
|
+
const qmdFound = await detectQmd();
|
|
1713
|
+
let hasCollection = false;
|
|
1714
|
+
let health = null;
|
|
1715
|
+
let embeddings = "n/a";
|
|
1716
|
+
if (qmdFound) {
|
|
1717
|
+
hasCollection = await checkCollection();
|
|
1718
|
+
if (hasCollection) {
|
|
1719
|
+
await ensureQmdAvailableForSync();
|
|
1720
|
+
health = await getQmdHealth();
|
|
1721
|
+
// A live semantic probe confirms embeddings are actually usable, but
|
|
1722
|
+
// it costs a real qmd query (and a possible model load), so it's
|
|
1723
|
+
// opt-in — the cheap pending-embed count below covers the common case.
|
|
1724
|
+
if (hasFlag(flags, "probe")) {
|
|
1725
|
+
embeddings = await probeEmbeddings();
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
const embedMode = getQmdEmbedMode();
|
|
1730
|
+
let officialPlugin = {
|
|
1731
|
+
installed: false,
|
|
1732
|
+
result: "unavailable",
|
|
1733
|
+
entitlement: "missing",
|
|
1734
|
+
};
|
|
1735
|
+
try {
|
|
1736
|
+
const plugin = await createDefaultPluginBootstrap(VERSION).status();
|
|
1737
|
+
officialPlugin = {
|
|
1738
|
+
installed: Boolean(plugin.bundle),
|
|
1739
|
+
result: plugin.result,
|
|
1740
|
+
entitlement: plugin.entitlement.state,
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
catch {
|
|
1744
|
+
// Commercial status must never make core status fail.
|
|
1745
|
+
}
|
|
1746
|
+
if (json) {
|
|
1747
|
+
output({
|
|
1748
|
+
directory: dir,
|
|
1749
|
+
memoryFile: {
|
|
1750
|
+
exists: memContent !== null,
|
|
1751
|
+
chars: memContent?.length ?? 0,
|
|
1752
|
+
lines: memContent ? memContent.split("\n").length : 0,
|
|
1753
|
+
},
|
|
805
1754
|
scratchpadFile: {
|
|
806
1755
|
exists: spContent !== null,
|
|
807
1756
|
items: spContent ? parseScratchpad(spContent).length : 0,
|
|
@@ -842,7 +1791,7 @@ async function cmdStatus(flags) {
|
|
|
842
1791
|
console.log("");
|
|
843
1792
|
if (qmdFound) {
|
|
844
1793
|
console.log(`qmd: available`);
|
|
845
|
-
console.log(`Collection '${getCollectionName()}': ${hasCollection ? "configured" : "not configured — run: agent-memory
|
|
1794
|
+
console.log(`Collection '${getCollectionName()}': ${hasCollection ? "configured" : "not configured — run: agent-memory setup"}`);
|
|
846
1795
|
console.log(`Embed mode: ${embedMode}`);
|
|
847
1796
|
if (hasCollection && embeddings !== "n/a") {
|
|
848
1797
|
const embLabel = embeddings === "ready"
|
|
@@ -875,6 +1824,320 @@ async function cmdStatus(flags) {
|
|
|
875
1824
|
}
|
|
876
1825
|
}
|
|
877
1826
|
}
|
|
1827
|
+
function printDoctorRow(row) {
|
|
1828
|
+
const mark = row.status === "ok" ? MARK_OK : row.status === "warn" ? MARK_WARN : MARK_FAIL;
|
|
1829
|
+
console.log(` ${mark} ${row.label.padEnd(28)} ${colorize(row.detail, "dim")}`);
|
|
1830
|
+
if (row.fix)
|
|
1831
|
+
console.log(` ${colorize(`↳ fix: ${row.fix}`, "cyan")}`);
|
|
1832
|
+
}
|
|
1833
|
+
async function cmdDoctor(flags) {
|
|
1834
|
+
const json = hasFlag(flags, "json");
|
|
1835
|
+
ensureDirs();
|
|
1836
|
+
const rows = [];
|
|
1837
|
+
// Memory dir
|
|
1838
|
+
const dir = getMemoryDir();
|
|
1839
|
+
let dirWritable = false;
|
|
1840
|
+
try {
|
|
1841
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
1842
|
+
dirWritable = true;
|
|
1843
|
+
}
|
|
1844
|
+
catch {
|
|
1845
|
+
// noop
|
|
1846
|
+
}
|
|
1847
|
+
rows.push({
|
|
1848
|
+
status: dirWritable ? "ok" : "fail",
|
|
1849
|
+
label: "Memory directory",
|
|
1850
|
+
detail: dirWritable ? dir : `${dir} — not writable`,
|
|
1851
|
+
fix: dirWritable ? undefined : `chmod u+w ${dir}`,
|
|
1852
|
+
});
|
|
1853
|
+
// MEMORY.md seeded?
|
|
1854
|
+
const memContent = readFileSafe(getMemoryFile());
|
|
1855
|
+
rows.push({
|
|
1856
|
+
status: memContent && memContent.length > 0 ? "ok" : "warn",
|
|
1857
|
+
label: "MEMORY.md",
|
|
1858
|
+
detail: memContent && memContent.length > 0
|
|
1859
|
+
? `${memContent.split("\n").length} lines, ${memContent.length} chars`
|
|
1860
|
+
: "empty — new sessions will see no long-term context",
|
|
1861
|
+
fix: memContent && memContent.length > 0
|
|
1862
|
+
? undefined
|
|
1863
|
+
: 'agent-memory write --target long_term --content "…first fact…"',
|
|
1864
|
+
});
|
|
1865
|
+
// qmd
|
|
1866
|
+
const qmdFound = await detectQmd();
|
|
1867
|
+
if (!qmdFound) {
|
|
1868
|
+
rows.push({
|
|
1869
|
+
status: "warn",
|
|
1870
|
+
label: "qmd search index",
|
|
1871
|
+
detail: "not installed — keyword/semantic search unavailable",
|
|
1872
|
+
fix: "bun install -g https://github.com/tobi/qmd",
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
else {
|
|
1876
|
+
const hasCollection = await checkCollection();
|
|
1877
|
+
if (!hasCollection) {
|
|
1878
|
+
rows.push({
|
|
1879
|
+
status: "warn",
|
|
1880
|
+
label: "qmd collection",
|
|
1881
|
+
detail: `'${getCollectionName()}' not configured`,
|
|
1882
|
+
fix: "agent-memory setup",
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
else {
|
|
1886
|
+
try {
|
|
1887
|
+
await ensureQmdAvailableForSync();
|
|
1888
|
+
}
|
|
1889
|
+
catch {
|
|
1890
|
+
// noop
|
|
1891
|
+
}
|
|
1892
|
+
const health = await getQmdHealth();
|
|
1893
|
+
const files = health?.totalFiles ?? 0;
|
|
1894
|
+
const pending = health?.pendingEmbed ?? 0;
|
|
1895
|
+
rows.push({
|
|
1896
|
+
status: pending > 0 ? "warn" : "ok",
|
|
1897
|
+
label: "qmd search index",
|
|
1898
|
+
detail: `${files} files indexed${pending > 0 ? `, ${pending} pending embeds` : ""}`,
|
|
1899
|
+
fix: pending > 0 ? "agent-memory sync" : undefined,
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
// Skills + hooks per detected host
|
|
1904
|
+
const { homeDir, targets } = detectHookAgents();
|
|
1905
|
+
const detected = targets.filter((target) => target.detected);
|
|
1906
|
+
const hookMode = readHookMode();
|
|
1907
|
+
rows.push({
|
|
1908
|
+
status: "ok",
|
|
1909
|
+
label: "Hook mode",
|
|
1910
|
+
detail: hookMode === "per-turn"
|
|
1911
|
+
? "per-turn (SessionStart stable snapshot + UserPromptSubmit query-scoped recall)"
|
|
1912
|
+
: "stable (SessionStart-only, full snapshot every session)",
|
|
1913
|
+
});
|
|
1914
|
+
if (detected.length === 0) {
|
|
1915
|
+
rows.push({
|
|
1916
|
+
status: "warn",
|
|
1917
|
+
label: "Agent hosts",
|
|
1918
|
+
detail: "no supported agents detected (Claude Code, Codex, Cursor, opencode)",
|
|
1919
|
+
fix: "install one of the agents first, then: agent-memory install-skills",
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
else {
|
|
1923
|
+
rows.push({
|
|
1924
|
+
status: "ok",
|
|
1925
|
+
label: "Agent hosts detected",
|
|
1926
|
+
detail: detected.map((target) => target.label).join(", "),
|
|
1927
|
+
});
|
|
1928
|
+
for (const target of detected) {
|
|
1929
|
+
if (!target.supported) {
|
|
1930
|
+
// Skip skill/hook rows for hosts we don't yet integrate with (e.g. pi has its own extension).
|
|
1931
|
+
continue;
|
|
1932
|
+
}
|
|
1933
|
+
const skillPath = homeDir ? `${target.homeMarker}/skills/agent-memory/SKILL.md` : null;
|
|
1934
|
+
const skillInstalled = skillPath ? fs.existsSync(skillPath) : false;
|
|
1935
|
+
rows.push({
|
|
1936
|
+
status: skillInstalled ? "ok" : "warn",
|
|
1937
|
+
label: `Skill: ${target.label}`,
|
|
1938
|
+
detail: skillInstalled ? "SKILL.md installed" : "SKILL.md missing — agent cannot call memory",
|
|
1939
|
+
fix: skillInstalled ? undefined : "agent-memory install-skills",
|
|
1940
|
+
});
|
|
1941
|
+
const sessionInstalled = homeDir ? isHookInstalled(homeDir, target.key) : false;
|
|
1942
|
+
const supportsPerTurn = target.key === "claude" || target.key === "codex";
|
|
1943
|
+
// opencode only gets a static instructions file (no command-execution hook API in its
|
|
1944
|
+
// plugin surface we could verify) — never report it as a guaranteed-automatic hook.
|
|
1945
|
+
const guaranteedAutomatic = target.key !== "opencode";
|
|
1946
|
+
const promptInstalled = homeDir && supportsPerTurn ? isUserPromptSubmitInstalled(homeDir, target.key) : false;
|
|
1947
|
+
const wantsPerTurn = hookMode === "per-turn" && supportsPerTurn;
|
|
1948
|
+
// Stop backs the write side with a periodic memory-write nudge. Claude
|
|
1949
|
+
// Code only for now, mode-independent — always wanted when supported.
|
|
1950
|
+
const wantsWriteHooks = target.key === "claude";
|
|
1951
|
+
const stopInstalled = homeDir && wantsWriteHooks ? isStopHookInstalled(homeDir, target.key) : false;
|
|
1952
|
+
const writeHooksOk = !wantsWriteHooks || stopInstalled;
|
|
1953
|
+
const ok = sessionInstalled && guaranteedAutomatic && (wantsPerTurn ? promptInstalled : true) && writeHooksOk;
|
|
1954
|
+
let detail;
|
|
1955
|
+
if (!sessionInstalled) {
|
|
1956
|
+
detail = "not installed — no automatic context";
|
|
1957
|
+
}
|
|
1958
|
+
else if (!guaranteedAutomatic) {
|
|
1959
|
+
detail = "static instructions installed — model must run context manually, not guaranteed";
|
|
1960
|
+
}
|
|
1961
|
+
else if (!supportsPerTurn) {
|
|
1962
|
+
detail = "SessionStart hook active";
|
|
1963
|
+
}
|
|
1964
|
+
else if (wantsPerTurn && !promptInstalled) {
|
|
1965
|
+
detail = "SessionStart active, UserPromptSubmit missing — per-turn recall disabled";
|
|
1966
|
+
}
|
|
1967
|
+
else if (wantsPerTurn) {
|
|
1968
|
+
detail = "SessionStart + UserPromptSubmit hooks active";
|
|
1969
|
+
}
|
|
1970
|
+
else {
|
|
1971
|
+
detail = "SessionStart hook active";
|
|
1972
|
+
}
|
|
1973
|
+
if (wantsWriteHooks) {
|
|
1974
|
+
detail += stopInstalled ? "; Stop memory-write nudge active" : "; Stop memory-write nudge missing";
|
|
1975
|
+
}
|
|
1976
|
+
rows.push({
|
|
1977
|
+
status: ok ? "ok" : "warn",
|
|
1978
|
+
label: `Hook: ${target.label}`,
|
|
1979
|
+
detail,
|
|
1980
|
+
fix: ok ? undefined : `agent-memory install-hooks --mode ${hookMode}`,
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
// Pro plugin — emit exactly one row per outcome (bootstrap failure, not installed, or installed).
|
|
1985
|
+
let proBootstrapError;
|
|
1986
|
+
let proStatus;
|
|
1987
|
+
try {
|
|
1988
|
+
const plugin = await createDefaultPluginBootstrap(VERSION).status();
|
|
1989
|
+
proStatus = {
|
|
1990
|
+
installed: Boolean(plugin.bundle),
|
|
1991
|
+
result: plugin.result,
|
|
1992
|
+
entitlement: plugin.entitlement.state,
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
catch (error) {
|
|
1996
|
+
proBootstrapError = error instanceof Error ? error.message : String(error);
|
|
1997
|
+
}
|
|
1998
|
+
if (proBootstrapError) {
|
|
1999
|
+
rows.push({
|
|
2000
|
+
status: "fail",
|
|
2001
|
+
label: "AgentMemory Pro",
|
|
2002
|
+
detail: `bootstrap failed: ${proBootstrapError}`,
|
|
2003
|
+
fix: "agent-memory pro install",
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
else if (!proStatus?.installed) {
|
|
2007
|
+
rows.push({
|
|
2008
|
+
status: "warn",
|
|
2009
|
+
label: "AgentMemory Pro",
|
|
2010
|
+
detail: "not installed — recall + learn + dashboard unavailable",
|
|
2011
|
+
fix: "agent-memory pro install",
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
else {
|
|
2015
|
+
rows.push({
|
|
2016
|
+
status: "ok",
|
|
2017
|
+
label: "AgentMemory Pro",
|
|
2018
|
+
detail: `installed (${proStatus.result}, entitlement=${proStatus.entitlement})`,
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
if (json) {
|
|
2022
|
+
output({ rows }, true);
|
|
2023
|
+
if (rows.some((row) => row.status === "fail"))
|
|
2024
|
+
process.exitCode = 1;
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
console.log("");
|
|
2028
|
+
console.log(colorize("AgentMemory diagnostic", "bold"));
|
|
2029
|
+
console.log("");
|
|
2030
|
+
for (const row of rows)
|
|
2031
|
+
printDoctorRow(row);
|
|
2032
|
+
console.log("");
|
|
2033
|
+
const failed = rows.filter((row) => row.status === "fail").length;
|
|
2034
|
+
const warned = rows.filter((row) => row.status === "warn").length;
|
|
2035
|
+
if (failed > 0) {
|
|
2036
|
+
console.log(colorize(`${failed} issue(s) need attention.`, "red"));
|
|
2037
|
+
process.exitCode = 1;
|
|
2038
|
+
}
|
|
2039
|
+
else if (warned > 0) {
|
|
2040
|
+
console.log(colorize(`${warned} optional improvement(s) available.`, "yellow"));
|
|
2041
|
+
}
|
|
2042
|
+
else {
|
|
2043
|
+
console.log(colorize("Everything looks healthy.", "green"));
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
async function cmdTutorial(flags) {
|
|
2047
|
+
const json = hasFlag(flags, "json");
|
|
2048
|
+
if (json) {
|
|
2049
|
+
exitError("tutorial is interactive — remove --json to run", true);
|
|
2050
|
+
}
|
|
2051
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2052
|
+
exitError("tutorial requires an interactive terminal", false);
|
|
2053
|
+
}
|
|
2054
|
+
const os = await import("node:os");
|
|
2055
|
+
const originalDir = getMemoryDir();
|
|
2056
|
+
const sandboxDir = fs.mkdtempSync(`${os.tmpdir()}/agent-memory-tutorial-`);
|
|
2057
|
+
_setBaseDir(sandboxDir);
|
|
2058
|
+
try {
|
|
2059
|
+
console.log("");
|
|
2060
|
+
console.log(colorize("AgentMemory Tutorial (3 minutes)", "bold"));
|
|
2061
|
+
console.log(colorize("Everything below runs in a throwaway sandbox — your real memory is untouched.", "dim"));
|
|
2062
|
+
console.log(colorize(`Sandbox: ${sandboxDir}`, "dim"));
|
|
2063
|
+
console.log("");
|
|
2064
|
+
console.log(colorize("Real memory dir stays at:", "dim"), originalDir);
|
|
2065
|
+
console.log("");
|
|
2066
|
+
await promptEnter("Press Enter to begin.");
|
|
2067
|
+
// Step 1: init
|
|
2068
|
+
console.log("");
|
|
2069
|
+
console.log(colorize("Step 1/4 — init", "bold"));
|
|
2070
|
+
console.log("This creates MEMORY.md, SCRATCHPAD.md, daily/ and topics/ in the sandbox.");
|
|
2071
|
+
console.log("");
|
|
2072
|
+
await promptEnter("Ready? Press Enter to run `agent-memory init --yes`.");
|
|
2073
|
+
ensureDirs();
|
|
2074
|
+
console.log(` ${MARK_OK} Sandbox ready at ${sandboxDir}`);
|
|
2075
|
+
// Step 2: save
|
|
2076
|
+
console.log("");
|
|
2077
|
+
console.log(colorize("Step 2/4 — save a memory", "bold"));
|
|
2078
|
+
console.log("`save` appends a line to today's log. Try it:");
|
|
2079
|
+
console.log("");
|
|
2080
|
+
await promptEnter('Press Enter to run `agent-memory save "tutorial: first save"`.');
|
|
2081
|
+
await memoryWrite({ target: "daily", content: "tutorial: first save" });
|
|
2082
|
+
console.log(` ${MARK_OK} Saved. Look inside: ${sandboxDir}/daily/`);
|
|
2083
|
+
// Step 3: note (scratchpad)
|
|
2084
|
+
console.log("");
|
|
2085
|
+
console.log(colorize("Step 3/4 — track a follow-up", "bold"));
|
|
2086
|
+
console.log("`note` adds a persistent checklist item.");
|
|
2087
|
+
console.log("");
|
|
2088
|
+
await promptEnter('Press Enter to run `agent-memory note "tutorial: try recall"`.');
|
|
2089
|
+
// Direct scratchpad manipulation to keep the demo dep-free
|
|
2090
|
+
const spFile = getScratchpadFile();
|
|
2091
|
+
const existing = readFileSafe(spFile) ?? "# Scratchpad\n";
|
|
2092
|
+
fs.writeFileSync(spFile, `${existing}\n<!-- ${nowTimestamp()} [cli] -->\n- [ ] tutorial: try recall\n`);
|
|
2093
|
+
console.log(` ${MARK_OK} Added a scratchpad item you can complete later.`);
|
|
2094
|
+
// Step 4: context
|
|
2095
|
+
console.log("");
|
|
2096
|
+
console.log(colorize("Step 4/4 — inspect what the agent sees", "bold"));
|
|
2097
|
+
console.log("`context` builds the injected context block your agent reads at session start.");
|
|
2098
|
+
console.log("");
|
|
2099
|
+
await promptEnter("Press Enter to preview the context.");
|
|
2100
|
+
const context = buildMemoryContext("");
|
|
2101
|
+
const preview = context.split("\n").slice(0, 20).join("\n");
|
|
2102
|
+
console.log(colorize("--- context preview (first 20 lines) ---", "dim"));
|
|
2103
|
+
console.log(preview);
|
|
2104
|
+
console.log(colorize("--- end preview ---", "dim"));
|
|
2105
|
+
console.log("");
|
|
2106
|
+
console.log(colorize("You're done.", "bold"));
|
|
2107
|
+
console.log("");
|
|
2108
|
+
console.log("In your real setup:");
|
|
2109
|
+
console.log(" agent-memory setup — one-shot install: memory dir, skills, hooks, MCP");
|
|
2110
|
+
console.log(' agent-memory save "your note" — quick save');
|
|
2111
|
+
console.log(' agent-memory note "your todo" — scratchpad item');
|
|
2112
|
+
console.log(" agent-memory doctor — health check");
|
|
2113
|
+
console.log("");
|
|
2114
|
+
if (await promptYesNo("Remove the sandbox directory now?", true)) {
|
|
2115
|
+
try {
|
|
2116
|
+
fs.rmSync(sandboxDir, { recursive: true, force: true });
|
|
2117
|
+
console.log(` ${MARK_OK} Sandbox removed.`);
|
|
2118
|
+
}
|
|
2119
|
+
catch (error) {
|
|
2120
|
+
console.log(colorize(` Could not remove sandbox: ${error instanceof Error ? error.message : String(error)}`, "yellow"));
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
else {
|
|
2124
|
+
console.log(` Sandbox preserved: ${sandboxDir}`);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
finally {
|
|
2128
|
+
_setBaseDir(originalDir);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
async function promptEnter(prompt) {
|
|
2132
|
+
const readline = await import("node:readline/promises");
|
|
2133
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
2134
|
+
try {
|
|
2135
|
+
await rl.question(`${colorize(prompt, "cyan")} `);
|
|
2136
|
+
}
|
|
2137
|
+
finally {
|
|
2138
|
+
rl.close();
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
878
2141
|
async function cmdDistil(flags) {
|
|
879
2142
|
const json = hasFlag(flags, "json");
|
|
880
2143
|
const dryRun = hasFlag(flags, "dry-run");
|
|
@@ -910,8 +2173,8 @@ Usage:
|
|
|
910
2173
|
agent-memory plugin manage [--no-browser]
|
|
911
2174
|
|
|
912
2175
|
The public core remains fully usable without AgentMemory Pro. Install uses a random
|
|
913
|
-
installation identifier and requires no account or email. The free
|
|
914
|
-
|
|
2176
|
+
installation identifier and requires no account or email. The free tier includes
|
|
2177
|
+
20 recalls and 5 learning scans per local day; indexing and the Memory Dashboard
|
|
915
2178
|
remain available. Memory and session content stay on this device.`);
|
|
916
2179
|
}
|
|
917
2180
|
function pluginCommandFailure(command, error) {
|
|
@@ -1026,16 +2289,155 @@ async function printFirstRunProof() {
|
|
|
1026
2289
|
// Personalized proof is helpful but must never turn a successful install into a failure.
|
|
1027
2290
|
}
|
|
1028
2291
|
}
|
|
2292
|
+
const PRO_PREVIEW_DAILY_SESSION_CAP = 50;
|
|
2293
|
+
const PRO_PREVIEW_DISCOVERY_FILE_CAP = 10_000;
|
|
2294
|
+
function previewSessionRoots() {
|
|
2295
|
+
return {
|
|
2296
|
+
claude: process.env.AGENT_MEMORY_CLAUDE_SESSION_ROOT ?? path.join(os.homedir(), ".claude", "projects"),
|
|
2297
|
+
codex: process.env.AGENT_MEMORY_CODEX_SESSION_ROOT ?? path.join(os.homedir(), ".codex", "sessions"),
|
|
2298
|
+
pi: process.env.AGENT_MEMORY_PI_SESSION_ROOT ?? path.join(os.homedir(), ".pi", "agent", "sessions"),
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
function localDayKey(now = new Date()) {
|
|
2302
|
+
const year = now.getFullYear();
|
|
2303
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
2304
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
2305
|
+
return `${year}-${month}-${day}`;
|
|
2306
|
+
}
|
|
2307
|
+
function nextLocalMidnight(now = new Date()) {
|
|
2308
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).toISOString();
|
|
2309
|
+
}
|
|
2310
|
+
function previewUsagePath() {
|
|
2311
|
+
return path.join(process.env.AGENT_MEMORY_PLUGIN_DIR ?? path.join(os.homedir(), ".agent-memory", "system", "plugins"), "state", "pro-preview-usage.json");
|
|
2312
|
+
}
|
|
2313
|
+
function countSessionFiles(root, cap) {
|
|
2314
|
+
if (!fs.existsSync(root))
|
|
2315
|
+
return 0;
|
|
2316
|
+
let count = 0;
|
|
2317
|
+
const stack = [root];
|
|
2318
|
+
while (stack.length && count < cap) {
|
|
2319
|
+
const directory = stack.pop();
|
|
2320
|
+
let entries;
|
|
2321
|
+
try {
|
|
2322
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
2323
|
+
}
|
|
2324
|
+
catch {
|
|
2325
|
+
continue;
|
|
2326
|
+
}
|
|
2327
|
+
for (const entry of entries) {
|
|
2328
|
+
if (entry.name.startsWith("."))
|
|
2329
|
+
continue;
|
|
2330
|
+
const fullPath = path.join(directory, entry.name);
|
|
2331
|
+
if (entry.isDirectory()) {
|
|
2332
|
+
stack.push(fullPath);
|
|
2333
|
+
}
|
|
2334
|
+
else if (entry.isFile()) {
|
|
2335
|
+
count++;
|
|
2336
|
+
if (count >= cap)
|
|
2337
|
+
break;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
return count;
|
|
2342
|
+
}
|
|
2343
|
+
function reserveProPreviewSessions(discovered) {
|
|
2344
|
+
const now = new Date();
|
|
2345
|
+
const date = localDayKey(now);
|
|
2346
|
+
const filePath = previewUsagePath();
|
|
2347
|
+
let used = 0;
|
|
2348
|
+
try {
|
|
2349
|
+
const existing = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
2350
|
+
if (existing.date === date && Number.isSafeInteger(existing.used))
|
|
2351
|
+
used = Math.max(0, Number(existing.used));
|
|
2352
|
+
}
|
|
2353
|
+
catch {
|
|
2354
|
+
// Missing or corrupt preview usage starts a fresh local day window.
|
|
2355
|
+
}
|
|
2356
|
+
const remainingBefore = Math.max(0, PRO_PREVIEW_DAILY_SESSION_CAP - used);
|
|
2357
|
+
const consumed = Math.min(discovered, remainingBefore);
|
|
2358
|
+
const nextUsed = used + consumed;
|
|
2359
|
+
try {
|
|
2360
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
2361
|
+
fs.writeFileSync(filePath, `${JSON.stringify({ schemaVersion: 1, date, used: nextUsed, updatedAt: now.toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
2362
|
+
}
|
|
2363
|
+
catch {
|
|
2364
|
+
// Preview metering is best-effort and local only; never block the preview.
|
|
2365
|
+
}
|
|
2366
|
+
return {
|
|
2367
|
+
consumed,
|
|
2368
|
+
cap: {
|
|
2369
|
+
limit: PRO_PREVIEW_DAILY_SESSION_CAP,
|
|
2370
|
+
used: nextUsed,
|
|
2371
|
+
remaining: Math.max(0, PRO_PREVIEW_DAILY_SESSION_CAP - nextUsed),
|
|
2372
|
+
resetAt: nextLocalMidnight(now),
|
|
2373
|
+
exhausted: remainingBefore === 0 && discovered > 0,
|
|
2374
|
+
},
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
function scanProPreviewSessions() {
|
|
2378
|
+
const roots = previewSessionRoots();
|
|
2379
|
+
const sessions = { claude: 0, codex: 0, pi: 0 };
|
|
2380
|
+
for (const host of Object.keys(roots)) {
|
|
2381
|
+
sessions[host] = countSessionFiles(roots[host], PRO_PREVIEW_DISCOVERY_FILE_CAP);
|
|
2382
|
+
}
|
|
2383
|
+
const discovered = sessions.claude + sessions.codex + sessions.pi;
|
|
2384
|
+
const { cap, consumed } = reserveProPreviewSessions(discovered);
|
|
2385
|
+
const previewed = Math.min(discovered, consumed);
|
|
2386
|
+
return { available: discovered > 0 && previewed > 0, sessions, discovered, previewed, cap };
|
|
2387
|
+
}
|
|
2388
|
+
async function cmdProPreview(flags) {
|
|
2389
|
+
const json = hasFlag(flags, "json");
|
|
2390
|
+
const preview = scanProPreviewSessions();
|
|
2391
|
+
if (json)
|
|
2392
|
+
output(preview, true);
|
|
2393
|
+
else
|
|
2394
|
+
renderProPreview(preview);
|
|
2395
|
+
}
|
|
2396
|
+
function renderProPreview(preview) {
|
|
2397
|
+
if (!preview.discovered) {
|
|
2398
|
+
console.log("No local coding history found yet. Try running a session in Claude Code or Codex first.");
|
|
2399
|
+
console.log("Then re-run: agent-memory pro preview");
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
if (preview.cap.exhausted) {
|
|
2403
|
+
console.log("");
|
|
2404
|
+
console.log(`Daily preview cap reached (${preview.cap.used}/${preview.cap.limit} sessions).`);
|
|
2405
|
+
console.log(`Resets at ${preview.cap.resetAt}.`);
|
|
2406
|
+
console.log("");
|
|
2407
|
+
console.log("Install to make your local history searchable:");
|
|
2408
|
+
console.log(" agent-memory pro install");
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
console.log("");
|
|
2412
|
+
console.log(colorize(` ${preview.previewed.toLocaleString("en-US")} of ${preview.discovered.toLocaleString("en-US")} local sessions previewed (nothing uploaded).`, "bold"));
|
|
2413
|
+
console.log("");
|
|
2414
|
+
console.log(` ${MARK_OK} Claude Code ${preview.sessions.claude.toLocaleString("en-US")}`);
|
|
2415
|
+
console.log(` ${MARK_OK} Codex ${preview.sessions.codex.toLocaleString("en-US")}`);
|
|
2416
|
+
if (preview.sessions.pi > 0) {
|
|
2417
|
+
console.log(` ${MARK_OK} Pi ${preview.sessions.pi.toLocaleString("en-US")}`);
|
|
2418
|
+
}
|
|
2419
|
+
console.log("");
|
|
2420
|
+
console.log(`Daily preview cap: ${preview.cap.used}/${preview.cap.limit} sessions used, ${preview.cap.remaining} remaining.`);
|
|
2421
|
+
console.log("");
|
|
2422
|
+
console.log("Install to make all of it searchable:");
|
|
2423
|
+
console.log(" agent-memory pro install");
|
|
2424
|
+
console.log("");
|
|
2425
|
+
console.log(colorize("No account. No email. Free preview starts now.", "dim"));
|
|
2426
|
+
}
|
|
1029
2427
|
async function cmdPro(flags, positional) {
|
|
1030
2428
|
const subcommand = positional[0];
|
|
1031
2429
|
if (!subcommand) {
|
|
1032
2430
|
await cmdPlugin(flags, ["list"]);
|
|
1033
2431
|
return;
|
|
1034
2432
|
}
|
|
2433
|
+
if (subcommand === "preview") {
|
|
2434
|
+
await cmdProPreview(flags);
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
1035
2437
|
const mapped = subcommand === "upgrade" ? "update" : subcommand;
|
|
1036
2438
|
if (!["install", "status", "update", "manage"].includes(mapped)) {
|
|
1037
2439
|
const json = hasFlag(flags, "json");
|
|
1038
|
-
const message = `Unknown Pro command: ${subcommand}. Available commands: install, status, upgrade, manage.`;
|
|
2440
|
+
const message = `Unknown Pro command: ${subcommand}. Available commands: install, preview, status, upgrade, manage.`;
|
|
1039
2441
|
if (json)
|
|
1040
2442
|
console.log(JSON.stringify({ error: message }));
|
|
1041
2443
|
else
|
|
@@ -1048,64 +2450,447 @@ async function cmdPro(flags, positional) {
|
|
|
1048
2450
|
await printFirstRunProof();
|
|
1049
2451
|
}
|
|
1050
2452
|
// ---------------------------------------------------------------------------
|
|
2453
|
+
// Upgrade (CLI + Pro plugin bundle)
|
|
2454
|
+
// ---------------------------------------------------------------------------
|
|
2455
|
+
async function readPluginCurrentVersion() {
|
|
2456
|
+
try {
|
|
2457
|
+
const manager = createDefaultPluginBootstrap(VERSION);
|
|
2458
|
+
const status = await manager.status("stable");
|
|
2459
|
+
return status.bundle?.version ?? null;
|
|
2460
|
+
}
|
|
2461
|
+
catch {
|
|
2462
|
+
return null;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
async function resolvePluginLatestHint() {
|
|
2466
|
+
try {
|
|
2467
|
+
const manager = createDefaultPluginBootstrap(VERSION);
|
|
2468
|
+
const status = await manager.status("stable");
|
|
2469
|
+
// `status.result === "update_available"` means the release feed advertises
|
|
2470
|
+
// a newer signed release. `bundle.version` always reflects the *installed*
|
|
2471
|
+
// version, so we cannot report the new version number from this call —
|
|
2472
|
+
// only the fact that an upgrade exists. `checkForUpgrades` treats
|
|
2473
|
+
// `latest == null` + explicit `pluginUpgradeAvailable` accordingly.
|
|
2474
|
+
const updateAvailable = status.result === "update_available";
|
|
2475
|
+
return {
|
|
2476
|
+
latest: updateAvailable ? null : (status.bundle?.version ?? null),
|
|
2477
|
+
updateAvailable,
|
|
2478
|
+
result: status,
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
catch {
|
|
2482
|
+
return { latest: null, updateAvailable: false };
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
async function cmdUpgrade(flags) {
|
|
2486
|
+
const json = hasFlag(flags, "json");
|
|
2487
|
+
const quiet = hasFlag(flags, "quiet");
|
|
2488
|
+
const checkOnly = hasFlag(flags, "check");
|
|
2489
|
+
const refresh = hasFlag(flags, "refresh");
|
|
2490
|
+
const onlyCli = hasFlag(flags, "cli");
|
|
2491
|
+
const onlyPlugin = hasFlag(flags, "plugin");
|
|
2492
|
+
const targetCli = onlyCli || !onlyPlugin;
|
|
2493
|
+
const targetPlugin = onlyPlugin || !onlyCli;
|
|
2494
|
+
const applyAll = hasFlag(flags, "yes") || !process.stdin.isTTY || !process.stdout.isTTY;
|
|
2495
|
+
const pluginCurrent = await readPluginCurrentVersion();
|
|
2496
|
+
const pluginProbe = targetPlugin ? await resolvePluginLatestHint() : { latest: null, updateAvailable: false };
|
|
2497
|
+
const status = await checkForUpgrades({
|
|
2498
|
+
cliCurrent: VERSION,
|
|
2499
|
+
pluginCurrent,
|
|
2500
|
+
refresh,
|
|
2501
|
+
pluginLatestHint: pluginProbe.latest,
|
|
2502
|
+
pluginUpgradeAvailable: pluginProbe.updateAvailable,
|
|
2503
|
+
});
|
|
2504
|
+
if (checkOnly) {
|
|
2505
|
+
if (json)
|
|
2506
|
+
output(status, true);
|
|
2507
|
+
else if (!quiet)
|
|
2508
|
+
printUpgradeStatus(status, { targetCli, targetPlugin });
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
const cliNeedsUpgrade = targetCli && status.cli.upgradeAvailable;
|
|
2512
|
+
const pluginNeedsUpgrade = targetPlugin && status.plugin.upgradeAvailable;
|
|
2513
|
+
if (!cliNeedsUpgrade && !pluginNeedsUpgrade) {
|
|
2514
|
+
if (json)
|
|
2515
|
+
output({ ...status, action: "noop" }, true);
|
|
2516
|
+
else if (!quiet) {
|
|
2517
|
+
console.log(`agent-memory ${VERSION} is up to date${pluginCurrent ? ` (Pro ${pluginCurrent})` : ""}.`);
|
|
2518
|
+
}
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
if (!applyAll) {
|
|
2522
|
+
if (!quiet)
|
|
2523
|
+
printUpgradeStatus(status, { targetCli, targetPlugin });
|
|
2524
|
+
const confirmed = await promptYesNo("Install available upgrades now?", true);
|
|
2525
|
+
if (!confirmed) {
|
|
2526
|
+
if (json)
|
|
2527
|
+
output({ ...status, action: "aborted" }, true);
|
|
2528
|
+
else
|
|
2529
|
+
console.log("Upgrade cancelled.");
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
const actions = [];
|
|
2534
|
+
if (cliNeedsUpgrade) {
|
|
2535
|
+
const method = detectInstallMethod();
|
|
2536
|
+
if (!quiet)
|
|
2537
|
+
console.log(`Upgrading CLI via: ${method.command.join(" ")}`);
|
|
2538
|
+
const result = runInstaller(method);
|
|
2539
|
+
actions.push({
|
|
2540
|
+
target: "cli",
|
|
2541
|
+
ok: result.ok,
|
|
2542
|
+
detail: result.ok
|
|
2543
|
+
? `CLI upgraded to ${status.cli.latest ?? "latest"}`
|
|
2544
|
+
: (result.stderr || result.stdout || `exit ${result.code}`).trim(),
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
if (pluginNeedsUpgrade) {
|
|
2548
|
+
if (!quiet)
|
|
2549
|
+
console.log("Upgrading Pro plugin bundle…");
|
|
2550
|
+
const pluginResult = await cmdPlugin({ json: json ? true : "" }, ["update"]);
|
|
2551
|
+
const ok = Boolean(pluginResult?.ok);
|
|
2552
|
+
actions.push({
|
|
2553
|
+
target: "plugin",
|
|
2554
|
+
ok,
|
|
2555
|
+
detail: ok
|
|
2556
|
+
? `Pro upgraded to ${status.plugin.latest ?? "latest"}`
|
|
2557
|
+
: (pluginResult?.error?.message ?? "plugin update failed"),
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
const allOk = actions.every((entry) => entry.ok);
|
|
2561
|
+
if (json)
|
|
2562
|
+
output({ ...status, actions, ok: allOk }, true);
|
|
2563
|
+
else if (!quiet) {
|
|
2564
|
+
for (const entry of actions)
|
|
2565
|
+
console.log(` ${entry.ok ? MARK_OK : MARK_FAIL} ${entry.detail}`);
|
|
2566
|
+
}
|
|
2567
|
+
if (!allOk)
|
|
2568
|
+
process.exitCode = 1;
|
|
2569
|
+
}
|
|
2570
|
+
function printUpgradeStatus(status, scope) {
|
|
2571
|
+
const lines = [];
|
|
2572
|
+
if (scope.targetCli) {
|
|
2573
|
+
const marker = status.cli.upgradeAvailable ? MARK_WARN : MARK_OK;
|
|
2574
|
+
const target = status.cli.latest ?? "unknown";
|
|
2575
|
+
lines.push(` ${marker} CLI ${status.cli.current} → ${target}${status.cli.upgradeAvailable ? " (upgrade available)" : ""}`);
|
|
2576
|
+
}
|
|
2577
|
+
if (scope.targetPlugin) {
|
|
2578
|
+
const current = status.plugin.current ?? "not installed";
|
|
2579
|
+
const target = status.plugin.latest ?? "unknown";
|
|
2580
|
+
const marker = status.plugin.upgradeAvailable ? MARK_WARN : MARK_OK;
|
|
2581
|
+
lines.push(` ${marker} Pro ${current} → ${target}${status.plugin.upgradeAvailable ? " (upgrade available)" : ""}`);
|
|
2582
|
+
}
|
|
2583
|
+
console.log(`Checked ${status.checkedAt}${status.fromCache ? " (cached)" : ""}`);
|
|
2584
|
+
for (const line of lines)
|
|
2585
|
+
console.log(line);
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Register the `agent-memory` MCP server across every supported local harness.
|
|
2589
|
+
* Idempotent: existing entries are preserved, missing agent dirs are reported
|
|
2590
|
+
* as `not-installed` rather than treated as an error. Returns a structured
|
|
2591
|
+
* result so both `serve --register` and `setup` can format it their own way.
|
|
2592
|
+
*/
|
|
2593
|
+
function registerMcpInAgents(only) {
|
|
2594
|
+
const home = os.homedir();
|
|
2595
|
+
const mcpEntry = { type: "stdio", command: "agent-memory", args: ["serve", "--mcp"] };
|
|
2596
|
+
const want = (key) => !only || only.has(key);
|
|
2597
|
+
const results = [];
|
|
2598
|
+
const registerJson = (key, displayName, detectPaths, configFile) => {
|
|
2599
|
+
const p = path.join(home, configFile);
|
|
2600
|
+
const detected = detectPaths.some((rel) => fs.existsSync(path.join(home, rel)));
|
|
2601
|
+
if (!detected) {
|
|
2602
|
+
results.push({ key, displayName, path: p, status: "not-installed" });
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
let s = {};
|
|
2606
|
+
try {
|
|
2607
|
+
s = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
2608
|
+
}
|
|
2609
|
+
catch { }
|
|
2610
|
+
const servers = (s.mcpServers ?? {});
|
|
2611
|
+
if (servers["agent-memory"]) {
|
|
2612
|
+
results.push({ key, displayName, path: p, status: "already" });
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
servers["agent-memory"] = mcpEntry;
|
|
2616
|
+
s.mcpServers = servers;
|
|
2617
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
2618
|
+
fs.writeFileSync(p, `${JSON.stringify(s, null, 2)}\n`);
|
|
2619
|
+
results.push({ key, displayName, path: p, status: "registered" });
|
|
2620
|
+
};
|
|
2621
|
+
// Claude Code stores user-level MCP servers in ~/.claude.json (top-level
|
|
2622
|
+
// `mcpServers` key), NOT in ~/.claude/settings.json. The settings file
|
|
2623
|
+
// holds hooks/permissions/env — Claude Code does not read MCP config from
|
|
2624
|
+
// there. Detect either the `.claude/` dir or `.claude.json` file so we
|
|
2625
|
+
// register whenever the user has Claude Code installed at all.
|
|
2626
|
+
if (want("claude"))
|
|
2627
|
+
registerJson("claude", "Claude Code", [".claude", ".claude.json"], ".claude.json");
|
|
2628
|
+
if (want("cursor"))
|
|
2629
|
+
registerJson("cursor", "Cursor", [".cursor"], ".cursor/mcp.json");
|
|
2630
|
+
if (want("windsurf"))
|
|
2631
|
+
registerJson("windsurf", "Windsurf", [".windsurf"], ".windsurf/mcp_settings.json");
|
|
2632
|
+
if (want("codex")) {
|
|
2633
|
+
const p = path.join(home, ".codex", "config.toml");
|
|
2634
|
+
if (!fs.existsSync(p)) {
|
|
2635
|
+
results.push({ key: "codex", displayName: "Codex", path: p, status: "not-installed" });
|
|
2636
|
+
}
|
|
2637
|
+
else {
|
|
2638
|
+
const existing = fs.readFileSync(p, "utf8");
|
|
2639
|
+
if (existing.includes("[mcp_servers.agent-memory]")) {
|
|
2640
|
+
results.push({ key: "codex", displayName: "Codex", path: p, status: "already" });
|
|
2641
|
+
}
|
|
2642
|
+
else {
|
|
2643
|
+
const block = '\n[mcp_servers.agent-memory]\ncommand = "agent-memory"\nargs = ["serve", "--mcp"]\n';
|
|
2644
|
+
fs.writeFileSync(p, existing.trimEnd() + block, "utf8");
|
|
2645
|
+
results.push({ key: "codex", displayName: "Codex", path: p, status: "registered" });
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
return results;
|
|
2650
|
+
}
|
|
2651
|
+
async function cmdServe(flags) {
|
|
2652
|
+
const isMcp = hasFlag(flags, "mcp");
|
|
2653
|
+
const isRegister = hasFlag(flags, "register");
|
|
2654
|
+
if (isRegister) {
|
|
2655
|
+
const only = getFlag(flags, "only");
|
|
2656
|
+
const onlySet = only ? new Set(only.split(",").map((s) => s.trim())) : null;
|
|
2657
|
+
const results = registerMcpInAgents(onlySet);
|
|
2658
|
+
const shown = onlySet ? results : results.filter((r) => r.status !== "not-installed");
|
|
2659
|
+
for (const r of shown) {
|
|
2660
|
+
if (r.status === "already")
|
|
2661
|
+
console.log(` ${r.displayName}: already registered`);
|
|
2662
|
+
else if (r.status === "registered")
|
|
2663
|
+
console.log(` ${r.displayName}: registered (${r.path})`);
|
|
2664
|
+
else
|
|
2665
|
+
console.log(` ${r.displayName}: not installed`);
|
|
2666
|
+
}
|
|
2667
|
+
const anyRegistered = results.some((r) => r.status === "registered");
|
|
2668
|
+
const anyDetected = results.some((r) => r.status !== "not-installed");
|
|
2669
|
+
if (anyRegistered) {
|
|
2670
|
+
console.log("");
|
|
2671
|
+
console.log("Restart the agent(s) above for the change to take effect.");
|
|
2672
|
+
}
|
|
2673
|
+
else if (!onlySet && !anyDetected) {
|
|
2674
|
+
console.log("No supported agents detected (Claude Code, Cursor, Windsurf, Codex).");
|
|
2675
|
+
console.log("Use --only <agent> to force: --only claude,cursor,windsurf,codex");
|
|
2676
|
+
}
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
if (!isMcp) {
|
|
2680
|
+
console.log("Usage: agent-memory serve --mcp");
|
|
2681
|
+
console.log(" agent-memory serve --register (register in all detected agents)");
|
|
2682
|
+
console.log(" agent-memory serve --register --only claude,cursor,windsurf,codex");
|
|
2683
|
+
console.log("");
|
|
2684
|
+
console.log("Supported agents: Claude Code, Cursor, Windsurf, Codex");
|
|
2685
|
+
console.log("Tip: run --register once, then restart the agent(s).");
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
const server = new StdioMcpServer(VERSION);
|
|
2689
|
+
// Core tools: free tier, available without Pro.
|
|
2690
|
+
server.addTool({
|
|
2691
|
+
name: "memory_read",
|
|
2692
|
+
description: "Read the current long-term memory (MEMORY.md) and scratchpad checklist.",
|
|
2693
|
+
inputSchema: { type: "object", properties: {} },
|
|
2694
|
+
}, async () => {
|
|
2695
|
+
const memFile = getMemoryFile();
|
|
2696
|
+
const scratchFile = getScratchpadFile();
|
|
2697
|
+
const memory = redactSecrets(readFileSafe(memFile) ?? "").content ||
|
|
2698
|
+
'(empty — this only covers what was explicitly saved. For things said in prior chat sessions, try `agent-memory recall "<query>"` or the `session_recall`/`session_search` MCP tools, if AgentMemory Pro is installed.)';
|
|
2699
|
+
const scratchRaw = readFileSafe(scratchFile) ?? "";
|
|
2700
|
+
const scratchItems = parseScratchpad(scratchRaw)
|
|
2701
|
+
.filter((item) => !item.done)
|
|
2702
|
+
.map((item) => `- [ ] ${redactSecrets(item.text).content}`);
|
|
2703
|
+
return [
|
|
2704
|
+
"## Long-term memory (MEMORY.md)",
|
|
2705
|
+
memory,
|
|
2706
|
+
"",
|
|
2707
|
+
"## Open scratchpad items",
|
|
2708
|
+
scratchItems.length ? scratchItems.join("\n") : "(none)",
|
|
2709
|
+
].join("\n");
|
|
2710
|
+
});
|
|
2711
|
+
server.addTool({
|
|
2712
|
+
name: "memory_write",
|
|
2713
|
+
description: "Append a new entry to memory. Use target='daily' for session notes, 'long_term' for durable facts.",
|
|
2714
|
+
inputSchema: {
|
|
2715
|
+
type: "object",
|
|
2716
|
+
properties: {
|
|
2717
|
+
content: { type: "string", description: "Text to store" },
|
|
2718
|
+
target: { type: "string", description: "Where to write", enum: ["daily", "long_term", "scratchpad"] },
|
|
2719
|
+
},
|
|
2720
|
+
required: ["content"],
|
|
2721
|
+
},
|
|
2722
|
+
}, async (input) => {
|
|
2723
|
+
const content = typeof input.content === "string" ? input.content.trim() : "";
|
|
2724
|
+
const target = typeof input.target === "string" ? input.target : "daily";
|
|
2725
|
+
if (!content)
|
|
2726
|
+
return "Error: content is required";
|
|
2727
|
+
if (target === "scratchpad") {
|
|
2728
|
+
const result = await scratchpadAction({ action: "add", text: content, sessionId: "mcp-serve" });
|
|
2729
|
+
return result.text;
|
|
2730
|
+
}
|
|
2731
|
+
await memoryWrite({ target: target, content, sessionId: "mcp-serve" });
|
|
2732
|
+
return `Written to ${target} memory.`;
|
|
2733
|
+
});
|
|
2734
|
+
// Load Pro plugin and let it register additional MCP tools + startup hooks.
|
|
2735
|
+
const runtime = new InstalledPluginRuntimeV1({ coreVersion: VERSION });
|
|
2736
|
+
try {
|
|
2737
|
+
await runtime.load();
|
|
2738
|
+
for (const tool of runtime.getMcpTools()) {
|
|
2739
|
+
server.addTool({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }, (input) => runtime.runMcpTool(tool.name, input));
|
|
2740
|
+
}
|
|
2741
|
+
server.addStartupHook(() => runtime.runMcpStartup());
|
|
2742
|
+
}
|
|
2743
|
+
catch {
|
|
2744
|
+
// Pro not installed or failed to load — serve with core tools only.
|
|
2745
|
+
}
|
|
2746
|
+
await server.start();
|
|
2747
|
+
}
|
|
2748
|
+
// ---------------------------------------------------------------------------
|
|
1051
2749
|
// Usage
|
|
1052
2750
|
// ---------------------------------------------------------------------------
|
|
1053
2751
|
function printUsage() {
|
|
1054
|
-
const
|
|
1055
|
-
|
|
2752
|
+
const groups = [
|
|
2753
|
+
["Do things", ["save", "note", "recall", "search"]],
|
|
2754
|
+
["See things", ["status", "doctor", "dashboard"]],
|
|
2755
|
+
["Advanced", ["write", "read", "context", "scratchpad", "distil", "sync"]],
|
|
2756
|
+
["Setup", ["setup", "install-skills", "install-hooks", "completion"]],
|
|
2757
|
+
["Pro", ["pro", "learn"]],
|
|
2758
|
+
];
|
|
2759
|
+
const knownGroupCommands = new Set(groups.flatMap(([, list]) => list));
|
|
2760
|
+
const otherCommands = COMMANDS.filter((command) => !knownGroupCommands.has(command));
|
|
2761
|
+
if (otherCommands.length > 0)
|
|
2762
|
+
groups.push(["Other", [...otherCommands]]);
|
|
2763
|
+
const allCommands = groups.flatMap(([, list]) => list);
|
|
2764
|
+
const width = Math.max(...allCommands.map((command) => command.length));
|
|
2765
|
+
const sections = groups
|
|
2766
|
+
.map(([title, list]) => {
|
|
2767
|
+
const rows = list
|
|
2768
|
+
.filter((command) => COMMAND_DESCRIPTIONS[command])
|
|
2769
|
+
.map((command) => {
|
|
2770
|
+
const description = COMMAND_DESCRIPTIONS[command];
|
|
2771
|
+
return ` ${command.padEnd(width)} ${description}`;
|
|
2772
|
+
})
|
|
2773
|
+
.join("\n");
|
|
2774
|
+
return `${title}:\n${rows}`;
|
|
2775
|
+
})
|
|
2776
|
+
.join("\n\n");
|
|
1056
2777
|
console.log(`agent-memory — persistent memory for coding agents
|
|
1057
2778
|
|
|
1058
2779
|
Usage:
|
|
1059
2780
|
agent-memory <command> [options]
|
|
1060
2781
|
|
|
1061
|
-
|
|
1062
|
-
${commandList}
|
|
2782
|
+
${sections}
|
|
1063
2783
|
|
|
1064
2784
|
Global flags:
|
|
1065
2785
|
--dir <path> Override memory directory
|
|
1066
2786
|
--json Machine-readable JSON output
|
|
1067
2787
|
|
|
1068
|
-
|
|
1069
|
-
agent-memory
|
|
1070
|
-
agent-memory
|
|
1071
|
-
agent-memory
|
|
1072
|
-
agent-memory
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
agent-memory read --target list
|
|
1076
|
-
agent-memory read --target topic --topic "auth"
|
|
1077
|
-
agent-memory read --target topics
|
|
1078
|
-
agent-memory scratchpad add --text "Review PR #42"
|
|
1079
|
-
agent-memory scratchpad list
|
|
1080
|
-
agent-memory scratchpad done --text "PR #42"
|
|
1081
|
-
agent-memory search --query "database choice" --mode keyword
|
|
1082
|
-
agent-memory distil --dry-run
|
|
1083
|
-
agent-memory context --query "database choice"
|
|
1084
|
-
agent-memory sync
|
|
1085
|
-
agent-memory status --json
|
|
1086
|
-
agent-memory completion zsh
|
|
1087
|
-
agent-memory install-hooks --yes
|
|
1088
|
-
agent-memory pro status
|
|
1089
|
-
agent-memory pro install
|
|
1090
|
-
agent-memory recall "what did we decide about authentication?"
|
|
1091
|
-
agent-memory dashboard`);
|
|
2788
|
+
Get started:
|
|
2789
|
+
agent-memory setup — one-shot idempotent installer (recommended)
|
|
2790
|
+
agent-memory save "your note" — quick save to today's log
|
|
2791
|
+
agent-memory recall "what did we decide about X?" — search prior sessions
|
|
2792
|
+
agent-memory status — one-page health readout
|
|
2793
|
+
|
|
2794
|
+
New here? Run: ${colorize("agent-memory setup", "cyan")}`);
|
|
1092
2795
|
}
|
|
1093
2796
|
// ---------------------------------------------------------------------------
|
|
1094
2797
|
// Main
|
|
1095
2798
|
// ---------------------------------------------------------------------------
|
|
2799
|
+
// Pre-parse content-taking aliases (`save`, `note`) before the general flag parser
|
|
2800
|
+
// consumes tokens that were meant to be literal user text. Extracts only known
|
|
2801
|
+
// infrastructure flags — `--json`, `--dir <path>`, and `--target <val>` for `save` —
|
|
2802
|
+
// and treats the rest of argv as the content string.
|
|
2803
|
+
function extractRawContentAlias(argv) {
|
|
2804
|
+
if (argv.length === 0)
|
|
2805
|
+
return null;
|
|
2806
|
+
const command = argv[0];
|
|
2807
|
+
if (command !== "save" && command !== "note")
|
|
2808
|
+
return null;
|
|
2809
|
+
// Never treat reserved flags as content — they must fall through to the normal
|
|
2810
|
+
// parser so `save --help` renders help and `save --json` sets JSON mode without
|
|
2811
|
+
// accidentally saving "--help" or "--json" as a daily-log entry.
|
|
2812
|
+
if (argv.slice(1).some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-V")) {
|
|
2813
|
+
return null;
|
|
2814
|
+
}
|
|
2815
|
+
const flags = {};
|
|
2816
|
+
const contentParts = [];
|
|
2817
|
+
const validValueFlags = new Set(["--dir", "--target"]);
|
|
2818
|
+
for (let i = 1; i < argv.length; i++) {
|
|
2819
|
+
const arg = argv[i];
|
|
2820
|
+
if (arg === "--json") {
|
|
2821
|
+
flags.json = true;
|
|
2822
|
+
continue;
|
|
2823
|
+
}
|
|
2824
|
+
if (validValueFlags.has(arg) && i + 1 < argv.length) {
|
|
2825
|
+
flags[arg.slice(2)] = argv[i + 1];
|
|
2826
|
+
i++;
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
contentParts.push(arg);
|
|
2830
|
+
}
|
|
2831
|
+
return { command, content: contentParts.join(" ").trim(), flags };
|
|
2832
|
+
}
|
|
1096
2833
|
async function main() {
|
|
1097
|
-
const
|
|
2834
|
+
const rawArgv = process.argv.slice(2);
|
|
2835
|
+
const rawAlias = extractRawContentAlias(rawArgv);
|
|
2836
|
+
const { command, flags, positional } = rawAlias
|
|
2837
|
+
? { command: rawAlias.command, flags: rawAlias.flags, positional: [rawAlias.content].filter(Boolean) }
|
|
2838
|
+
: parseArgs(rawArgv);
|
|
1098
2839
|
const json = hasFlag(flags, "json");
|
|
2840
|
+
// Reject unknown commands / flags with a helpful "did you mean" hint.
|
|
2841
|
+
// Plugin-provided commands fall through and are validated by the runtime.
|
|
2842
|
+
validateCommand(command);
|
|
2843
|
+
validateFlags(command, positional, flags);
|
|
1099
2844
|
// Apply --dir override
|
|
1100
2845
|
const dir = getFlag(flags, "dir");
|
|
1101
2846
|
if (dir) {
|
|
1102
2847
|
_setBaseDir(dir);
|
|
1103
2848
|
}
|
|
2849
|
+
// First-run detection: nudge new users toward `init` before they run anything
|
|
2850
|
+
// destructive/creative. Gated on the memory *directory* (not MEMORY.md) so a
|
|
2851
|
+
// user who intentionally cleared their memory files doesn't get the wizard on
|
|
2852
|
+
// every subsequent command. TTY-only; skip introspection commands.
|
|
2853
|
+
if (process.stdin.isTTY &&
|
|
2854
|
+
process.stdout.isTTY &&
|
|
2855
|
+
!json &&
|
|
2856
|
+
command &&
|
|
2857
|
+
!["init", "help", "version", "doctor", "status", "completion", "hook", "serve"].includes(command) &&
|
|
2858
|
+
!fs.existsSync(getMemoryDir())) {
|
|
2859
|
+
console.log(colorize("It looks like this is your first run — no memory directory yet.", "yellow"));
|
|
2860
|
+
if (await promptYesNo("Run agent-memory init to set things up?", true)) {
|
|
2861
|
+
await cmdInit({});
|
|
2862
|
+
console.log("");
|
|
2863
|
+
console.log(colorize(`Now continuing with: agent-memory ${command}`, "dim"));
|
|
2864
|
+
console.log("");
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
1104
2867
|
if (command === "version" || hasFlag(flags, "version")) {
|
|
1105
2868
|
output(json ? { version: VERSION } : VERSION, json);
|
|
1106
2869
|
return;
|
|
1107
2870
|
}
|
|
1108
|
-
|
|
2871
|
+
// Per-command help: `agent-memory <cmd> --help` shows only that command's usage,
|
|
2872
|
+
// flags, and examples. Falls back to the top-level overview when no command was
|
|
2873
|
+
// specified. Plugin subcommands render their own help via the plugin runtime.
|
|
2874
|
+
if (command === "help") {
|
|
2875
|
+
const target = positional[0];
|
|
2876
|
+
if (target) {
|
|
2877
|
+
console.log(renderCommandHelp(target));
|
|
2878
|
+
}
|
|
2879
|
+
else {
|
|
2880
|
+
printUsage();
|
|
2881
|
+
}
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
if (hasFlag(flags, "help") && command !== "plugin") {
|
|
2885
|
+
if (!command) {
|
|
2886
|
+
printUsage();
|
|
2887
|
+
}
|
|
2888
|
+
else {
|
|
2889
|
+
console.log(renderCommandHelp(command));
|
|
2890
|
+
}
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
if (!command) {
|
|
1109
2894
|
printUsage();
|
|
1110
2895
|
return;
|
|
1111
2896
|
}
|
|
@@ -1113,9 +2898,28 @@ async function main() {
|
|
|
1113
2898
|
case "context":
|
|
1114
2899
|
await cmdContext(flags);
|
|
1115
2900
|
break;
|
|
1116
|
-
case "write":
|
|
1117
|
-
|
|
2901
|
+
case "write": {
|
|
2902
|
+
// Accept content as the first positional so `write "text"` mirrors
|
|
2903
|
+
// `save "text"` / `note "text"`. `--content` still works for scripts
|
|
2904
|
+
// and existing callers.
|
|
2905
|
+
const positionalContent = positional[0];
|
|
2906
|
+
await cmdWrite(positionalContent && !getFlag(flags, "content") ? { ...flags, content: positionalContent } : flags);
|
|
2907
|
+
break;
|
|
2908
|
+
}
|
|
2909
|
+
case "save": {
|
|
2910
|
+
const text = positional[0] ?? "";
|
|
2911
|
+
if (!text)
|
|
2912
|
+
exitError('save requires content: agent-memory save "your note here"', json);
|
|
2913
|
+
await cmdWrite({ ...flags, content: text, target: getFlag(flags, "target") ?? "daily" });
|
|
2914
|
+
break;
|
|
2915
|
+
}
|
|
2916
|
+
case "note": {
|
|
2917
|
+
const text = positional[0] ?? "";
|
|
2918
|
+
if (!text)
|
|
2919
|
+
exitError('note requires text: agent-memory note "your item"', json);
|
|
2920
|
+
await cmdScratchpad({ ...flags, text }, ["add"]);
|
|
1118
2921
|
break;
|
|
2922
|
+
}
|
|
1119
2923
|
case "read":
|
|
1120
2924
|
await cmdRead(flags);
|
|
1121
2925
|
break;
|
|
@@ -1141,9 +2945,18 @@ async function main() {
|
|
|
1141
2945
|
case "init":
|
|
1142
2946
|
await cmdInit(flags);
|
|
1143
2947
|
break;
|
|
2948
|
+
case "setup":
|
|
2949
|
+
await cmdSetup(flags);
|
|
2950
|
+
break;
|
|
1144
2951
|
case "status":
|
|
1145
2952
|
await cmdStatus(flags);
|
|
1146
2953
|
break;
|
|
2954
|
+
case "doctor":
|
|
2955
|
+
await cmdDoctor(flags);
|
|
2956
|
+
break;
|
|
2957
|
+
case "tutorial":
|
|
2958
|
+
await cmdTutorial(flags);
|
|
2959
|
+
break;
|
|
1147
2960
|
case "completion":
|
|
1148
2961
|
cmdCompletion(flags, positional);
|
|
1149
2962
|
break;
|
|
@@ -1154,23 +2967,70 @@ async function main() {
|
|
|
1154
2967
|
cmdUninstallHooks(flags);
|
|
1155
2968
|
break;
|
|
1156
2969
|
case "hook": {
|
|
1157
|
-
|
|
1158
|
-
|
|
2970
|
+
const sub = positional[0];
|
|
2971
|
+
if (sub !== "session-start" && sub !== "user-prompt-submit" && sub !== "stop") {
|
|
2972
|
+
exitError("hook requires 'session-start', 'user-prompt-submit', or 'stop'", json);
|
|
2973
|
+
}
|
|
1159
2974
|
const agent = getFlag(flags, "agent");
|
|
1160
2975
|
if (!agent)
|
|
1161
|
-
exitError(
|
|
1162
|
-
|
|
2976
|
+
exitError(`hook ${sub} requires --agent`, json);
|
|
2977
|
+
if (sub === "user-prompt-submit") {
|
|
2978
|
+
await cmdUserPromptSubmit(flags);
|
|
2979
|
+
break;
|
|
2980
|
+
}
|
|
2981
|
+
if (sub === "stop") {
|
|
2982
|
+
await cmdStop(flags);
|
|
2983
|
+
break;
|
|
2984
|
+
}
|
|
2985
|
+
// In per-turn mode, SessionStart emits only the stable layer (MEMORY.md + scratchpad).
|
|
2986
|
+
// The dynamic layer (daily logs + search) is emitted per-turn by UserPromptSubmit.
|
|
2987
|
+
// In stable mode, SessionStart emits the full context (current behavior).
|
|
2988
|
+
const layer = readHookMode() === "per-turn" ? "stable" : undefined;
|
|
2989
|
+
await cmdContext(layer ? { "no-search": true, layer } : { "no-search": true });
|
|
2990
|
+
try {
|
|
2991
|
+
const cache = readUpgradeCache();
|
|
2992
|
+
if (cache) {
|
|
2993
|
+
const status = await checkForUpgrades({
|
|
2994
|
+
cliCurrent: VERSION,
|
|
2995
|
+
pluginCurrent: cache.pluginCurrent,
|
|
2996
|
+
cacheOnly: true,
|
|
2997
|
+
});
|
|
2998
|
+
const notice = formatUpgradeNotice(status);
|
|
2999
|
+
if (notice)
|
|
3000
|
+
console.error(notice);
|
|
3001
|
+
}
|
|
3002
|
+
if (!isCacheFresh(cache) && !process.env.AGENT_MEMORY_UPGRADE_BACKGROUND) {
|
|
3003
|
+
refreshUpgradeCacheBackground();
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
catch {
|
|
3007
|
+
// Passive upgrade notice must never break session start.
|
|
3008
|
+
}
|
|
1163
3009
|
try {
|
|
1164
3010
|
const decision = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).runSessionStart({
|
|
1165
3011
|
host: agent,
|
|
1166
3012
|
cwd: process.cwd(),
|
|
1167
3013
|
signal: new AbortController().signal,
|
|
1168
3014
|
});
|
|
1169
|
-
if (decision
|
|
1170
|
-
|
|
3015
|
+
if (decision) {
|
|
3016
|
+
cacheProUsage({
|
|
3017
|
+
used: decision.used,
|
|
3018
|
+
limit: decision.limit,
|
|
3019
|
+
remaining: decision.remaining,
|
|
3020
|
+
resetAt: decision.resetAt,
|
|
3021
|
+
state: decision.state,
|
|
3022
|
+
capability: "session",
|
|
3023
|
+
});
|
|
3024
|
+
if (decision.state === "exhausted") {
|
|
3025
|
+
console.error(`AgentMemory free session allowance resets in ${formatResetTime(decision.resetAt)}. Upgrade: ${UPGRADE_URL}`);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
1171
3028
|
}
|
|
1172
|
-
catch {
|
|
1173
|
-
// Paid SessionStart work must never make public-core context unavailable
|
|
3029
|
+
catch (error) {
|
|
3030
|
+
// Paid SessionStart work must never make public-core context unavailable,
|
|
3031
|
+
// but a completely silent failure hides broken installs from users and `doctor`.
|
|
3032
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3033
|
+
console.error(`AgentMemory Pro session hook failed: ${message}`);
|
|
1174
3034
|
}
|
|
1175
3035
|
break;
|
|
1176
3036
|
}
|
|
@@ -1180,12 +3040,35 @@ async function main() {
|
|
|
1180
3040
|
case "pro":
|
|
1181
3041
|
await cmdPro(flags, positional);
|
|
1182
3042
|
break;
|
|
3043
|
+
case "upgrade":
|
|
3044
|
+
await cmdUpgrade(flags);
|
|
3045
|
+
break;
|
|
3046
|
+
case "serve":
|
|
3047
|
+
await cmdServe(flags);
|
|
3048
|
+
break;
|
|
1183
3049
|
default: {
|
|
1184
3050
|
const controller = new AbortController();
|
|
1185
3051
|
const abort = () => controller.abort();
|
|
1186
3052
|
process.once("SIGINT", abort);
|
|
1187
3053
|
try {
|
|
1188
3054
|
const pluginCommand = command === "dashboard" ? "web" : command;
|
|
3055
|
+
// Dev-only: AGENT_MEMORY_FORCE_QUOTA_EXHAUSTED=recall (or "learn") injects an
|
|
3056
|
+
// exhaustion result so the upgrade-prompt flow can be tested locally without
|
|
3057
|
+
// burning through real server quota.
|
|
3058
|
+
const forceExhausted = process.env.AGENT_MEMORY_FORCE_QUOTA_EXHAUSTED;
|
|
3059
|
+
if (!json && forceExhausted && forceExhausted.split(",").includes(command)) {
|
|
3060
|
+
const resetAt = new Date(Date.now() + 18 * 3_600_000).toISOString();
|
|
3061
|
+
const forced = {
|
|
3062
|
+
code: "quota_exceeded",
|
|
3063
|
+
used: 20,
|
|
3064
|
+
limit: 20,
|
|
3065
|
+
remaining: 0,
|
|
3066
|
+
resetAt,
|
|
3067
|
+
};
|
|
3068
|
+
cacheProUsage({ used: 20, limit: 20, remaining: 0, resetAt, state: "exhausted", capability: command });
|
|
3069
|
+
printCapExhaustedBox(command, forced);
|
|
3070
|
+
process.exit(1);
|
|
3071
|
+
}
|
|
1189
3072
|
const result = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).run(pluginCommand, {
|
|
1190
3073
|
args: positional,
|
|
1191
3074
|
flags,
|
|
@@ -1193,8 +3076,22 @@ async function main() {
|
|
|
1193
3076
|
});
|
|
1194
3077
|
if (!result)
|
|
1195
3078
|
exitError(`Unknown command: ${command}. Run 'agent-memory help' for usage.`, json);
|
|
1196
|
-
if (!result.ok)
|
|
3079
|
+
if (!result.ok) {
|
|
3080
|
+
const capInfo = detectCapExhausted(result);
|
|
3081
|
+
if (capInfo && !json) {
|
|
3082
|
+
cacheProUsage({
|
|
3083
|
+
used: capInfo.used,
|
|
3084
|
+
limit: capInfo.limit,
|
|
3085
|
+
remaining: capInfo.remaining ?? 0,
|
|
3086
|
+
resetAt: capInfo.resetAt,
|
|
3087
|
+
state: "exhausted",
|
|
3088
|
+
capability: command,
|
|
3089
|
+
});
|
|
3090
|
+
printCapExhaustedBox(command, capInfo);
|
|
3091
|
+
process.exit(1);
|
|
3092
|
+
}
|
|
1197
3093
|
exitError(result.error?.message ?? `Plugin command ${pluginCommand} failed`, json);
|
|
3094
|
+
}
|
|
1198
3095
|
output(result.data ?? { ok: true }, json);
|
|
1199
3096
|
}
|
|
1200
3097
|
finally {
|