myagentmemory 0.4.16 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +78 -68
- package/dist/cli-spec.d.ts +7 -1
- package/dist/cli-spec.js +226 -12
- package/dist/cli.js +2140 -182
- 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 +6 -6
- package/dist/plugin-host.d.ts +51 -0
- package/dist/plugin-runtime.d.ts +20 -1
- package/dist/plugin-runtime.js +83 -2
- package/dist/plugin-service.d.ts +10 -4
- package/dist/plugin-service.js +83 -42
- package/dist/upgrade.d.ts +80 -0
- package/dist/upgrade.js +243 -0
- package/docs/official-plugin-bootstrap.md +30 -22
- 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 +230 -12
- package/src/completions.ts +26 -18
- package/src/core.ts +312 -123
- package/src/hooks.ts +395 -85
- package/src/plugin-bootstrap.ts +6 -6
- package/src/plugin-host.ts +53 -0
- package/src/cli.ts +0 -1271
- package/src/plugin-runtime.ts +0 -326
- package/src/plugin-service.ts +0 -628
|
@@ -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
|
+
}
|
package/dist/plugin-bootstrap.js
CHANGED
|
@@ -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 {
|
|
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: "
|
|
18
|
-
{ id: OFFICIAL_PLUGIN_IDS[1], name: "
|
|
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
|
|
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
|
|
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":
|
|
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");
|
package/dist/plugin-host.d.ts
CHANGED
|
@@ -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>;
|
package/dist/plugin-runtime.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/plugin-runtime.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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");
|
package/dist/plugin-service.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
14
|
-
export declare class
|
|
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?:
|
|
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 {};
|
package/dist/plugin-service.js
CHANGED
|
@@ -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/
|
|
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: "
|
|
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(
|
|
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
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
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
|
|
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", "
|
|
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
|
|
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 ?? (() =>
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
376
|
-
|
|
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
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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 !==
|
|
455
|
-
|
|
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(
|
|
469
|
-
if (
|
|
470
|
-
throw new PluginBootstrapFailure("
|
|
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({
|
|
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
|
+
}
|