myagentmemory 0.4.16 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 = {
@@ -14,13 +14,13 @@ const MISSING_ENTITLEMENT = {
14
14
  reason: "No signed AgentMemory commercial entitlement is installed",
15
15
  };
16
16
  const OFFICIAL_PLUGINS = [
17
- { id: OFFICIAL_PLUGIN_IDS[0], name: "Session Intelligence" },
18
- { id: OFFICIAL_PLUGIN_IDS[1], name: "Web Console" },
17
+ { id: OFFICIAL_PLUGIN_IDS[0], name: "Coding History Recall" },
18
+ { id: OFFICIAL_PLUGIN_IDS[1], name: "Memory Dashboard" },
19
19
  ];
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;
@@ -103,11 +118,47 @@ export interface PluginStructuredErrorV1 {
103
118
  message: string;
104
119
  retryable?: boolean;
105
120
  }
121
+ export interface PluginContextSectionV1 {
122
+ id: string;
123
+ label: string;
124
+ content: string;
125
+ artifactPath?: string;
126
+ metadata?: Record<string, unknown>;
127
+ }
128
+ export interface PluginContextProviderV1 {
129
+ name: string;
130
+ requiredCapability: string;
131
+ provide(context: {
132
+ host: string;
133
+ cwd?: string;
134
+ query?: string;
135
+ signal: AbortSignal;
136
+ }): Promise<PluginContextSectionV1[]>;
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
+ inputSchema: PluginMcpToolInputSchema;
151
+ run(input: Record<string, unknown>): unknown | Promise<unknown>;
152
+ }
106
153
  export interface AgentMemoryPluginHostV1 {
107
154
  apiVersion: 1;
108
155
  coreVersion: string;
109
156
  registerCommand(command: PluginCommandV1): void;
110
157
  registerSessionStartHook(hook: PluginSessionStartHookV1): void;
158
+ registerBackgroundRefresh?(hook: PluginBackgroundRefreshHookV1): void;
159
+ registerContextProvider?(provider: PluginContextProviderV1): void;
160
+ registerMcpTool?(tool: PluginMcpToolV1): void;
161
+ registerMcpStartup?(fn: () => void | Promise<void>): void;
111
162
  getStateDirectory(): string;
112
163
  getMemoryDirectory(): string;
113
164
  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 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,11 +11,30 @@ export declare class InstalledPluginRuntimeV1 {
11
11
  private readonly backend;
12
12
  private readonly commands;
13
13
  private readonly hooks;
14
+ private readonly backgroundRefreshHooks;
15
+ private readonly contextProviders;
16
+ private readonly mcpTools;
17
+ private readonly mcpStartupHooks;
14
18
  private loaded;
15
19
  constructor(options: PluginRuntimeOptionsV1);
16
20
  load(): Promise<boolean>;
17
21
  run(name: string, context: PluginCommandContextV1): Promise<PluginCommandResultV1 | null>;
18
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>;
30
+ provideContext(context: {
31
+ host: string;
32
+ cwd?: string;
33
+ query?: string;
34
+ signal: AbortSignal;
35
+ }): Promise<PluginContextSectionV1[]>;
36
+ getMcpTools(): PluginMcpToolV1[];
37
+ runMcpStartup(): Promise<void>;
19
38
  private createHost;
20
39
  private refreshEntitlement;
21
40
  }
@@ -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,11 +61,15 @@ export class InstalledPluginRuntimeV1 {
61
61
  backend;
62
62
  commands = new Map();
63
63
  hooks = [];
64
+ backgroundRefreshHooks = [];
65
+ contextProviders = [];
66
+ mcpTools = [];
67
+ mcpStartupHooks = [];
64
68
  loaded = false;
65
69
  constructor(options) {
66
70
  this.options = options;
67
71
  this.store = options.store ?? new FilePluginInstallStore();
68
- this.backend = options.backend ?? new TemporaryPluginBackend({ root: this.store.root });
72
+ this.backend = options.backend ?? new AgentMemoryServiceBackend({ root: this.store.root });
69
73
  }
70
74
  async load() {
71
75
  if (this.loaded)
@@ -132,6 +136,60 @@ export class InstalledPluginRuntimeV1 {
132
136
  throw error;
133
137
  }
134
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
+ }
155
+ async provideContext(context) {
156
+ if (!(await this.load()) || this.contextProviders.length === 0)
157
+ return [];
158
+ const entitlement = await this.refreshEntitlement();
159
+ const sections = [];
160
+ for (const registered of this.contextProviders) {
161
+ if (!isPluginCapabilityEnabled(entitlement, registered.provider.requiredCapability))
162
+ continue;
163
+ const provided = await registered.provider.provide(context);
164
+ if (!Array.isArray(provided) || provided.length > 16)
165
+ throw new PluginBootstrapFailure("plugin_context_invalid", `Plugin ${registered.pluginId} returned invalid context sections`);
166
+ for (const section of provided) {
167
+ if (!section ||
168
+ typeof section.id !== "string" ||
169
+ section.id.length === 0 ||
170
+ section.id.length > 256 ||
171
+ typeof section.label !== "string" ||
172
+ section.label.length === 0 ||
173
+ section.label.length > 256 ||
174
+ typeof section.content !== "string" ||
175
+ Buffer.byteLength(section.content, "utf-8") > 64 * 1024 ||
176
+ (section.artifactPath !== undefined &&
177
+ (typeof section.artifactPath !== "string" || section.artifactPath.length > 4_096)) ||
178
+ (section.metadata !== undefined &&
179
+ (!section.metadata || typeof section.metadata !== "object" || Array.isArray(section.metadata))))
180
+ throw new PluginBootstrapFailure("plugin_context_invalid", `Plugin ${registered.pluginId} returned an invalid context section`);
181
+ sections.push(structuredClone(section));
182
+ }
183
+ }
184
+ return sections;
185
+ }
186
+ getMcpTools() {
187
+ return [...this.mcpTools];
188
+ }
189
+ async runMcpStartup() {
190
+ for (const hook of this.mcpStartupHooks)
191
+ await hook();
192
+ }
135
193
  createHost(manifest) {
136
194
  const descriptors = new Map(manifest.commands.map((command) => [command.name, command]));
137
195
  const stateRoot = path.join(this.store.root, "state");
@@ -165,6 +223,28 @@ export class InstalledPluginRuntimeV1 {
165
223
  throw new PluginBootstrapFailure("plugin_hook_invalid", `Plugin ${manifest.id} registered a hook with an undeclared capability`);
166
224
  this.hooks.push(hook);
167
225
  },
226
+ registerBackgroundRefresh: (hook) => {
227
+ if (!(manifest.capabilities ?? []).includes(hook.requiredCapability))
228
+ throw new PluginBootstrapFailure("plugin_hook_invalid", `Plugin ${manifest.id} registered a background refresh hook with an undeclared capability`);
229
+ if (!hook.name || this.backgroundRefreshHooks.some((existing) => existing.name === hook.name))
230
+ throw new PluginBootstrapFailure("plugin_hook_invalid", `Plugin background refresh hook ${hook.name || "(unnamed)"} is invalid or already registered`);
231
+ this.backgroundRefreshHooks.push(hook);
232
+ },
233
+ registerContextProvider: (provider) => {
234
+ if (!(manifest.capabilities ?? []).includes(provider.requiredCapability))
235
+ throw new PluginBootstrapFailure("plugin_context_invalid", `Plugin ${manifest.id} registered a context provider with an undeclared capability`);
236
+ if (!provider.name || this.contextProviders.some((item) => item.provider.name === provider.name))
237
+ throw new PluginBootstrapFailure("plugin_context_invalid", `Plugin context provider ${provider.name || "(unnamed)"} is invalid or already registered`);
238
+ this.contextProviders.push({ provider, pluginId: manifest.id });
239
+ },
240
+ registerMcpTool: (tool) => {
241
+ if (!tool.name || this.mcpTools.some((existing) => existing.name === tool.name))
242
+ throw new PluginBootstrapFailure("plugin_mcp_tool_invalid", `Plugin MCP tool ${tool.name || "(unnamed)"} is invalid or already registered`);
243
+ this.mcpTools.push(tool);
244
+ },
245
+ registerMcpStartup: (fn) => {
246
+ this.mcpStartupHooks.push(fn);
247
+ },
168
248
  getStateDirectory: () => stateDirectory,
169
249
  getMemoryDirectory: () => {
170
250
  assertPermission(manifest, "memory:read");
@@ -215,6 +295,7 @@ export function createInstalledBundleHealthCheck(coreVersion, backend, storeRoot
215
295
  coreVersion,
216
296
  registerCommand() { },
217
297
  registerSessionStartHook() { },
298
+ registerBackgroundRefresh() { },
218
299
  getStateDirectory: () => stateDirectory,
219
300
  getMemoryDirectory: () => {
220
301
  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;
@@ -18,29 +19,45 @@ const MISSING_ENTITLEMENT = {
18
19
  state: "missing",
19
20
  features: [],
20
21
  capabilities: {},
21
- reason: "Enter an email address to activate the free daily session allowance",
22
+ reason: "Install the no-account Pro preview to activate local recall and learning",
22
23
  };
23
24
  function cloneEntitlement(value) {
24
25
  return structuredClone(value);
25
26
  }
26
- function freeEntitlement(dailySessionLimit) {
27
+ function freeEntitlement() {
27
28
  return {
28
29
  plan: "free",
29
30
  state: "active",
30
31
  features: ["session-intelligence", "web-console"],
31
32
  capabilities: {
32
33
  "session-index": { enabled: true },
33
- "session-worker": {
34
- enabled: true,
35
- quota: { limit: dailySessionLimit, window: "day", scope: "account" },
36
- },
34
+ recall: { enabled: true, quota: { limit: 20, window: "day", scope: "device" } },
35
+ "session-worker": { enabled: false },
36
+ learning: { enabled: true, quota: { limit: 5, window: "day", scope: "device" } },
37
+ "retrieval-evaluation": { enabled: true },
38
+ "operational-metrics": { enabled: true },
39
+ "web-console": { enabled: true },
40
+ "memory-explorer": { enabled: true },
41
+ },
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 },
37
54
  learning: { enabled: true },
38
55
  "retrieval-evaluation": { enabled: true },
39
56
  "operational-metrics": { enabled: true },
40
57
  "web-console": { enabled: true },
41
58
  "memory-explorer": { enabled: true },
42
59
  },
43
- reason: `${dailySessionLimit} free agent sessions per UTC day`,
60
+ reason: "Dev entitlement: all capabilities enabled, no quotas (AGENT_MEMORY_DEV_ENTITLEMENT=1)",
44
61
  };
45
62
  }
46
63
  function isEmail(value) {
@@ -255,7 +272,7 @@ export function openLoopbackUrl(url) {
255
272
  return false;
256
273
  }
257
274
  }
258
- export async function collectTemporaryActivation(openUrl = openLoopbackUrl) {
275
+ export async function collectActivation(openUrl = openLoopbackUrl) {
259
276
  const nonce = randomBytes(24).toString("hex");
260
277
  const activationPath = `/activate/${nonce}`;
261
278
  let expectedHost = "";
@@ -319,7 +336,7 @@ export async function collectTemporaryActivation(openUrl = openLoopbackUrl) {
319
336
  throw new PluginBootstrapFailure("browser_unavailable", `Open this URL in a browser: ${url}`);
320
337
  }
321
338
  const timer = setTimeout(() => {
322
- fail?.(new PluginBootstrapFailure("activation_timeout", "Temporary activation timed out", true));
339
+ fail?.(new PluginBootstrapFailure("activation_timeout", "Browser activation timed out", true));
323
340
  server.close();
324
341
  }, 5 * 60_000);
325
342
  timer.unref();
@@ -331,7 +348,7 @@ export async function collectTemporaryActivation(openUrl = openLoopbackUrl) {
331
348
  server.close();
332
349
  }
333
350
  }
334
- export class TemporaryPluginBackend {
351
+ export class AgentMemoryServiceBackend {
335
352
  root;
336
353
  coreVersion;
337
354
  apiOrigin;
@@ -346,41 +363,31 @@ export class TemporaryPluginBackend {
346
363
  this.artifactOrigin = options.artifactOrigin ?? ARTIFACT_ORIGIN;
347
364
  this.fetchImplementation = options.fetch ?? globalThis.fetch;
348
365
  this.openUrl = options.openUrl ?? openLoopbackUrl;
349
- this.activate = options.activate ?? (() => collectTemporaryActivation(this.openUrl));
366
+ this.activate = options.activate ?? (async () => `am_install_${randomBytes(24).toString("base64url")}`);
350
367
  }
351
368
  async getLocalEntitlement() {
352
369
  const activation = this.readActivation();
353
- return activation ? freeEntitlement(activation.dailySessionLimit) : 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();
354
375
  }
355
376
  async resolveAccess(request) {
356
377
  const activation = this.readActivation();
357
- let email = activation?.email;
358
- if (!email) {
359
- if (!request.allowAuthentication)
360
- return {
361
- kind: "auth_required",
362
- entitlement: cloneEntitlement(MISSING_ENTITLEMENT),
363
- nextAction: {
364
- kind: "authenticate",
365
- url: "https://jayzeng.github.io/agentmemory/",
366
- message: "Run plugin install in an interactive terminal to enter an email address",
367
- },
368
- };
369
- email = await this.activate();
370
- }
378
+ const installationId = activation?.installationId ?? (await this.activate());
371
379
  const response = await this.request(`${this.apiOrigin}/v1/plugin/access`, {
372
380
  method: "POST",
373
381
  headers: { "Content-Type": "application/json" },
374
382
  body: JSON.stringify({
375
- schemaVersion: 1,
376
- email,
383
+ schemaVersion: 2,
384
+ installationId,
377
385
  bundleId: request.bundleId,
378
386
  installedVersion: request.installedVersion ?? null,
379
387
  coreVersion: this.coreVersion,
380
388
  channel: request.channel,
381
389
  platform: process.platform,
382
390
  architecture: process.arch,
383
- consentVersion: "activation-v2",
384
391
  }),
385
392
  });
386
393
  const value = (await readJson(response));
@@ -389,14 +396,25 @@ export class TemporaryPluginBackend {
389
396
  throw new PluginBootstrapFailure("service_response_invalid", "The access response omitted its artifact grant");
390
397
  if (typeof value.usageCredential !== "string" || !ACTIVATION_CREDENTIAL.test(value.usageCredential))
391
398
  throw new PluginBootstrapFailure("service_response_invalid", "The access response omitted its usage credential");
392
- const freeQuota = value.entitlement.capabilities["session-worker"]?.quota;
399
+ const recallQuota = value.entitlement.capabilities.recall?.quota;
400
+ const learningQuota = value.entitlement.capabilities.learning?.quota;
393
401
  if (value.entitlement.plan !== "free" ||
394
402
  value.entitlement.state !== "active" ||
395
- !freeQuota ||
396
- freeQuota.scope !== "account" ||
397
- freeQuota.window !== "day")
398
- throw new PluginBootstrapFailure("service_response_invalid", "The free session policy is invalid");
399
- this.writeActivation(email, value.usageCredential, freeQuota.limit);
403
+ value.entitlement.capabilities.recall?.enabled !== true ||
404
+ !recallQuota ||
405
+ recallQuota.limit !== 20 ||
406
+ recallQuota.scope !== "device" ||
407
+ recallQuota.window !== "day" ||
408
+ value.entitlement.capabilities.learning?.enabled !== true ||
409
+ !learningQuota ||
410
+ learningQuota.limit !== 5 ||
411
+ learningQuota.scope !== "device" ||
412
+ learningQuota.window !== "day" ||
413
+ value.entitlement.capabilities["session-index"]?.enabled !== true ||
414
+ value.entitlement.capabilities["session-worker"]?.enabled !== false ||
415
+ value.entitlement.capabilities["web-console"]?.enabled !== true)
416
+ throw new PluginBootstrapFailure("service_response_invalid", "The free preview policy is invalid");
417
+ this.writeActivation(installationId, value.usageCredential, 1);
400
418
  return { kind: "granted", entitlement: value.entitlement, artifactGrant: value.artifactGrant };
401
419
  }
402
420
  async reserveSession(operationId) {
@@ -437,6 +455,17 @@ export class TemporaryPluginBackend {
437
455
  }
438
456
  readActivation() {
439
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
+ }
440
469
  if (!fs.existsSync(activationPath))
441
470
  return null;
442
471
  try {
@@ -451,8 +480,9 @@ export class TemporaryPluginBackend {
451
480
  if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" && (stat.mode & 0o077) !== 0))
452
481
  return null;
453
482
  const value = JSON.parse(fs.readFileSync(activationPath, "utf-8"));
454
- if (value.schemaVersion !== 2 ||
455
- !isEmail(value.email) ||
483
+ if (value.schemaVersion !== 3 ||
484
+ typeof value.installationId !== "string" ||
485
+ !/^am_install_[A-Za-z0-9_-]{32}$/.test(value.installationId) ||
456
486
  !Number.isFinite(Date.parse(value.activatedAt)) ||
457
487
  !ACTIVATION_CREDENTIAL.test(value.usageCredential) ||
458
488
  !Number.isSafeInteger(value.dailySessionLimit) ||
@@ -465,9 +495,9 @@ export class TemporaryPluginBackend {
465
495
  return null;
466
496
  }
467
497
  }
468
- writeActivation(email, usageCredential, dailySessionLimit) {
469
- if (!isEmail(email))
470
- throw new PluginBootstrapFailure("email_invalid", "Enter a valid email address");
498
+ writeActivation(installationId, usageCredential, dailySessionLimit) {
499
+ if (!/^am_install_[A-Za-z0-9_-]{32}$/.test(installationId))
500
+ throw new PluginBootstrapFailure("activation_failed", "The installation identifier is invalid");
471
501
  if (!ACTIVATION_CREDENTIAL.test(usageCredential))
472
502
  throw new PluginBootstrapFailure("activation_failed", "The activation credential is invalid");
473
503
  if (!Number.isSafeInteger(dailySessionLimit) || dailySessionLimit <= 0 || dailySessionLimit > 10_000)
@@ -484,7 +514,13 @@ export class TemporaryPluginBackend {
484
514
  if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink())
485
515
  throw new PluginBootstrapFailure("activation_path_invalid", "The plugin activation directory is unsafe");
486
516
  const temporary = `${target}.tmp-${process.pid}-${randomUUID()}`;
487
- fs.writeFileSync(temporary, `${JSON.stringify({ schemaVersion: 2, email, activatedAt: new Date().toISOString(), usageCredential, dailySessionLimit }, null, 2)}\n`, { mode: 0o600, flag: "wx" });
517
+ fs.writeFileSync(temporary, `${JSON.stringify({
518
+ schemaVersion: 3,
519
+ installationId,
520
+ activatedAt: new Date().toISOString(),
521
+ usageCredential,
522
+ dailySessionLimit,
523
+ }, null, 2)}\n`, { mode: 0o600, flag: "wx" });
488
524
  fs.renameSync(temporary, target);
489
525
  }
490
526
  async sessionUsage(action, operationId) {
@@ -545,3 +581,8 @@ export class TemporaryPluginBackend {
545
581
  return response;
546
582
  }
547
583
  }
584
+ export class TemporaryPluginBackend extends AgentMemoryServiceBackend {
585
+ constructor(options) {
586
+ super({ ...options, openUrl: () => false });
587
+ }
588
+ }