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.
@@ -0,0 +1,106 @@
1
+ import * as readline from "node:readline";
2
+ // ---------------------------------------------------------------------------
3
+ // Bare stdio MCP server — no external dependencies
4
+ //
5
+ // MCP uses newline-delimited JSON-RPC 2.0 over stdin/stdout (NOT Content-Length
6
+ // framing). Each message is one JSON object followed by \n.
7
+ // ---------------------------------------------------------------------------
8
+ export class StdioMcpServer {
9
+ version;
10
+ tools = new Map();
11
+ startupHooks = [];
12
+ constructor(version = "0.0.0") {
13
+ this.version = version;
14
+ }
15
+ addTool(definition, handler) {
16
+ this.tools.set(definition.name, { definition, handler });
17
+ }
18
+ addStartupHook(fn) {
19
+ this.startupHooks.push(fn);
20
+ }
21
+ async start() {
22
+ // Run all startup hooks before entering the message loop.
23
+ for (const hook of this.startupHooks)
24
+ await hook();
25
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
26
+ rl.on("line", (line) => {
27
+ const trimmed = line.trim();
28
+ if (!trimmed)
29
+ return;
30
+ let message;
31
+ try {
32
+ message = JSON.parse(trimmed);
33
+ }
34
+ catch {
35
+ return;
36
+ }
37
+ this.handleMessage(message);
38
+ });
39
+ await new Promise((resolve) => {
40
+ rl.on("close", resolve);
41
+ process.stdin.on("end", resolve);
42
+ });
43
+ }
44
+ handleMessage(msg) {
45
+ const id = msg.id;
46
+ const method = typeof msg.method === "string" ? msg.method : "";
47
+ // Notifications (no id) — no response needed.
48
+ if (id === undefined || id === null) {
49
+ // notifications/initialized is the only one we care about; ignore the rest.
50
+ return;
51
+ }
52
+ if (method === "initialize") {
53
+ this.respond(id, {
54
+ protocolVersion: "2024-11-05",
55
+ capabilities: { tools: {} },
56
+ serverInfo: { name: "agent-memory", version: this.version },
57
+ });
58
+ return;
59
+ }
60
+ if (method === "ping") {
61
+ this.respond(id, {});
62
+ return;
63
+ }
64
+ if (method === "tools/list") {
65
+ this.respond(id, {
66
+ tools: [...this.tools.values()].map(({ definition }) => definition),
67
+ });
68
+ return;
69
+ }
70
+ if (method === "tools/call") {
71
+ const params = (msg.params ?? {});
72
+ const toolName = typeof params.name === "string" ? params.name : "";
73
+ const toolInput = (params.arguments ?? {});
74
+ const entry = this.tools.get(toolName);
75
+ if (!entry) {
76
+ this.respondError(id, -32601, `Unknown tool: ${toolName}`);
77
+ return;
78
+ }
79
+ // Execute handler — handle both sync and async.
80
+ Promise.resolve()
81
+ .then(() => entry.handler(toolInput))
82
+ .then((result) => {
83
+ this.respond(id, {
84
+ content: [
85
+ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) },
86
+ ],
87
+ });
88
+ })
89
+ .catch((error) => {
90
+ this.respond(id, {
91
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
92
+ isError: true,
93
+ });
94
+ });
95
+ return;
96
+ }
97
+ // Unknown method — JSON-RPC method not found.
98
+ this.respondError(id, -32601, `Method not found: ${method}`);
99
+ }
100
+ respond(id, result) {
101
+ process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id, result })}\n`);
102
+ }
103
+ respondError(id, code, message) {
104
+ process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } })}\n`);
105
+ }
106
+ }
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { AGENT_MEMORY_PLUGIN_API_VERSION, isSafeBundlePath, validateBundleManifestV1, } from "./plugin-host.js";
6
- import { TemporaryPluginBackend } from "./plugin-service.js";
6
+ import { AgentMemoryServiceBackend } from "./plugin-service.js";
7
7
  export const OFFICIAL_BUNDLE_ID = "agentmemory.pro";
8
8
  export const OFFICIAL_PLUGIN_IDS = ["agentmemory.session-intelligence", "agentmemory.web-console"];
9
9
  const MISSING_ENTITLEMENT = {
@@ -20,7 +20,7 @@ const OFFICIAL_PLUGINS = [
20
20
  const PACKAGE_MAX_BYTES = 64 * 1024 * 1024;
21
21
  const PACKAGE_MAX_EXPANDED_BYTES = 128 * 1024 * 1024;
22
22
  const PACKAGE_MAX_FILES = 10_000;
23
- const TEMPORARY_RELEASE_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
23
+ const RELEASE_SIGNING_KEY_2026_08 = `-----BEGIN PUBLIC KEY-----
24
24
  MCowBQYDK2VwAyEASefZFUVFy1EmvGbd0ckHZThmPgqQ3u9HCwZRReAZQW8=
25
25
  -----END PUBLIC KEY-----`;
26
26
  export class PluginBootstrapFailure extends Error {
@@ -423,11 +423,11 @@ export class PluginBootstrapV1 {
423
423
  }
424
424
  export function createDefaultPluginBootstrap(coreVersion) {
425
425
  const store = new FilePluginInstallStore();
426
- const backend = new TemporaryPluginBackend({ root: store.root, coreVersion });
426
+ const backend = new AgentMemoryServiceBackend({ root: store.root, coreVersion });
427
427
  return new PluginBootstrapV1({
428
428
  coreVersion,
429
429
  backend,
430
- verifier: new Ed25519ReleaseVerifier({ "agentmemory-temporary-2026-08": TEMPORARY_RELEASE_PUBLIC_KEY }),
430
+ verifier: new Ed25519ReleaseVerifier({ "agentmemory-temporary-2026-08": RELEASE_SIGNING_KEY_2026_08 }),
431
431
  store,
432
432
  healthCheck: async (directory, release) => {
433
433
  const { createInstalledBundleHealthCheck } = await import("./plugin-runtime.js");
@@ -73,6 +73,21 @@ export interface PluginSessionStartHookV1 {
73
73
  requiredCapability: string;
74
74
  run(context: PluginSessionStartContextV1): Promise<void>;
75
75
  }
76
+ export interface PluginBackgroundRefreshContextV1 {
77
+ host: string;
78
+ cwd?: string;
79
+ signal: AbortSignal;
80
+ }
81
+ /**
82
+ * Un-metered per-turn refresh callback. Fired on every UserPromptSubmit so
83
+ * plugins can keep background workers alive across `/clear` and long idle.
84
+ * MUST be idempotent and cheap — no quota is charged.
85
+ */
86
+ export interface PluginBackgroundRefreshHookV1 {
87
+ name: string;
88
+ requiredCapability: string;
89
+ run(context: PluginBackgroundRefreshContextV1): Promise<void>;
90
+ }
76
91
  export interface PluginMemoryWriteV1 {
77
92
  target: "long_term" | "daily" | "topic";
78
93
  content: string;
@@ -120,12 +135,31 @@ export interface PluginContextProviderV1 {
120
135
  signal: AbortSignal;
121
136
  }): Promise<PluginContextSectionV1[]>;
122
137
  }
138
+ export interface PluginMcpToolInputSchema {
139
+ type: "object";
140
+ properties: Record<string, {
141
+ type: string;
142
+ description?: string;
143
+ enum?: string[];
144
+ }>;
145
+ required?: string[];
146
+ }
147
+ export interface PluginMcpToolV1 {
148
+ name: string;
149
+ description: string;
150
+ requiredCapability: string;
151
+ inputSchema: PluginMcpToolInputSchema;
152
+ run(input: Record<string, unknown>): unknown | Promise<unknown>;
153
+ }
123
154
  export interface AgentMemoryPluginHostV1 {
124
155
  apiVersion: 1;
125
156
  coreVersion: string;
126
157
  registerCommand(command: PluginCommandV1): void;
127
158
  registerSessionStartHook(hook: PluginSessionStartHookV1): void;
159
+ registerBackgroundRefresh?(hook: PluginBackgroundRefreshHookV1): void;
128
160
  registerContextProvider?(provider: PluginContextProviderV1): void;
161
+ registerMcpTool?(tool: PluginMcpToolV1): void;
162
+ registerMcpStartup?(fn: () => void | Promise<void>): void;
129
163
  getStateDirectory(): string;
130
164
  getMemoryDirectory(): string;
131
165
  getEntitlement(): Promise<PluginEntitlementStatusV1>;
@@ -1,5 +1,5 @@
1
1
  import { type PluginBootstrapBackendV1, type PluginInstallStoreV1, type PluginSessionUsageDecisionV1, type SignedPluginReleaseV1 } from "./plugin-bootstrap.js";
2
- import { type PluginCommandContextV1, type PluginCommandResultV1, type PluginContextSectionV1, type PluginSessionStartHookV1 } from "./plugin-host.js";
2
+ import { type PluginBackgroundRefreshContextV1, type PluginCommandContextV1, type PluginCommandResultV1, type PluginContextSectionV1, type PluginMcpToolV1, type PluginSessionStartHookV1 } from "./plugin-host.js";
3
3
  export interface PluginRuntimeOptionsV1 {
4
4
  coreVersion: string;
5
5
  store?: PluginInstallStoreV1;
@@ -11,18 +11,39 @@ export declare class InstalledPluginRuntimeV1 {
11
11
  private readonly backend;
12
12
  private readonly commands;
13
13
  private readonly hooks;
14
+ private readonly backgroundRefreshHooks;
14
15
  private readonly contextProviders;
16
+ private readonly mcpTools;
17
+ private readonly mcpStartupHooks;
15
18
  private loaded;
16
19
  constructor(options: PluginRuntimeOptionsV1);
17
20
  load(): Promise<boolean>;
18
21
  run(name: string, context: PluginCommandContextV1): Promise<PluginCommandResultV1 | null>;
19
22
  runSessionStart(context: Parameters<PluginSessionStartHookV1["run"]>[0]): Promise<PluginSessionUsageDecisionV1 | null>;
23
+ /**
24
+ * Fire all registered background-refresh hooks. Un-metered — intended for
25
+ * per-turn UserPromptSubmit invocation, keeping workers alive across
26
+ * `/clear` and idle without charging session quota. Failures propagate to
27
+ * the caller (which is expected to swallow them silently).
28
+ */
29
+ refreshBackgroundWorkers(context: PluginBackgroundRefreshContextV1): Promise<void>;
20
30
  provideContext(context: {
21
31
  host: string;
22
32
  cwd?: string;
23
33
  query?: string;
24
34
  signal: AbortSignal;
25
35
  }): Promise<PluginContextSectionV1[]>;
36
+ getMcpTools(): PluginMcpToolV1[];
37
+ /**
38
+ * Invoke a registered MCP tool by name, re-checking entitlement on every
39
+ * call — matching `run()`'s behavior for commands. MCP tools are
40
+ * registered once at `serve --mcp` startup and the server process can
41
+ * live for a long session, so a tool's entitlement must be re-verified
42
+ * per-call rather than trusted from registration time (e.g. a trial
43
+ * expiring mid-session must actually stop the tool from working).
44
+ */
45
+ runMcpTool(name: string, input: Record<string, unknown>): Promise<unknown>;
46
+ runMcpStartup(): Promise<void>;
26
47
  private createHost;
27
48
  private refreshEntitlement;
28
49
  }
@@ -5,7 +5,7 @@ import { pathToFileURL } from "node:url";
5
5
  import { getMemoryDir, memoryWrite, redactSecrets, scheduleQmdUpdate } from "./core.js";
6
6
  import { FilePluginInstallStore, OFFICIAL_BUNDLE_ID, PluginBootstrapFailure, } from "./plugin-bootstrap.js";
7
7
  import { AGENT_MEMORY_PLUGIN_API_VERSION, isPluginCapabilityEnabled, validateBundleManifestV1, validatePluginEntitlementStatusV1, validatePluginManifestV1, } from "./plugin-host.js";
8
- import { TemporaryPluginBackend } from "./plugin-service.js";
8
+ import { AgentMemoryServiceBackend } from "./plugin-service.js";
9
9
  function assertPermission(manifest, permission) {
10
10
  if (!manifest.permissions.includes(permission))
11
11
  throw new PluginBootstrapFailure("plugin_permission_denied", `Plugin ${manifest.id} did not declare permission ${permission}`);
@@ -61,12 +61,15 @@ export class InstalledPluginRuntimeV1 {
61
61
  backend;
62
62
  commands = new Map();
63
63
  hooks = [];
64
+ backgroundRefreshHooks = [];
64
65
  contextProviders = [];
66
+ mcpTools = [];
67
+ mcpStartupHooks = [];
65
68
  loaded = false;
66
69
  constructor(options) {
67
70
  this.options = options;
68
71
  this.store = options.store ?? new FilePluginInstallStore();
69
- this.backend = options.backend ?? new TemporaryPluginBackend({ root: this.store.root });
72
+ this.backend = options.backend ?? new AgentMemoryServiceBackend({ root: this.store.root });
70
73
  }
71
74
  async load() {
72
75
  if (this.loaded)
@@ -133,6 +136,22 @@ export class InstalledPluginRuntimeV1 {
133
136
  throw error;
134
137
  }
135
138
  }
139
+ /**
140
+ * Fire all registered background-refresh hooks. Un-metered — intended for
141
+ * per-turn UserPromptSubmit invocation, keeping workers alive across
142
+ * `/clear` and idle without charging session quota. Failures propagate to
143
+ * the caller (which is expected to swallow them silently).
144
+ */
145
+ async refreshBackgroundWorkers(context) {
146
+ if (!(await this.load()) || this.backgroundRefreshHooks.length === 0)
147
+ return;
148
+ const entitlement = await this.refreshEntitlement();
149
+ for (const hook of this.backgroundRefreshHooks) {
150
+ if (!isPluginCapabilityEnabled(entitlement, hook.requiredCapability))
151
+ continue;
152
+ await hook.run(context);
153
+ }
154
+ }
136
155
  async provideContext(context) {
137
156
  if (!(await this.load()) || this.contextProviders.length === 0)
138
157
  return [];
@@ -164,6 +183,32 @@ export class InstalledPluginRuntimeV1 {
164
183
  }
165
184
  return sections;
166
185
  }
186
+ getMcpTools() {
187
+ return [...this.mcpTools];
188
+ }
189
+ /**
190
+ * Invoke a registered MCP tool by name, re-checking entitlement on every
191
+ * call — matching `run()`'s behavior for commands. MCP tools are
192
+ * registered once at `serve --mcp` startup and the server process can
193
+ * live for a long session, so a tool's entitlement must be re-verified
194
+ * per-call rather than trusted from registration time (e.g. a trial
195
+ * expiring mid-session must actually stop the tool from working).
196
+ */
197
+ async runMcpTool(name, input) {
198
+ if (!(await this.load()))
199
+ return { error: `Unknown MCP tool: ${name}` };
200
+ const tool = this.mcpTools.find((candidate) => candidate.name === name);
201
+ if (!tool)
202
+ return { error: `Unknown MCP tool: ${name}` };
203
+ const entitlement = await this.refreshEntitlement();
204
+ if (!isPluginCapabilityEnabled(entitlement, tool.requiredCapability))
205
+ return { error: `Capability ${tool.requiredCapability} is not enabled for the ${name} tool` };
206
+ return tool.run(input);
207
+ }
208
+ async runMcpStartup() {
209
+ for (const hook of this.mcpStartupHooks)
210
+ await hook();
211
+ }
167
212
  createHost(manifest) {
168
213
  const descriptors = new Map(manifest.commands.map((command) => [command.name, command]));
169
214
  const stateRoot = path.join(this.store.root, "state");
@@ -197,6 +242,13 @@ export class InstalledPluginRuntimeV1 {
197
242
  throw new PluginBootstrapFailure("plugin_hook_invalid", `Plugin ${manifest.id} registered a hook with an undeclared capability`);
198
243
  this.hooks.push(hook);
199
244
  },
245
+ registerBackgroundRefresh: (hook) => {
246
+ if (!(manifest.capabilities ?? []).includes(hook.requiredCapability))
247
+ throw new PluginBootstrapFailure("plugin_hook_invalid", `Plugin ${manifest.id} registered a background refresh hook with an undeclared capability`);
248
+ if (!hook.name || this.backgroundRefreshHooks.some((existing) => existing.name === hook.name))
249
+ throw new PluginBootstrapFailure("plugin_hook_invalid", `Plugin background refresh hook ${hook.name || "(unnamed)"} is invalid or already registered`);
250
+ this.backgroundRefreshHooks.push(hook);
251
+ },
200
252
  registerContextProvider: (provider) => {
201
253
  if (!(manifest.capabilities ?? []).includes(provider.requiredCapability))
202
254
  throw new PluginBootstrapFailure("plugin_context_invalid", `Plugin ${manifest.id} registered a context provider with an undeclared capability`);
@@ -204,6 +256,16 @@ export class InstalledPluginRuntimeV1 {
204
256
  throw new PluginBootstrapFailure("plugin_context_invalid", `Plugin context provider ${provider.name || "(unnamed)"} is invalid or already registered`);
205
257
  this.contextProviders.push({ provider, pluginId: manifest.id });
206
258
  },
259
+ registerMcpTool: (tool) => {
260
+ if (!(manifest.capabilities ?? []).includes(tool.requiredCapability))
261
+ throw new PluginBootstrapFailure("plugin_mcp_tool_invalid", `Plugin ${manifest.id} registered an MCP tool with an undeclared capability`);
262
+ if (!tool.name || this.mcpTools.some((existing) => existing.name === tool.name))
263
+ throw new PluginBootstrapFailure("plugin_mcp_tool_invalid", `Plugin MCP tool ${tool.name || "(unnamed)"} is invalid or already registered`);
264
+ this.mcpTools.push(tool);
265
+ },
266
+ registerMcpStartup: (fn) => {
267
+ this.mcpStartupHooks.push(fn);
268
+ },
207
269
  getStateDirectory: () => stateDirectory,
208
270
  getMemoryDirectory: () => {
209
271
  assertPermission(manifest, "memory:read");
@@ -254,6 +316,7 @@ export function createInstalledBundleHealthCheck(coreVersion, backend, storeRoot
254
316
  coreVersion,
255
317
  registerCommand() { },
256
318
  registerSessionStartHook() { },
319
+ registerBackgroundRefresh() { },
257
320
  getStateDirectory: () => stateDirectory,
258
321
  getMemoryDirectory: () => {
259
322
  assertPermission(plugin.manifest, "memory:read");
@@ -1,6 +1,6 @@
1
1
  import { type PluginAccessDecisionV1, type PluginBootstrapBackendV1, type PluginNextActionV1, type PluginSessionUsageDecisionV1, type SignedPluginReleaseV1 } from "./plugin-bootstrap.js";
2
2
  import { type PluginEntitlementStatusV1 } from "./plugin-host.js";
3
- interface TemporaryPluginBackendOptions {
3
+ interface AgentMemoryServiceBackendOptions {
4
4
  root?: string;
5
5
  coreVersion?: string;
6
6
  apiOrigin?: string;
@@ -10,8 +10,8 @@ interface TemporaryPluginBackendOptions {
10
10
  activate?: () => Promise<string>;
11
11
  }
12
12
  export declare function openLoopbackUrl(url: string): boolean;
13
- export declare function collectTemporaryActivation(openUrl?: (url: string) => boolean): Promise<string>;
14
- export declare class TemporaryPluginBackend implements PluginBootstrapBackendV1 {
13
+ export declare function collectActivation(openUrl?: (url: string) => boolean): Promise<string>;
14
+ export declare class AgentMemoryServiceBackend implements PluginBootstrapBackendV1 {
15
15
  private readonly root;
16
16
  private readonly coreVersion;
17
17
  private readonly apiOrigin;
@@ -19,7 +19,7 @@ export declare class TemporaryPluginBackend implements PluginBootstrapBackendV1
19
19
  private readonly fetchImplementation;
20
20
  private readonly openUrl;
21
21
  private readonly activate;
22
- constructor(options?: TemporaryPluginBackendOptions);
22
+ constructor(options?: AgentMemoryServiceBackendOptions);
23
23
  getLocalEntitlement(): Promise<PluginEntitlementStatusV1>;
24
24
  resolveAccess(request: {
25
25
  bundleId: string;
@@ -46,4 +46,10 @@ export declare class TemporaryPluginBackend implements PluginBootstrapBackendV1
46
46
  private sessionUsage;
47
47
  private request;
48
48
  }
49
+ export declare class TemporaryPluginBackend extends AgentMemoryServiceBackend {
50
+ constructor(options: {
51
+ root: string;
52
+ coreVersion: string;
53
+ });
54
+ }
49
55
  export {};
@@ -7,7 +7,8 @@ import { getDefaultPluginInstallRoot, PluginBootstrapFailure, } from "./plugin-b
7
7
  import { validatePluginEntitlementStatusV1 } from "./plugin-host.js";
8
8
  const API_ORIGIN = "https://api.agentmemory.paperpilot.me";
9
9
  const ARTIFACT_ORIGIN = "https://plugins.agentmemory.paperpilot.me";
10
- const ACTIVATION_FILE = "credentials/temporary-access.json";
10
+ const ACTIVATION_FILE = "credentials/activation.json";
11
+ const ACTIVATION_FILE_LEGACY = "credentials/temporary-access.json";
11
12
  const REQUEST_TIMEOUT_MS = 30_000;
12
13
  const EMAIL_MAX_BYTES = 254;
13
14
  const FORM_MAX_BYTES = 2_048;
@@ -30,15 +31,33 @@ function freeEntitlement() {
30
31
  features: ["session-intelligence", "web-console"],
31
32
  capabilities: {
32
33
  "session-index": { enabled: true },
33
- recall: { enabled: true, quota: { limit: 10, window: "day", scope: "device" } },
34
+ recall: { enabled: true, quota: { limit: 20, window: "day", scope: "device" } },
34
35
  "session-worker": { enabled: false },
35
- learning: { enabled: true, quota: { limit: 1, window: "day", scope: "device" } },
36
+ learning: { enabled: true, quota: { limit: 5, window: "day", scope: "device" } },
36
37
  "retrieval-evaluation": { enabled: true },
37
38
  "operational-metrics": { enabled: true },
38
39
  "web-console": { enabled: true },
39
40
  "memory-explorer": { enabled: true },
40
41
  },
41
- reason: "Free preview: 10 recalls and 1 learning scan per local day; indexing and dashboard access remain available",
42
+ reason: "Included at no cost: 20 recalls and 5 learning scans per local day; local indexing and dashboard access remain available",
43
+ };
44
+ }
45
+ function devEntitlement() {
46
+ return {
47
+ plan: "pro",
48
+ state: "active",
49
+ features: ["session-intelligence", "web-console"],
50
+ capabilities: {
51
+ "session-index": { enabled: true },
52
+ recall: { enabled: true },
53
+ "session-worker": { enabled: true },
54
+ learning: { enabled: true },
55
+ "retrieval-evaluation": { enabled: true },
56
+ "operational-metrics": { enabled: true },
57
+ "web-console": { enabled: true },
58
+ "memory-explorer": { enabled: true },
59
+ },
60
+ reason: "Dev entitlement: all capabilities enabled, no quotas (AGENT_MEMORY_DEV_ENTITLEMENT=1)",
42
61
  };
43
62
  }
44
63
  function isEmail(value) {
@@ -253,7 +272,7 @@ export function openLoopbackUrl(url) {
253
272
  return false;
254
273
  }
255
274
  }
256
- export async function collectTemporaryActivation(openUrl = openLoopbackUrl) {
275
+ export async function collectActivation(openUrl = openLoopbackUrl) {
257
276
  const nonce = randomBytes(24).toString("hex");
258
277
  const activationPath = `/activate/${nonce}`;
259
278
  let expectedHost = "";
@@ -317,7 +336,7 @@ export async function collectTemporaryActivation(openUrl = openLoopbackUrl) {
317
336
  throw new PluginBootstrapFailure("browser_unavailable", `Open this URL in a browser: ${url}`);
318
337
  }
319
338
  const timer = setTimeout(() => {
320
- fail?.(new PluginBootstrapFailure("activation_timeout", "Temporary activation timed out", true));
339
+ fail?.(new PluginBootstrapFailure("activation_timeout", "Browser activation timed out", true));
321
340
  server.close();
322
341
  }, 5 * 60_000);
323
342
  timer.unref();
@@ -329,7 +348,7 @@ export async function collectTemporaryActivation(openUrl = openLoopbackUrl) {
329
348
  server.close();
330
349
  }
331
350
  }
332
- export class TemporaryPluginBackend {
351
+ export class AgentMemoryServiceBackend {
333
352
  root;
334
353
  coreVersion;
335
354
  apiOrigin;
@@ -348,7 +367,11 @@ export class TemporaryPluginBackend {
348
367
  }
349
368
  async getLocalEntitlement() {
350
369
  const activation = this.readActivation();
351
- return activation ? freeEntitlement() : cloneEntitlement(MISSING_ENTITLEMENT);
370
+ if (!activation)
371
+ return cloneEntitlement(MISSING_ENTITLEMENT);
372
+ if (process.env.AGENT_MEMORY_DEV_ENTITLEMENT === "1")
373
+ return devEntitlement();
374
+ return freeEntitlement();
352
375
  }
353
376
  async resolveAccess(request) {
354
377
  const activation = this.readActivation();
@@ -377,13 +400,18 @@ export class TemporaryPluginBackend {
377
400
  const learningQuota = value.entitlement.capabilities.learning?.quota;
378
401
  if (value.entitlement.plan !== "free" ||
379
402
  value.entitlement.state !== "active" ||
403
+ value.entitlement.capabilities.recall?.enabled !== true ||
380
404
  !recallQuota ||
405
+ recallQuota.limit !== 20 ||
381
406
  recallQuota.scope !== "device" ||
382
407
  recallQuota.window !== "day" ||
408
+ value.entitlement.capabilities.learning?.enabled !== true ||
383
409
  !learningQuota ||
410
+ learningQuota.limit !== 5 ||
384
411
  learningQuota.scope !== "device" ||
385
412
  learningQuota.window !== "day" ||
386
413
  value.entitlement.capabilities["session-index"]?.enabled !== true ||
414
+ value.entitlement.capabilities["session-worker"]?.enabled !== false ||
387
415
  value.entitlement.capabilities["web-console"]?.enabled !== true)
388
416
  throw new PluginBootstrapFailure("service_response_invalid", "The free preview policy is invalid");
389
417
  this.writeActivation(installationId, value.usageCredential, 1);
@@ -427,6 +455,17 @@ export class TemporaryPluginBackend {
427
455
  }
428
456
  readActivation() {
429
457
  const activationPath = this.activationPath();
458
+ if (!fs.existsSync(activationPath)) {
459
+ const legacyPath = path.join(this.root, ...ACTIVATION_FILE_LEGACY.split("/"));
460
+ if (fs.existsSync(legacyPath)) {
461
+ try {
462
+ fs.renameSync(legacyPath, activationPath);
463
+ }
464
+ catch {
465
+ /* best-effort migration */
466
+ }
467
+ }
468
+ }
430
469
  if (!fs.existsSync(activationPath))
431
470
  return null;
432
471
  try {
@@ -542,3 +581,8 @@ export class TemporaryPluginBackend {
542
581
  return response;
543
582
  }
544
583
  }
584
+ export class TemporaryPluginBackend extends AgentMemoryServiceBackend {
585
+ constructor(options) {
586
+ super({ ...options, openUrl: () => false });
587
+ }
588
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Upgrade orchestration for the `agent-memory` CLI and its official Pro plugin bundle.
3
+ *
4
+ * Two consumers:
5
+ * 1. `agent-memory upgrade` — explicit user command; checks and (optionally) installs.
6
+ * 2. `agent-memory hook session-start` — passive notice from a 24h-cached record.
7
+ *
8
+ * Network calls always have a hard timeout and always fail closed (upgrade is a
9
+ * quality-of-life feature; a flaky registry must never break the CLI).
10
+ */
11
+ import { type SpawnOptions } from "node:child_process";
12
+ export type InstallManager = "bun" | "npm" | "pnpm" | "yarn" | "unknown";
13
+ export interface InstallMethod {
14
+ manager: InstallManager;
15
+ global: boolean;
16
+ /** Absolute path we think holds the current install (for diagnostics). */
17
+ origin: string;
18
+ /** Argv used to invoke the package manager (e.g. ["npm","i","-g","myagentmemory@latest"]). */
19
+ command: string[];
20
+ }
21
+ export interface UpgradeCache {
22
+ checkedAt: string;
23
+ cliCurrent: string;
24
+ cliLatest: string | null;
25
+ pluginCurrent: string | null;
26
+ pluginLatest: string | null;
27
+ }
28
+ export interface UpgradeStatus {
29
+ cli: {
30
+ current: string;
31
+ latest: string | null;
32
+ upgradeAvailable: boolean;
33
+ };
34
+ plugin: {
35
+ current: string | null;
36
+ latest: string | null;
37
+ upgradeAvailable: boolean;
38
+ };
39
+ checkedAt: string;
40
+ fromCache: boolean;
41
+ }
42
+ export declare function readUpgradeCache(): UpgradeCache | null;
43
+ export declare function writeUpgradeCache(record: UpgradeCache): void;
44
+ export declare function isCacheFresh(record: UpgradeCache | null, now?: number): boolean;
45
+ /**
46
+ * Best-effort detection of how `myagentmemory` was installed. Path signatures
47
+ * are heuristic but cover the common managers. On no match we fall back to
48
+ * `npm -g` per the user's choice ("best-effort try anyway").
49
+ */
50
+ export declare function detectInstallMethod(location?: string): InstallMethod;
51
+ export interface InstallResult {
52
+ ok: boolean;
53
+ code: number | null;
54
+ stdout: string;
55
+ stderr: string;
56
+ command: string[];
57
+ }
58
+ export declare function runInstaller(method: InstallMethod, opts?: SpawnOptions): InstallResult;
59
+ /**
60
+ * Fire-and-forget: spawn a detached child that runs `agent-memory upgrade
61
+ * --check --refresh --quiet` so the next session-start has a fresh cache.
62
+ * Never awaits, never throws.
63
+ */
64
+ export declare function refreshUpgradeCacheBackground(): void;
65
+ export interface CheckOptions {
66
+ cliCurrent: string;
67
+ pluginCurrent: string | null;
68
+ /** When true, do NOT hit the network — read cache only. Returns fromCache=true even on miss. */
69
+ cacheOnly?: boolean;
70
+ /** When true, force a network refresh and rewrite the cache regardless of freshness. */
71
+ refresh?: boolean;
72
+ /** Optional injected npm fetcher (used by tests). */
73
+ fetchCliLatest?: () => Promise<string | null>;
74
+ /** Optional injected plugin-latest resolver. When omitted, plugin latest is taken from pluginLatestHint. */
75
+ pluginLatestHint?: string | null;
76
+ /** Explicit signal from the bootstrap that a newer release exists even when the version number is unknown. */
77
+ pluginUpgradeAvailable?: boolean;
78
+ }
79
+ export declare function checkForUpgrades(opts: CheckOptions): Promise<UpgradeStatus>;
80
+ export declare function formatUpgradeNotice(status: UpgradeStatus): string | null;