@agentmemory/agentmemory 0.9.31 → 0.9.32
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/.env.example +18 -15
- package/AGENTS.md +4 -4
- package/README.md +92 -270
- package/dist/.env.example +18 -15
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +530 -75
- package/dist/cli.mjs.map +1 -1
- package/dist/{compression-recovery-CnhLQC6N.mjs → compression-recovery-CZh63Jmz.mjs} +174 -89
- package/dist/compression-recovery-CZh63Jmz.mjs.map +1 -0
- package/dist/{connect-BSLIAMpw.mjs → connect-BX3njQ3Y.mjs} +28 -14
- package/dist/connect-BX3njQ3Y.mjs.map +1 -0
- package/dist/hooks/_capture.d.mts +3 -2
- package/dist/hooks/_capture.d.mts.map +1 -1
- package/dist/hooks/_capture.mjs +87 -8
- package/dist/hooks/_capture.mjs.map +1 -1
- package/dist/hooks/post-commit.mjs +58 -5
- package/dist/hooks/post-commit.mjs.map +1 -1
- package/dist/hooks/post-tool-failure.mjs +7 -2
- package/dist/hooks/post-tool-failure.mjs.map +1 -1
- package/dist/hooks/post-tool-use.mjs +7 -2
- package/dist/hooks/post-tool-use.mjs.map +1 -1
- package/dist/hooks/pre-compact.mjs +67 -12
- package/dist/hooks/pre-compact.mjs.map +1 -1
- package/dist/hooks/pre-tool-use.mjs +58 -5
- package/dist/hooks/pre-tool-use.mjs.map +1 -1
- package/dist/hooks/session-end.mjs +14 -3
- package/dist/hooks/session-end.mjs.map +1 -1
- package/dist/hooks/session-start.mjs +58 -5
- package/dist/hooks/session-start.mjs.map +1 -1
- package/dist/hooks/stop.mjs +2 -1
- package/dist/hooks/stop.mjs.map +1 -1
- package/dist/hooks/subagent-stop.mjs +15 -5
- package/dist/hooks/subagent-stop.mjs.map +1 -1
- package/dist/hooks/task-completed.mjs +8 -1
- package/dist/hooks/task-completed.mjs.map +1 -1
- package/dist/index.mjs +724 -200
- package/dist/index.mjs.map +1 -1
- package/dist/integrations/dsh.mjs +31 -4
- package/dist/integrations/dsh.mjs.map +1 -1
- package/dist/{secret-store-D9GwO0xp.mjs → secret-store-iYXRcrO8.mjs} +197 -55
- package/dist/secret-store-iYXRcrO8.mjs.map +1 -0
- package/dist/{src-DaMtDzt_.mjs → src-C23nMpda.mjs} +359 -62
- package/dist/src-C23nMpda.mjs.map +1 -0
- package/dist/{standalone-BlqshyZ5.mjs → standalone-BYo5USDb.mjs} +169 -10
- package/dist/standalone-BYo5USDb.mjs.map +1 -0
- package/dist/standalone.d.mts.map +1 -1
- package/dist/standalone.mjs +240 -10
- package/dist/standalone.mjs.map +1 -1
- package/dist/viewer/index.html +49 -28
- package/integrations/hermes/README.md +11 -1
- package/integrations/hermes/__init__.py +1 -0
- package/integrations/hermes/plugin.yaml +1 -1
- package/integrations/omp/dist/index.mjs +31 -4
- package/integrations/omp/package.json +1 -1
- package/integrations/omp/skills/agentmemory-agents/REFERENCE.md +1 -1
- package/integrations/omp/skills/agentmemory-architecture/SKILL.md +5 -1
- package/integrations/omp/skills/agentmemory-config/REFERENCE.md +2 -1
- package/integrations/omp/skills/agentmemory-config/SKILL.md +29 -9
- package/integrations/omp/skills/agentmemory-hooks/SKILL.md +17 -6
- package/integrations/omp/skills/agentmemory-mcp-tools/REFERENCE.md +1 -1
- package/integrations/omp/skills/agentmemory-mcp-tools/SKILL.md +10 -0
- package/integrations/omp/skills/agentmemory-rest-api/REFERENCE.md +2 -1
- package/integrations/omp/skills/agentmemory-rest-api/SKILL.md +1 -1
- package/integrations/openclaw/README.md +10 -0
- package/integrations/openclaw/openclaw.plugin.json +1 -1
- package/integrations/openclaw/package.json +1 -1
- package/integrations/openclaw/plugin.yaml +1 -1
- package/integrations/openclaw/skills/agentmemory-agents/REFERENCE.md +1 -1
- package/integrations/openclaw/skills/agentmemory-architecture/SKILL.md +5 -1
- package/integrations/openclaw/skills/agentmemory-config/REFERENCE.md +2 -1
- package/integrations/openclaw/skills/agentmemory-config/SKILL.md +29 -9
- package/integrations/openclaw/skills/agentmemory-hooks/SKILL.md +17 -6
- package/integrations/openclaw/skills/agentmemory-mcp-tools/REFERENCE.md +1 -1
- package/integrations/openclaw/skills/agentmemory-mcp-tools/SKILL.md +10 -0
- package/integrations/openclaw/skills/agentmemory-rest-api/REFERENCE.md +2 -1
- package/integrations/openclaw/skills/agentmemory-rest-api/SKILL.md +1 -1
- package/integrations/pi/README.md +1 -0
- package/integrations/pi/dist/index.mjs +92 -11
- package/integrations/pi/index.ts +56 -6
- package/integrations/pi/package.json +1 -1
- package/integrations/pi/skills/agentmemory-agents/REFERENCE.md +1 -1
- package/integrations/pi/skills/agentmemory-architecture/SKILL.md +5 -1
- package/integrations/pi/skills/agentmemory-config/REFERENCE.md +2 -1
- package/integrations/pi/skills/agentmemory-config/SKILL.md +29 -9
- package/integrations/pi/skills/agentmemory-hooks/SKILL.md +17 -6
- package/integrations/pi/skills/agentmemory-mcp-tools/REFERENCE.md +1 -1
- package/integrations/pi/skills/agentmemory-mcp-tools/SKILL.md +10 -0
- package/integrations/pi/skills/agentmemory-rest-api/REFERENCE.md +2 -1
- package/integrations/pi/skills/agentmemory-rest-api/SKILL.md +1 -1
- package/package.json +4 -3
- package/plugin/.claude-plugin/plugin.json +16 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.devin-plugin/plugin.json +3 -4
- package/plugin/.mcp.json +5 -1
- package/plugin/README.md +71 -0
- package/plugin/hooks/hooks.json +24 -12
- package/plugin/opencode/plugin.json +1 -1
- package/plugin/plugin.json +1 -1
- package/plugin/scripts/_capture.d.mts +3 -2
- package/plugin/scripts/_capture.d.mts.map +1 -1
- package/plugin/scripts/_capture.mjs +87 -8
- package/plugin/scripts/_capture.mjs.map +1 -1
- package/plugin/scripts/plugin-bridge.mjs +25 -6
- package/plugin/scripts/post-tool-failure.mjs +7 -2
- package/plugin/scripts/post-tool-failure.mjs.map +1 -1
- package/plugin/scripts/post-tool-use.mjs +7 -2
- package/plugin/scripts/post-tool-use.mjs.map +1 -1
- package/plugin/scripts/pre-compact.mjs +67 -12
- package/plugin/scripts/pre-compact.mjs.map +1 -1
- package/plugin/scripts/pre-tool-use.mjs +58 -5
- package/plugin/scripts/pre-tool-use.mjs.map +1 -1
- package/plugin/scripts/session-end.mjs +14 -3
- package/plugin/scripts/session-end.mjs.map +1 -1
- package/plugin/scripts/session-start.mjs +58 -5
- package/plugin/scripts/session-start.mjs.map +1 -1
- package/plugin/scripts/stop.mjs +2 -1
- package/plugin/scripts/stop.mjs.map +1 -1
- package/plugin/scripts/subagent-stop.mjs +15 -5
- package/plugin/scripts/subagent-stop.mjs.map +1 -1
- package/plugin/scripts/task-completed.mjs +8 -1
- package/plugin/scripts/task-completed.mjs.map +1 -1
- package/plugin/skills/agentmemory-agents/REFERENCE.md +1 -1
- package/plugin/skills/agentmemory-architecture/SKILL.md +5 -1
- package/plugin/skills/agentmemory-config/REFERENCE.md +2 -1
- package/plugin/skills/agentmemory-config/SKILL.md +29 -9
- package/plugin/skills/agentmemory-hooks/SKILL.md +17 -6
- package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +1 -1
- package/plugin/skills/agentmemory-mcp-tools/SKILL.md +10 -0
- package/plugin/skills/agentmemory-rest-api/REFERENCE.md +2 -1
- package/plugin/skills/agentmemory-rest-api/SKILL.md +1 -1
- package/dist/compression-recovery-CnhLQC6N.mjs.map +0 -1
- package/dist/connect-BSLIAMpw.mjs.map +0 -1
- package/dist/secret-store-D9GwO0xp.mjs.map +0 -1
- package/dist/src-DaMtDzt_.mjs.map +0 -1
- package/dist/standalone-BlqshyZ5.mjs.map +0 -1
- package/plugin/scripts/post-commit.d.mts +0 -1
- package/plugin/scripts/post-commit.mjs +0 -204
- package/plugin/scripts/post-commit.mjs.map +0 -1
|
@@ -3,6 +3,15 @@ import { readFileSync, realpathSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { execSync } from "node:child_process";
|
|
6
|
+
//#region src/hooks/_capture-filter.ts
|
|
7
|
+
function preCompactBudget() {
|
|
8
|
+
const raw = process.env["AGENTMEMORY_PRE_COMPACT_BUDGET"];
|
|
9
|
+
if (raw?.trim() === "0") return 0;
|
|
10
|
+
if (!raw?.trim()) return 1500;
|
|
11
|
+
const parsed = Number.parseInt(raw, 10);
|
|
12
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 1500;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
6
15
|
//#region src/secret-store.ts
|
|
7
16
|
const SECRET_KEY = "AGENTMEMORY_SECRET";
|
|
8
17
|
function agentmemoryHomeDir() {
|
|
@@ -90,13 +99,58 @@ function canonicalSecretPath() {
|
|
|
90
99
|
return join(homedir(), ".agentmemory", "secret");
|
|
91
100
|
}
|
|
92
101
|
//#endregion
|
|
93
|
-
//#region src/hooks/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return
|
|
102
|
+
//#region src/hooks/_endpoint.ts
|
|
103
|
+
const OPTION_PREFIX = "CLAUDE_PLUGIN_OPTION_";
|
|
104
|
+
const DEFAULT_URL = "http://localhost:3111";
|
|
105
|
+
function filled(value) {
|
|
106
|
+
const trimmed = value?.trim();
|
|
107
|
+
if (!trimmed || trimmed.startsWith("${") && trimmed.endsWith("}")) return void 0;
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function hookSetting(name, env = process.env) {
|
|
111
|
+
const optionKey = `${OPTION_PREFIX}${name}`;
|
|
112
|
+
const option = filled(env[optionKey])?.trim();
|
|
113
|
+
if (option) return option;
|
|
114
|
+
if (name === "AGENTMEMORY_SECRET" && optionKey in env) return void 0;
|
|
115
|
+
return filled(env[name]);
|
|
116
|
+
}
|
|
117
|
+
function resolveSettingSecret(url, env = process.env) {
|
|
118
|
+
const secretEnv = { ...env };
|
|
119
|
+
const explicit = hookSetting("AGENTMEMORY_SECRET", env);
|
|
120
|
+
if (explicit === void 0) delete secretEnv["AGENTMEMORY_SECRET"];
|
|
121
|
+
else secretEnv["AGENTMEMORY_SECRET"] = explicit;
|
|
122
|
+
return resolveClientSecret(url, secretEnv);
|
|
123
|
+
}
|
|
124
|
+
function secretTransportAllowed(url) {
|
|
125
|
+
try {
|
|
126
|
+
return new URL(url).protocol === "https:" || isLoopbackUrl(url);
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function resolveHookEndpoint(env = process.env) {
|
|
132
|
+
const url = hookSetting("AGENTMEMORY_URL", env) || DEFAULT_URL;
|
|
133
|
+
const secret = resolveSettingSecret(url, env);
|
|
134
|
+
const blocked = !secretTransportAllowed(url);
|
|
135
|
+
return {
|
|
136
|
+
url,
|
|
137
|
+
secret: blocked ? "" : secret,
|
|
138
|
+
blocked
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
let blockedNoticeShown = false;
|
|
142
|
+
function reportBlockedTransport(endpoint) {
|
|
143
|
+
if (!endpoint.blocked) return false;
|
|
144
|
+
if (!blockedNoticeShown) {
|
|
145
|
+
blockedNoticeShown = true;
|
|
146
|
+
process.stderr.write("[agentmemory] AGENTMEMORY_URL must use HTTPS unless it is a loopback address. Nothing was sent.\n");
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
function hookAuthHeaders(endpoint) {
|
|
151
|
+
const h = { "Content-Type": "application/json" };
|
|
152
|
+
if (endpoint.secret) h["Authorization"] = `Bearer ${endpoint.secret}`;
|
|
153
|
+
return h;
|
|
100
154
|
}
|
|
101
155
|
//#endregion
|
|
102
156
|
//#region src/hooks/_project.ts
|
|
@@ -142,12 +196,11 @@ function isSdkChildContext(payload) {
|
|
|
142
196
|
if (!payload || typeof payload !== "object") return false;
|
|
143
197
|
return payload.entrypoint === "sdk-ts";
|
|
144
198
|
}
|
|
145
|
-
const
|
|
146
|
-
const
|
|
199
|
+
const INJECT_CONTEXT = process.env["AGENTMEMORY_INJECT_CONTEXT"] === "true";
|
|
200
|
+
const ENDPOINT = resolveHookEndpoint();
|
|
201
|
+
const REST_URL = ENDPOINT.url;
|
|
147
202
|
function authHeaders() {
|
|
148
|
-
|
|
149
|
-
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
|
150
|
-
return h;
|
|
203
|
+
return hookAuthHeaders(ENDPOINT);
|
|
151
204
|
}
|
|
152
205
|
async function main() {
|
|
153
206
|
let input = "";
|
|
@@ -160,6 +213,7 @@ async function main() {
|
|
|
160
213
|
}
|
|
161
214
|
if (!data || typeof data !== "object") return;
|
|
162
215
|
if (isSdkChildContext(data)) return;
|
|
216
|
+
if (reportBlockedTransport(ENDPOINT)) return;
|
|
163
217
|
const sessionId = data.session_id || data.sessionId || data.conversation_id || "unknown";
|
|
164
218
|
const project = resolveProject(hookCwd(data));
|
|
165
219
|
if (process.env["CLAUDE_MEMORY_BRIDGE"] === "true") try {
|
|
@@ -170,6 +224,7 @@ async function main() {
|
|
|
170
224
|
signal: AbortSignal.timeout(5e3)
|
|
171
225
|
});
|
|
172
226
|
} catch {}
|
|
227
|
+
if (!INJECT_CONTEXT) return;
|
|
173
228
|
const budget = preCompactBudget();
|
|
174
229
|
if (budget === 0) return;
|
|
175
230
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pre-compact.mjs","names":[],"sources":["../../src/secret-store.ts","../../src/hooks/_capture-filter.ts","../../src/hooks/_project.ts","../../src/hooks/pre-compact.ts"],"sourcesContent":["import {\n chmodSync,\n closeSync,\n linkSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeSync,\n} from \"node:fs\";\nimport { randomBytes } from \"node:crypto\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nconst SECRET_KEY = \"AGENTMEMORY_SECRET\";\n\nexport function agentmemoryHomeDir(): string {\n return process.env[\"AGENTMEMORY_DATA_DIR\"] || join(homedir(), \".agentmemory\");\n}\n\nexport function secretFilePath(): string {\n return join(agentmemoryHomeDir(), \"secret\");\n}\n\nfunction usable(value: string | undefined): string {\n if (typeof value !== \"string\") return \"\";\n const trimmed = value.trim();\n if (!trimmed) return \"\";\n if (trimmed.startsWith(\"${\") && trimmed.endsWith(\"}\")) return \"\";\n return trimmed;\n}\n\nfunction unquote(value: string): string {\n const quote = value[0];\n if ((quote === '\"' || quote === \"'\") && value.length > 1) {\n const close = value.indexOf(quote, 1);\n if (close !== -1) return value.slice(1, close);\n }\n const hash = value.indexOf(\" #\");\n return hash === -1 ? value : value.slice(0, hash).trim();\n}\n\nexport function readEnvFileSecret(): string {\n let content: unknown;\n try {\n content = readFileSync(process.env[\"AGENTMEMORY_ENV_FILE\"] || join(homedir(), \".agentmemory\", \".env\"), \"utf-8\");\n } catch {\n return \"\";\n }\n if (typeof content !== \"string\") return \"\";\n let found = \"\";\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed.startsWith(\"#\")) continue;\n const eq = trimmed.indexOf(\"=\");\n if (eq === -1) continue;\n const key = trimmed.slice(0, eq).replace(/^export\\s+/, \"\").trim();\n if (key !== SECRET_KEY) continue;\n found = usable(unquote(trimmed.slice(eq + 1).trim()));\n }\n return found;\n}\n\nfunction readSecretFile(path: string): string {\n try {\n return usable(readFileSync(path, \"utf-8\"));\n } catch {\n return \"\";\n }\n}\n\nfunction legacyDataDirSecretPaths(env: NodeJS.ProcessEnv = process.env): string[] {\n const dataDir = env[\"AGENTMEMORY_DATA_DIR\"];\n if (!dataDir) return [];\n const paths = [join(dataDir, \"secret\"), join(dataDir, \".hmac\"), join(dataDir, \"..\", \".hmac\")];\n return [...new Set(paths.filter((path) => path !== secretFilePath()))];\n}\n\nexport function readStoredSecret(env: NodeJS.ProcessEnv = process.env): string {\n const canonicalPath = canonicalSecretPath();\n const localPath = secretFilePath();\n const legacyPaths = legacyDataDirSecretPaths(env);\n return readSecretFile(canonicalPath) ||\n (localPath === canonicalPath ? \"\" : readSecretFile(localPath)) ||\n legacyPaths.map(readSecretFile).find(Boolean) || \"\";\n}\n\nexport function explicitSecret(env: NodeJS.ProcessEnv = process.env): string {\n return usable(env[SECRET_KEY]) || readEnvFileSecret();\n}\n\nexport function isLoopbackUrl(url: string): boolean {\n let hostname: string;\n try {\n hostname = new URL(url).hostname.toLowerCase();\n } catch {\n return false;\n }\n const bare = hostname.replace(/^\\[|\\]$/g, \"\");\n return bare === \"localhost\" || bare === \"::1\" || /^127(?:\\.\\d{1,3}){3}$/.test(bare);\n}\n\nexport function resolveClientSecret(\n baseUrl: string,\n env: NodeJS.ProcessEnv = process.env,\n): string {\n const fromEnv = usable(env[SECRET_KEY]);\n if (fromEnv) return fromEnv;\n if (env.AGENTMEMORY_CREDENTIAL_MODE === \"explicit\") return \"\";\n if (!isLoopbackUrl(baseUrl)) return \"\";\n return readEnvFileSecret() || readStoredSecret(env);\n}\n\nfunction canonicalSecretPath(): string {\n return join(homedir(), \".agentmemory\", \"secret\");\n}\n\nfunction writeSecretFile(path: string, secret: string): void {\n mkdirSync(join(path, \"..\"), { recursive: true, mode: 0o700 });\n const staging = `${path}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`;\n const fd = openSync(staging, \"wx\", 0o600);\n try {\n writeSync(fd, `${secret}\\n`);\n } finally {\n closeSync(fd);\n }\n try {\n chmodSync(staging, 0o600);\n renameSync(staging, path);\n chmodSync(path, 0o600);\n } finally {\n try { unlinkSync(staging); } catch {}\n }\n}\n\nfunction writeSecretIfAbsent(path: string, secret: string): string {\n mkdirSync(join(path, \"..\"), { recursive: true, mode: 0o700 });\n const staging = `${path}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`;\n const fd = openSync(staging, \"wx\", 0o600);\n try {\n writeSync(fd, `${secret}\\n`);\n } finally {\n closeSync(fd);\n }\n try {\n chmodSync(staging, 0o600);\n linkSync(staging, path);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"EEXIST\") throw err;\n } finally {\n try { unlinkSync(staging); } catch {}\n }\n const stored = readSecretFile(path);\n if (!stored) throw new Error(`Could not read stored API secret at ${path}.`);\n return stored;\n}\n\nexport function ensureServerSecret(env: NodeJS.ProcessEnv = process.env): {\n secret: string;\n source: \"env\" | \"file\" | \"generated\";\n} {\n const configured = explicitSecret(env);\n if (configured) return { secret: configured, source: \"env\" };\n\n const localPath = secretFilePath();\n const canonicalPath = canonicalSecretPath();\n const canonical = localPath === canonicalPath ? \"\" : readSecretFile(canonicalPath);\n if (canonical) {\n if (localPath !== canonicalPath && readSecretFile(localPath) !== canonical) {\n writeSecretFile(localPath, canonical);\n }\n return { secret: canonical, source: \"file\" };\n }\n\n const legacy = readSecretFile(localPath) || legacyDataDirSecretPaths(env).map(readSecretFile).find(Boolean) || \"\";\n const candidate = legacy || randomBytes(32).toString(\"hex\");\n const localSecret = writeSecretIfAbsent(localPath, candidate);\n const secret = localPath === canonicalPath\n ? localSecret\n : writeSecretIfAbsent(canonicalPath, localSecret);\n if (localPath !== canonicalPath && localSecret !== secret) writeSecretFile(localPath, secret);\n return { secret, source: legacy ? \"file\" : \"generated\" };\n}\n\nexport function bearerHeaders(\n baseUrl: string,\n env: NodeJS.ProcessEnv = process.env,\n): Record<string, string> {\n const secret = resolveClientSecret(baseUrl, env);\n return secret ? { Authorization: `Bearer ${secret}` } : {};\n}\n","const DEFAULT_DENY_PATTERNS = [\n \"memory_*\",\n \"toolsearch\",\n \"listmcpresources\",\n \"fetchmcpresource\",\n];\n\nfunction parseEnvList(raw: string | undefined): string[] | undefined {\n if (!raw?.trim()) return undefined;\n return raw\n .split(/[,\\s]+/)\n .map((part) => part.trim())\n .filter(Boolean);\n}\n\nexport function bareToolName(toolName: string): string {\n const trimmed = toolName.trim();\n if (/^mcp__/i.test(trimmed)) {\n const parts = trimmed.split(\"__\");\n if (parts.length >= 3) return parts[parts.length - 1]!;\n }\n return trimmed;\n}\n\nfunction normalizePattern(pattern: string): string {\n return pattern.trim().toLowerCase();\n}\n\nfunction matchesPattern(toolName: string, pattern: string): boolean {\n const bare = bareToolName(toolName).toLowerCase();\n const full = toolName.trim().toLowerCase();\n const pat = normalizePattern(pattern);\n if (!pat.includes(\"*\")) return bare === pat || full === pat;\n const escaped = pat.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const re = new RegExp(`^${escaped.replace(/\\*/g, \".*\")}$`);\n return re.test(bare) || re.test(full);\n}\n\nfunction matchesAny(toolName: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchesPattern(toolName, pattern));\n}\n\nexport function shouldCaptureTool(toolName: unknown): boolean {\n if (typeof toolName !== \"string\" || !toolName.trim()) return true;\n\n const allow = parseEnvList(process.env[\"AGENTMEMORY_CAPTURE_ALLOW\"]);\n if (allow) return matchesAny(toolName, allow);\n\n const deny = [\n ...DEFAULT_DENY_PATTERNS,\n ...(parseEnvList(process.env[\"AGENTMEMORY_CAPTURE_DENY\"]) ?? []),\n ];\n return !matchesAny(toolName, deny);\n}\n\nexport function captureOutputMax(): number {\n const raw = process.env[\"AGENTMEMORY_CAPTURE_OUTPUT_MAX\"];\n if (!raw?.trim()) return 8000;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 8000;\n}\n\nexport function truncateCaptureOutput(value: unknown, max: number): unknown {\n if (typeof value === \"string\" && value.length > max) {\n const suffix = \"\\n[...truncated]\";\n return max <= suffix.length\n ? suffix.slice(0, max)\n : value.slice(0, max - suffix.length) + suffix;\n }\n if (typeof value === \"object\" && value !== null) {\n const str = JSON.stringify(value);\n if (str.length > max) {\n const suffix = \"...[truncated]\";\n return max <= suffix.length\n ? suffix.slice(0, max)\n : str.slice(0, max - suffix.length) + suffix;\n }\n return value;\n }\n return value;\n}\n\nexport function preCompactBudget(): number {\n const raw = process.env[\"AGENTMEMORY_PRE_COMPACT_BUDGET\"];\n if (raw?.trim() === \"0\") return 0;\n if (!raw?.trim()) return 1500;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : 1500;\n}\n","import { execSync } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\n\n// Resolution order: AGENTMEMORY_PROJECT_NAME env → git repository basename → cwd basename.\nexport function resolveProject(cwd?: string): string {\n const explicit = process.env[\"AGENTMEMORY_PROJECT_NAME\"];\n if (explicit && explicit.trim()) return explicit.trim();\n const dir = cwd && cwd.trim() ? cwd : process.cwd();\n try {\n const [top, gitDir, commonDir] = execSync(\n \"git rev-parse --show-toplevel --git-dir --git-common-dir\",\n {\n cwd: dir,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n timeout: 500,\n },\n )\n .toString()\n .trim()\n .split(/\\r?\\n/);\n if (top) return basename(repositoryRoot(dir, top, gitDir, commonDir));\n } catch {}\n return basename(dir);\n}\n\nfunction repositoryRoot(dir: string, top: string, gitDir?: string, commonDir?: string): string {\n if (!gitDir || !commonDir) return top;\n const physical = realpathSync(dir);\n const common = resolve(physical, commonDir);\n if (resolve(physical, gitDir) === common || basename(common) !== \".git\") return top;\n return dirname(common);\n}\n\nexport function hookCwd(data: Record<string, unknown> | null | undefined): string | undefined {\n if (!data || typeof data !== \"object\") return undefined;\n if (typeof data.cwd === \"string\" && data.cwd.trim()) return data.cwd;\n const roots = data.workspace_roots;\n if (Array.isArray(roots)) {\n for (const root of roots) {\n if (typeof root === \"string\" && root.trim()) return root;\n }\n }\n const projectDir =\n process.env[\"DEVIN_PROJECT_DIR\"] || process.env[\"CLAUDE_PROJECT_DIR\"];\n if (projectDir && projectDir.trim()) return projectDir;\n return undefined;\n}\n","#!/usr/bin/env node\nimport { resolveClientSecret } from \"../secret-store.js\";\nimport { preCompactBudget } from \"./_capture-filter.js\";\nimport { resolveProject, hookCwd } from \"./_project.js\";\n\nfunction isSdkChildContext(payload: unknown): boolean {\n if (process.env[\"AGENTMEMORY_SDK_CHILD\"] === \"1\") return true;\n if (!payload || typeof payload !== \"object\") return false;\n return (payload as { entrypoint?: unknown }).entrypoint === \"sdk-ts\";\n}\n\nconst REST_URL = process.env[\"AGENTMEMORY_URL\"] || \"http://localhost:3111\";\nconst SECRET = resolveClientSecret(REST_URL);\n\nfunction authHeaders(): Record<string, string> {\n const h: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (SECRET) h[\"Authorization\"] = `Bearer ${SECRET}`;\n return h;\n}\n\nasync function main() {\n let input = \"\";\n for await (const chunk of process.stdin) {\n input += chunk;\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(input);\n } catch {\n return;\n }\n\n if (!data || typeof data !== \"object\") return;\n if (isSdkChildContext(data)) return;\n\n const sessionId = ((data.session_id || data.sessionId || data.conversation_id) as string) || \"unknown\";\n const project = resolveProject(hookCwd(data));\n\n if (process.env[\"CLAUDE_MEMORY_BRIDGE\"] === \"true\") {\n try {\n await fetch(`${REST_URL}/agentmemory/claude-bridge/sync`, {\n method: \"POST\",\n headers: authHeaders(),\n body: JSON.stringify({}),\n signal: AbortSignal.timeout(5000),\n });\n } catch {\n // best-effort\n }\n }\n\n const budget = preCompactBudget();\n if (budget === 0) return;\n\n try {\n const res = await fetch(`${REST_URL}/agentmemory/context`, {\n method: \"POST\",\n headers: authHeaders(),\n body: JSON.stringify({ sessionId, project, budget }),\n signal: AbortSignal.timeout(5000),\n });\n\n if (res.ok) {\n const result = (await res.json()) as { context?: string };\n if (result.context) {\n process.stdout.write(result.context);\n }\n }\n } catch {\n // best effort -- don't block compaction\n }\n}\n\nmain().catch(() => process.exit(0));\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pre-compact.mjs","names":[],"sources":["../../src/hooks/_capture-filter.ts","../../src/secret-store.ts","../../src/hooks/_endpoint.ts","../../src/hooks/_project.ts","../../src/hooks/pre-compact.ts"],"sourcesContent":["const DEFAULT_DENY_PATTERNS = [\n \"memory_*\",\n \"toolsearch\",\n \"listmcpresources\",\n \"fetchmcpresource\",\n];\n\nfunction parseEnvList(raw: string | undefined): string[] | undefined {\n if (!raw?.trim()) return undefined;\n return raw\n .split(/[,\\s]+/)\n .map((part) => part.trim())\n .filter(Boolean);\n}\n\nexport function bareToolName(toolName: string): string {\n const trimmed = toolName.trim();\n if (/^mcp__/i.test(trimmed)) {\n const parts = trimmed.split(\"__\");\n if (parts.length >= 3) return parts[parts.length - 1]!;\n }\n return trimmed;\n}\n\nfunction normalizePattern(pattern: string): string {\n return pattern.trim().toLowerCase();\n}\n\nfunction matchesPattern(toolName: string, pattern: string): boolean {\n const bare = bareToolName(toolName).toLowerCase();\n const full = toolName.trim().toLowerCase();\n const pat = normalizePattern(pattern);\n if (!pat.includes(\"*\")) return bare === pat || full === pat;\n const escaped = pat.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const re = new RegExp(`^${escaped.replace(/\\*/g, \".*\")}$`);\n return re.test(bare) || re.test(full);\n}\n\nfunction matchesAny(toolName: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchesPattern(toolName, pattern));\n}\n\nexport function shouldCaptureTool(toolName: unknown): boolean {\n if (typeof toolName !== \"string\" || !toolName.trim()) return true;\n\n const allow = parseEnvList(process.env[\"AGENTMEMORY_CAPTURE_ALLOW\"]);\n if (allow) return matchesAny(toolName, allow);\n\n const deny = [\n ...DEFAULT_DENY_PATTERNS,\n ...(parseEnvList(process.env[\"AGENTMEMORY_CAPTURE_DENY\"]) ?? []),\n ];\n return !matchesAny(toolName, deny);\n}\n\nexport function captureOutputMax(): number {\n const raw = process.env[\"AGENTMEMORY_CAPTURE_OUTPUT_MAX\"];\n if (!raw?.trim()) return 8000;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 8000;\n}\n\nexport function truncateText(value: string, max: number): string {\n if (value.length <= max) return value;\n const end = /[\\uD800-\\uDBFF]/.test(value[max - 1] ?? \"\") ? max - 1 : max;\n return value.slice(0, end);\n}\n\nexport function truncateCaptureOutput(value: unknown, max: number): unknown {\n if (typeof value === \"string\" && value.length > max) {\n const suffix = \"\\n[...truncated]\";\n return max <= suffix.length\n ? suffix.slice(0, max)\n : truncateText(value, max - suffix.length) + suffix;\n }\n if (typeof value === \"object\" && value !== null) {\n const str = JSON.stringify(value);\n if (str.length > max) {\n const suffix = \"...[truncated]\";\n return max <= suffix.length\n ? suffix.slice(0, max)\n : truncateText(str, max - suffix.length) + suffix;\n }\n return value;\n }\n return value;\n}\n\nexport function preCompactBudget(): number {\n const raw = process.env[\"AGENTMEMORY_PRE_COMPACT_BUDGET\"];\n if (raw?.trim() === \"0\") return 0;\n if (!raw?.trim()) return 1500;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : 1500;\n}\n","import {\n chmodSync,\n closeSync,\n linkSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeSync,\n} from \"node:fs\";\nimport { randomBytes } from \"node:crypto\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nconst SECRET_KEY = \"AGENTMEMORY_SECRET\";\n\nexport function agentmemoryHomeDir(): string {\n return process.env[\"AGENTMEMORY_DATA_DIR\"] || join(homedir(), \".agentmemory\");\n}\n\nexport function secretFilePath(): string {\n return join(agentmemoryHomeDir(), \"secret\");\n}\n\nfunction usable(value: string | undefined): string {\n if (typeof value !== \"string\") return \"\";\n const trimmed = value.trim();\n if (!trimmed) return \"\";\n if (trimmed.startsWith(\"${\") && trimmed.endsWith(\"}\")) return \"\";\n return trimmed;\n}\n\nfunction unquote(value: string): string {\n const quote = value[0];\n if ((quote === '\"' || quote === \"'\") && value.length > 1) {\n const close = value.indexOf(quote, 1);\n if (close !== -1) return value.slice(1, close);\n }\n const hash = value.indexOf(\" #\");\n return hash === -1 ? value : value.slice(0, hash).trim();\n}\n\nexport function readEnvFileSecret(): string {\n let content: unknown;\n try {\n content = readFileSync(process.env[\"AGENTMEMORY_ENV_FILE\"] || join(homedir(), \".agentmemory\", \".env\"), \"utf-8\");\n } catch {\n return \"\";\n }\n if (typeof content !== \"string\") return \"\";\n let found = \"\";\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed.startsWith(\"#\")) continue;\n const eq = trimmed.indexOf(\"=\");\n if (eq === -1) continue;\n const key = trimmed.slice(0, eq).replace(/^export\\s+/, \"\").trim();\n if (key !== SECRET_KEY) continue;\n found = usable(unquote(trimmed.slice(eq + 1).trim()));\n }\n return found;\n}\n\nfunction readSecretFile(path: string): string {\n try {\n return usable(readFileSync(path, \"utf-8\"));\n } catch {\n return \"\";\n }\n}\n\nfunction legacyDataDirSecretPaths(env: NodeJS.ProcessEnv = process.env): string[] {\n const dataDir = env[\"AGENTMEMORY_DATA_DIR\"];\n if (!dataDir) return [];\n const paths = [join(dataDir, \"secret\"), join(dataDir, \".hmac\"), join(dataDir, \"..\", \".hmac\")];\n return [...new Set(paths.filter((path) => path !== secretFilePath()))];\n}\n\nexport function readStoredSecret(env: NodeJS.ProcessEnv = process.env): string {\n const canonicalPath = canonicalSecretPath();\n const localPath = secretFilePath();\n const legacyPaths = legacyDataDirSecretPaths(env);\n return readSecretFile(canonicalPath) ||\n (localPath === canonicalPath ? \"\" : readSecretFile(localPath)) ||\n legacyPaths.map(readSecretFile).find(Boolean) || \"\";\n}\n\nexport function explicitSecret(env: NodeJS.ProcessEnv = process.env): string {\n return usable(env[SECRET_KEY]) || readEnvFileSecret();\n}\n\nexport function isLoopbackUrl(url: string): boolean {\n let hostname: string;\n try {\n hostname = new URL(url).hostname.toLowerCase();\n } catch {\n return false;\n }\n const bare = hostname.replace(/^\\[|\\]$/g, \"\");\n return bare === \"localhost\" || bare === \"::1\" || /^127(?:\\.\\d{1,3}){3}$/.test(bare);\n}\n\nexport function resolveClientSecret(\n baseUrl: string,\n env: NodeJS.ProcessEnv = process.env,\n): string {\n const fromEnv = usable(env[SECRET_KEY]);\n if (fromEnv) return fromEnv;\n if (env.AGENTMEMORY_CREDENTIAL_MODE === \"explicit\") return \"\";\n if (!isLoopbackUrl(baseUrl)) return \"\";\n return readEnvFileSecret() || readStoredSecret(env);\n}\n\nfunction canonicalSecretPath(): string {\n return join(homedir(), \".agentmemory\", \"secret\");\n}\n\nfunction writeSecretFile(path: string, secret: string): void {\n mkdirSync(join(path, \"..\"), { recursive: true, mode: 0o700 });\n const staging = `${path}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`;\n const fd = openSync(staging, \"wx\", 0o600);\n try {\n writeSync(fd, `${secret}\\n`);\n } finally {\n closeSync(fd);\n }\n try {\n chmodSync(staging, 0o600);\n renameSync(staging, path);\n chmodSync(path, 0o600);\n } finally {\n try { unlinkSync(staging); } catch {}\n }\n}\n\nfunction writeSecretIfAbsent(path: string, secret: string): string {\n mkdirSync(join(path, \"..\"), { recursive: true, mode: 0o700 });\n const staging = `${path}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`;\n const fd = openSync(staging, \"wx\", 0o600);\n try {\n writeSync(fd, `${secret}\\n`);\n } finally {\n closeSync(fd);\n }\n try {\n chmodSync(staging, 0o600);\n linkSync(staging, path);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"EEXIST\") throw err;\n } finally {\n try { unlinkSync(staging); } catch {}\n }\n const stored = readSecretFile(path);\n if (!stored) throw new Error(`Could not read stored API secret at ${path}.`);\n return stored;\n}\n\nexport function ensureServerSecret(env: NodeJS.ProcessEnv = process.env): {\n secret: string;\n source: \"env\" | \"file\" | \"generated\";\n} {\n const configured = explicitSecret(env);\n if (configured) return { secret: configured, source: \"env\" };\n\n const localPath = secretFilePath();\n const canonicalPath = canonicalSecretPath();\n const canonical = localPath === canonicalPath ? \"\" : readSecretFile(canonicalPath);\n if (canonical) {\n if (localPath !== canonicalPath && readSecretFile(localPath) !== canonical) {\n writeSecretFile(localPath, canonical);\n }\n return { secret: canonical, source: \"file\" };\n }\n\n const legacy = readSecretFile(localPath) || legacyDataDirSecretPaths(env).map(readSecretFile).find(Boolean) || \"\";\n const candidate = legacy || randomBytes(32).toString(\"hex\");\n const localSecret = writeSecretIfAbsent(localPath, candidate);\n const secret = localPath === canonicalPath\n ? localSecret\n : writeSecretIfAbsent(canonicalPath, localSecret);\n if (localPath !== canonicalPath && localSecret !== secret) writeSecretFile(localPath, secret);\n return { secret, source: legacy ? \"file\" : \"generated\" };\n}\n\nexport function bearerHeaders(\n baseUrl: string,\n env: NodeJS.ProcessEnv = process.env,\n): Record<string, string> {\n const secret = resolveClientSecret(baseUrl, env);\n return secret ? { Authorization: `Bearer ${secret}` } : {};\n}\n","import { isLoopbackUrl, resolveClientSecret } from \"../secret-store.js\";\n\nconst OPTION_PREFIX = \"CLAUDE_PLUGIN_OPTION_\";\nconst DEFAULT_URL = \"http://localhost:3111\";\n\nexport interface HookEndpoint {\n url: string;\n secret: string;\n blocked: boolean;\n}\n\nfunction filled(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n if (!trimmed || (trimmed.startsWith(\"${\") && trimmed.endsWith(\"}\"))) return undefined;\n return value;\n}\n\nexport function hookSetting(\n name: string,\n env: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n const optionKey = `${OPTION_PREFIX}${name}`;\n const option = filled(env[optionKey])?.trim();\n if (option) return option;\n if (name === \"AGENTMEMORY_SECRET\" && optionKey in env) return undefined;\n return filled(env[name]);\n}\n\nexport function resolveSettingSecret(url: string, env: NodeJS.ProcessEnv = process.env): string {\n const secretEnv: NodeJS.ProcessEnv = { ...env };\n const explicit = hookSetting(\"AGENTMEMORY_SECRET\", env);\n if (explicit === undefined) delete secretEnv[\"AGENTMEMORY_SECRET\"];\n else secretEnv[\"AGENTMEMORY_SECRET\"] = explicit;\n return resolveClientSecret(url, secretEnv);\n}\n\nexport function secretTransportAllowed(url: string): boolean {\n try {\n return new URL(url).protocol === \"https:\" || isLoopbackUrl(url);\n } catch {\n return false;\n }\n}\n\nexport function resolveHookEndpoint(env: NodeJS.ProcessEnv = process.env): HookEndpoint {\n const url = hookSetting(\"AGENTMEMORY_URL\", env) || DEFAULT_URL;\n const secret = resolveSettingSecret(url, env);\n const blocked = !secretTransportAllowed(url);\n return { url, secret: blocked ? \"\" : secret, blocked };\n}\n\nlet blockedNoticeShown = false;\n\nexport function reportBlockedTransport(endpoint: HookEndpoint): boolean {\n if (!endpoint.blocked) return false;\n if (!blockedNoticeShown) {\n blockedNoticeShown = true;\n process.stderr.write(\n \"[agentmemory] AGENTMEMORY_URL must use HTTPS unless it is a loopback address. Nothing was sent.\\n\",\n );\n }\n return true;\n}\n\nexport function hookAuthHeaders(endpoint: HookEndpoint): Record<string, string> {\n const h: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (endpoint.secret) h[\"Authorization\"] = `Bearer ${endpoint.secret}`;\n return h;\n}\n","import { execSync } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\n\n// Resolution order: AGENTMEMORY_PROJECT_NAME env → git repository basename → cwd basename.\nexport function resolveProject(cwd?: string): string {\n const explicit = process.env[\"AGENTMEMORY_PROJECT_NAME\"];\n if (explicit && explicit.trim()) return explicit.trim();\n const dir = cwd && cwd.trim() ? cwd : process.cwd();\n try {\n const [top, gitDir, commonDir] = execSync(\n \"git rev-parse --show-toplevel --git-dir --git-common-dir\",\n {\n cwd: dir,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n timeout: 500,\n },\n )\n .toString()\n .trim()\n .split(/\\r?\\n/);\n if (top) return basename(repositoryRoot(dir, top, gitDir, commonDir));\n } catch {}\n return basename(dir);\n}\n\nfunction repositoryRoot(dir: string, top: string, gitDir?: string, commonDir?: string): string {\n if (!gitDir || !commonDir) return top;\n const physical = realpathSync(dir);\n const common = resolve(physical, commonDir);\n if (resolve(physical, gitDir) === common || basename(common) !== \".git\") return top;\n return dirname(common);\n}\n\nexport function hookCwd(data: Record<string, unknown> | null | undefined): string | undefined {\n if (!data || typeof data !== \"object\") return undefined;\n if (typeof data.cwd === \"string\" && data.cwd.trim()) return data.cwd;\n const roots = data.workspace_roots;\n if (Array.isArray(roots)) {\n for (const root of roots) {\n if (typeof root === \"string\" && root.trim()) return root;\n }\n }\n const projectDir =\n process.env[\"DEVIN_PROJECT_DIR\"] || process.env[\"CLAUDE_PROJECT_DIR\"];\n if (projectDir && projectDir.trim()) return projectDir;\n return undefined;\n}\n","#!/usr/bin/env node\nimport { preCompactBudget } from \"./_capture-filter.js\";\nimport { hookAuthHeaders, reportBlockedTransport, resolveHookEndpoint } from \"./_endpoint.js\";\nimport { resolveProject, hookCwd } from \"./_project.js\";\n\nfunction isSdkChildContext(payload: unknown): boolean {\n if (process.env[\"AGENTMEMORY_SDK_CHILD\"] === \"1\") return true;\n if (!payload || typeof payload !== \"object\") return false;\n return (payload as { entrypoint?: unknown }).entrypoint === \"sdk-ts\";\n}\n\nconst INJECT_CONTEXT = process.env[\"AGENTMEMORY_INJECT_CONTEXT\"] === \"true\";\nconst ENDPOINT = resolveHookEndpoint();\nconst REST_URL = ENDPOINT.url;\n\nfunction authHeaders(): Record<string, string> {\n return hookAuthHeaders(ENDPOINT);\n}\n\nasync function main() {\n let input = \"\";\n for await (const chunk of process.stdin) {\n input += chunk;\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(input);\n } catch {\n return;\n }\n\n if (!data || typeof data !== \"object\") return;\n if (isSdkChildContext(data)) return;\n if (reportBlockedTransport(ENDPOINT)) return;\n\n const sessionId = ((data.session_id || data.sessionId || data.conversation_id) as string) || \"unknown\";\n const project = resolveProject(hookCwd(data));\n\n if (process.env[\"CLAUDE_MEMORY_BRIDGE\"] === \"true\") {\n try {\n await fetch(`${REST_URL}/agentmemory/claude-bridge/sync`, {\n method: \"POST\",\n headers: authHeaders(),\n body: JSON.stringify({}),\n signal: AbortSignal.timeout(5000),\n });\n } catch {\n // best-effort\n }\n }\n\n if (!INJECT_CONTEXT) return;\n const budget = preCompactBudget();\n if (budget === 0) return;\n\n try {\n const res = await fetch(`${REST_URL}/agentmemory/context`, {\n method: \"POST\",\n headers: authHeaders(),\n body: JSON.stringify({ sessionId, project, budget }),\n signal: AbortSignal.timeout(5000),\n });\n\n if (res.ok) {\n const result = (await res.json()) as { context?: string };\n if (result.context) {\n process.stdout.write(result.context);\n }\n }\n } catch {\n // best effort -- don't block compaction\n }\n}\n\nmain().catch(() => process.exit(0));\n"],"mappings":"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"}
|
|
@@ -89,6 +89,60 @@ function canonicalSecretPath() {
|
|
|
89
89
|
return join(homedir(), ".agentmemory", "secret");
|
|
90
90
|
}
|
|
91
91
|
//#endregion
|
|
92
|
+
//#region src/hooks/_endpoint.ts
|
|
93
|
+
const OPTION_PREFIX = "CLAUDE_PLUGIN_OPTION_";
|
|
94
|
+
const DEFAULT_URL = "http://localhost:3111";
|
|
95
|
+
function filled(value) {
|
|
96
|
+
const trimmed = value?.trim();
|
|
97
|
+
if (!trimmed || trimmed.startsWith("${") && trimmed.endsWith("}")) return void 0;
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
function hookSetting(name, env = process.env) {
|
|
101
|
+
const optionKey = `${OPTION_PREFIX}${name}`;
|
|
102
|
+
const option = filled(env[optionKey])?.trim();
|
|
103
|
+
if (option) return option;
|
|
104
|
+
if (name === "AGENTMEMORY_SECRET" && optionKey in env) return void 0;
|
|
105
|
+
return filled(env[name]);
|
|
106
|
+
}
|
|
107
|
+
function resolveSettingSecret(url, env = process.env) {
|
|
108
|
+
const secretEnv = { ...env };
|
|
109
|
+
const explicit = hookSetting("AGENTMEMORY_SECRET", env);
|
|
110
|
+
if (explicit === void 0) delete secretEnv["AGENTMEMORY_SECRET"];
|
|
111
|
+
else secretEnv["AGENTMEMORY_SECRET"] = explicit;
|
|
112
|
+
return resolveClientSecret(url, secretEnv);
|
|
113
|
+
}
|
|
114
|
+
function secretTransportAllowed(url) {
|
|
115
|
+
try {
|
|
116
|
+
return new URL(url).protocol === "https:" || isLoopbackUrl(url);
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function resolveHookEndpoint(env = process.env) {
|
|
122
|
+
const url = hookSetting("AGENTMEMORY_URL", env) || DEFAULT_URL;
|
|
123
|
+
const secret = resolveSettingSecret(url, env);
|
|
124
|
+
const blocked = !secretTransportAllowed(url);
|
|
125
|
+
return {
|
|
126
|
+
url,
|
|
127
|
+
secret: blocked ? "" : secret,
|
|
128
|
+
blocked
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
let blockedNoticeShown = false;
|
|
132
|
+
function reportBlockedTransport(endpoint) {
|
|
133
|
+
if (!endpoint.blocked) return false;
|
|
134
|
+
if (!blockedNoticeShown) {
|
|
135
|
+
blockedNoticeShown = true;
|
|
136
|
+
process.stderr.write("[agentmemory] AGENTMEMORY_URL must use HTTPS unless it is a loopback address. Nothing was sent.\n");
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
function hookAuthHeaders(endpoint) {
|
|
141
|
+
const h = { "Content-Type": "application/json" };
|
|
142
|
+
if (endpoint.secret) h["Authorization"] = `Bearer ${endpoint.secret}`;
|
|
143
|
+
return h;
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
92
146
|
//#region src/hooks/pre-tool-use.ts
|
|
93
147
|
function isSdkChildContext(payload) {
|
|
94
148
|
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
|
@@ -96,15 +150,14 @@ function isSdkChildContext(payload) {
|
|
|
96
150
|
return payload.entrypoint === "sdk-ts";
|
|
97
151
|
}
|
|
98
152
|
const INJECT_CONTEXT = process.env["AGENTMEMORY_INJECT_CONTEXT"] === "true";
|
|
99
|
-
const
|
|
100
|
-
const
|
|
153
|
+
const ENDPOINT = resolveHookEndpoint();
|
|
154
|
+
const REST_URL = ENDPOINT.url;
|
|
101
155
|
function authHeaders() {
|
|
102
|
-
|
|
103
|
-
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
|
104
|
-
return h;
|
|
156
|
+
return hookAuthHeaders(ENDPOINT);
|
|
105
157
|
}
|
|
106
158
|
async function main() {
|
|
107
159
|
if (!INJECT_CONTEXT) return;
|
|
160
|
+
if (reportBlockedTransport(ENDPOINT)) return;
|
|
108
161
|
let input = "";
|
|
109
162
|
for await (const chunk of process.stdin) input += chunk;
|
|
110
163
|
let data;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pre-tool-use.mjs","names":[],"sources":["../../src/secret-store.ts","../../src/hooks/pre-tool-use.ts"],"sourcesContent":["import {\n chmodSync,\n closeSync,\n linkSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeSync,\n} from \"node:fs\";\nimport { randomBytes } from \"node:crypto\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nconst SECRET_KEY = \"AGENTMEMORY_SECRET\";\n\nexport function agentmemoryHomeDir(): string {\n return process.env[\"AGENTMEMORY_DATA_DIR\"] || join(homedir(), \".agentmemory\");\n}\n\nexport function secretFilePath(): string {\n return join(agentmemoryHomeDir(), \"secret\");\n}\n\nfunction usable(value: string | undefined): string {\n if (typeof value !== \"string\") return \"\";\n const trimmed = value.trim();\n if (!trimmed) return \"\";\n if (trimmed.startsWith(\"${\") && trimmed.endsWith(\"}\")) return \"\";\n return trimmed;\n}\n\nfunction unquote(value: string): string {\n const quote = value[0];\n if ((quote === '\"' || quote === \"'\") && value.length > 1) {\n const close = value.indexOf(quote, 1);\n if (close !== -1) return value.slice(1, close);\n }\n const hash = value.indexOf(\" #\");\n return hash === -1 ? value : value.slice(0, hash).trim();\n}\n\nexport function readEnvFileSecret(): string {\n let content: unknown;\n try {\n content = readFileSync(process.env[\"AGENTMEMORY_ENV_FILE\"] || join(homedir(), \".agentmemory\", \".env\"), \"utf-8\");\n } catch {\n return \"\";\n }\n if (typeof content !== \"string\") return \"\";\n let found = \"\";\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed.startsWith(\"#\")) continue;\n const eq = trimmed.indexOf(\"=\");\n if (eq === -1) continue;\n const key = trimmed.slice(0, eq).replace(/^export\\s+/, \"\").trim();\n if (key !== SECRET_KEY) continue;\n found = usable(unquote(trimmed.slice(eq + 1).trim()));\n }\n return found;\n}\n\nfunction readSecretFile(path: string): string {\n try {\n return usable(readFileSync(path, \"utf-8\"));\n } catch {\n return \"\";\n }\n}\n\nfunction legacyDataDirSecretPaths(env: NodeJS.ProcessEnv = process.env): string[] {\n const dataDir = env[\"AGENTMEMORY_DATA_DIR\"];\n if (!dataDir) return [];\n const paths = [join(dataDir, \"secret\"), join(dataDir, \".hmac\"), join(dataDir, \"..\", \".hmac\")];\n return [...new Set(paths.filter((path) => path !== secretFilePath()))];\n}\n\nexport function readStoredSecret(env: NodeJS.ProcessEnv = process.env): string {\n const canonicalPath = canonicalSecretPath();\n const localPath = secretFilePath();\n const legacyPaths = legacyDataDirSecretPaths(env);\n return readSecretFile(canonicalPath) ||\n (localPath === canonicalPath ? \"\" : readSecretFile(localPath)) ||\n legacyPaths.map(readSecretFile).find(Boolean) || \"\";\n}\n\nexport function explicitSecret(env: NodeJS.ProcessEnv = process.env): string {\n return usable(env[SECRET_KEY]) || readEnvFileSecret();\n}\n\nexport function isLoopbackUrl(url: string): boolean {\n let hostname: string;\n try {\n hostname = new URL(url).hostname.toLowerCase();\n } catch {\n return false;\n }\n const bare = hostname.replace(/^\\[|\\]$/g, \"\");\n return bare === \"localhost\" || bare === \"::1\" || /^127(?:\\.\\d{1,3}){3}$/.test(bare);\n}\n\nexport function resolveClientSecret(\n baseUrl: string,\n env: NodeJS.ProcessEnv = process.env,\n): string {\n const fromEnv = usable(env[SECRET_KEY]);\n if (fromEnv) return fromEnv;\n if (env.AGENTMEMORY_CREDENTIAL_MODE === \"explicit\") return \"\";\n if (!isLoopbackUrl(baseUrl)) return \"\";\n return readEnvFileSecret() || readStoredSecret(env);\n}\n\nfunction canonicalSecretPath(): string {\n return join(homedir(), \".agentmemory\", \"secret\");\n}\n\nfunction writeSecretFile(path: string, secret: string): void {\n mkdirSync(join(path, \"..\"), { recursive: true, mode: 0o700 });\n const staging = `${path}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`;\n const fd = openSync(staging, \"wx\", 0o600);\n try {\n writeSync(fd, `${secret}\\n`);\n } finally {\n closeSync(fd);\n }\n try {\n chmodSync(staging, 0o600);\n renameSync(staging, path);\n chmodSync(path, 0o600);\n } finally {\n try { unlinkSync(staging); } catch {}\n }\n}\n\nfunction writeSecretIfAbsent(path: string, secret: string): string {\n mkdirSync(join(path, \"..\"), { recursive: true, mode: 0o700 });\n const staging = `${path}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`;\n const fd = openSync(staging, \"wx\", 0o600);\n try {\n writeSync(fd, `${secret}\\n`);\n } finally {\n closeSync(fd);\n }\n try {\n chmodSync(staging, 0o600);\n linkSync(staging, path);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"EEXIST\") throw err;\n } finally {\n try { unlinkSync(staging); } catch {}\n }\n const stored = readSecretFile(path);\n if (!stored) throw new Error(`Could not read stored API secret at ${path}.`);\n return stored;\n}\n\nexport function ensureServerSecret(env: NodeJS.ProcessEnv = process.env): {\n secret: string;\n source: \"env\" | \"file\" | \"generated\";\n} {\n const configured = explicitSecret(env);\n if (configured) return { secret: configured, source: \"env\" };\n\n const localPath = secretFilePath();\n const canonicalPath = canonicalSecretPath();\n const canonical = localPath === canonicalPath ? \"\" : readSecretFile(canonicalPath);\n if (canonical) {\n if (localPath !== canonicalPath && readSecretFile(localPath) !== canonical) {\n writeSecretFile(localPath, canonical);\n }\n return { secret: canonical, source: \"file\" };\n }\n\n const legacy = readSecretFile(localPath) || legacyDataDirSecretPaths(env).map(readSecretFile).find(Boolean) || \"\";\n const candidate = legacy || randomBytes(32).toString(\"hex\");\n const localSecret = writeSecretIfAbsent(localPath, candidate);\n const secret = localPath === canonicalPath\n ? localSecret\n : writeSecretIfAbsent(canonicalPath, localSecret);\n if (localPath !== canonicalPath && localSecret !== secret) writeSecretFile(localPath, secret);\n return { secret, source: legacy ? \"file\" : \"generated\" };\n}\n\nexport function bearerHeaders(\n baseUrl: string,\n env: NodeJS.ProcessEnv = process.env,\n): Record<string, string> {\n const secret = resolveClientSecret(baseUrl, env);\n return secret ? { Authorization: `Bearer ${secret}` } : {};\n}\n","#!/usr/bin/env node\nimport { resolveClientSecret } from \"../secret-store.js\";\n\nfunction isSdkChildContext(payload: unknown): boolean {\n if (process.env[\"AGENTMEMORY_SDK_CHILD\"] === \"1\") return true;\n if (!payload || typeof payload !== \"object\") return false;\n return (payload as { entrypoint?: unknown }).entrypoint === \"sdk-ts\";\n}\n\nconst INJECT_CONTEXT = process.env[\"AGENTMEMORY_INJECT_CONTEXT\"] === \"true\";\n\nconst REST_URL = process.env[\"AGENTMEMORY_URL\"] || \"http://localhost:3111\";\nconst SECRET = resolveClientSecret(REST_URL);\n\nfunction authHeaders(): Record<string, string> {\n const h: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (SECRET) h[\"Authorization\"] = `Bearer ${SECRET}`;\n return h;\n}\n\nasync function main() {\n // Default off: exit immediately so we don't even open stdin. This keeps\n // Claude Code's tool-call hot path as cheap as possible.\n if (!INJECT_CONTEXT) return;\n\n let input = \"\";\n for await (const chunk of process.stdin) {\n input += chunk;\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(input);\n } catch {\n return;\n }\n\n if (!data || typeof data !== \"object\") return;\n if (isSdkChildContext(data)) return;\n\n const toolName =\n typeof data.tool_name === \"string\"\n ? data.tool_name\n : typeof data.toolName === \"string\"\n ? data.toolName\n : undefined;\n if (!toolName) return;\n\n const normalizedToolName = toolName.toLowerCase();\n const fileTools = [\"edit\", \"write\", \"create\", \"read\", \"view\", \"glob\", \"grep\"];\n if (!fileTools.includes(normalizedToolName)) return;\n\n const rawToolInput = data.tool_input ?? data.toolArgs;\n const toolInput =\n typeof rawToolInput === \"object\" &&\n rawToolInput !== null &&\n !Array.isArray(rawToolInput)\n ? (rawToolInput as Record<string, unknown>)\n : {};\n const files: string[] = [];\n const fileKeys =\n normalizedToolName === \"grep\"\n ? [\"path\", \"file\"]\n : [\"file_path\", \"path\", \"file\", \"pattern\"];\n for (const key of fileKeys) {\n const val = toolInput[key];\n if (typeof val === \"string\" && val.length > 0) files.push(val);\n }\n if (files.length === 0) return;\n\n const terms: string[] = [];\n if (normalizedToolName === \"grep\" || normalizedToolName === \"glob\") {\n const pattern = toolInput[\"pattern\"];\n if (typeof pattern === \"string\" && pattern.length > 0) {\n terms.push(pattern);\n }\n }\n\n const rawSessionId = data.session_id || data.sessionId || data.conversation_id;\n const sessionId =\n typeof rawSessionId === \"string\" && rawSessionId.length > 0\n ? rawSessionId\n : \"unknown\";\n const project =\n typeof data.project === \"string\" && data.project.trim().length > 0\n ? data.project.trim()\n : undefined;\n\n try {\n const res = await fetch(`${REST_URL}/agentmemory/enrich`, {\n method: \"POST\",\n headers: authHeaders(),\n body: JSON.stringify({\n sessionId,\n files,\n terms,\n toolName,\n ...(project !== undefined && { project }),\n }),\n signal: AbortSignal.timeout(2000),\n });\n\n if (res.ok) {\n const result = (await res.json()) as { context?: string };\n if (result.context) {\n process.stdout.write(result.context);\n }\n }\n } catch {\n // don't block tool execution\n }\n}\n\nmain().catch(() => process.exit(0));\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pre-tool-use.mjs","names":[],"sources":["../../src/secret-store.ts","../../src/hooks/_endpoint.ts","../../src/hooks/pre-tool-use.ts"],"sourcesContent":["import {\n chmodSync,\n closeSync,\n linkSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeSync,\n} from \"node:fs\";\nimport { randomBytes } from \"node:crypto\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nconst SECRET_KEY = \"AGENTMEMORY_SECRET\";\n\nexport function agentmemoryHomeDir(): string {\n return process.env[\"AGENTMEMORY_DATA_DIR\"] || join(homedir(), \".agentmemory\");\n}\n\nexport function secretFilePath(): string {\n return join(agentmemoryHomeDir(), \"secret\");\n}\n\nfunction usable(value: string | undefined): string {\n if (typeof value !== \"string\") return \"\";\n const trimmed = value.trim();\n if (!trimmed) return \"\";\n if (trimmed.startsWith(\"${\") && trimmed.endsWith(\"}\")) return \"\";\n return trimmed;\n}\n\nfunction unquote(value: string): string {\n const quote = value[0];\n if ((quote === '\"' || quote === \"'\") && value.length > 1) {\n const close = value.indexOf(quote, 1);\n if (close !== -1) return value.slice(1, close);\n }\n const hash = value.indexOf(\" #\");\n return hash === -1 ? value : value.slice(0, hash).trim();\n}\n\nexport function readEnvFileSecret(): string {\n let content: unknown;\n try {\n content = readFileSync(process.env[\"AGENTMEMORY_ENV_FILE\"] || join(homedir(), \".agentmemory\", \".env\"), \"utf-8\");\n } catch {\n return \"\";\n }\n if (typeof content !== \"string\") return \"\";\n let found = \"\";\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed.startsWith(\"#\")) continue;\n const eq = trimmed.indexOf(\"=\");\n if (eq === -1) continue;\n const key = trimmed.slice(0, eq).replace(/^export\\s+/, \"\").trim();\n if (key !== SECRET_KEY) continue;\n found = usable(unquote(trimmed.slice(eq + 1).trim()));\n }\n return found;\n}\n\nfunction readSecretFile(path: string): string {\n try {\n return usable(readFileSync(path, \"utf-8\"));\n } catch {\n return \"\";\n }\n}\n\nfunction legacyDataDirSecretPaths(env: NodeJS.ProcessEnv = process.env): string[] {\n const dataDir = env[\"AGENTMEMORY_DATA_DIR\"];\n if (!dataDir) return [];\n const paths = [join(dataDir, \"secret\"), join(dataDir, \".hmac\"), join(dataDir, \"..\", \".hmac\")];\n return [...new Set(paths.filter((path) => path !== secretFilePath()))];\n}\n\nexport function readStoredSecret(env: NodeJS.ProcessEnv = process.env): string {\n const canonicalPath = canonicalSecretPath();\n const localPath = secretFilePath();\n const legacyPaths = legacyDataDirSecretPaths(env);\n return readSecretFile(canonicalPath) ||\n (localPath === canonicalPath ? \"\" : readSecretFile(localPath)) ||\n legacyPaths.map(readSecretFile).find(Boolean) || \"\";\n}\n\nexport function explicitSecret(env: NodeJS.ProcessEnv = process.env): string {\n return usable(env[SECRET_KEY]) || readEnvFileSecret();\n}\n\nexport function isLoopbackUrl(url: string): boolean {\n let hostname: string;\n try {\n hostname = new URL(url).hostname.toLowerCase();\n } catch {\n return false;\n }\n const bare = hostname.replace(/^\\[|\\]$/g, \"\");\n return bare === \"localhost\" || bare === \"::1\" || /^127(?:\\.\\d{1,3}){3}$/.test(bare);\n}\n\nexport function resolveClientSecret(\n baseUrl: string,\n env: NodeJS.ProcessEnv = process.env,\n): string {\n const fromEnv = usable(env[SECRET_KEY]);\n if (fromEnv) return fromEnv;\n if (env.AGENTMEMORY_CREDENTIAL_MODE === \"explicit\") return \"\";\n if (!isLoopbackUrl(baseUrl)) return \"\";\n return readEnvFileSecret() || readStoredSecret(env);\n}\n\nfunction canonicalSecretPath(): string {\n return join(homedir(), \".agentmemory\", \"secret\");\n}\n\nfunction writeSecretFile(path: string, secret: string): void {\n mkdirSync(join(path, \"..\"), { recursive: true, mode: 0o700 });\n const staging = `${path}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`;\n const fd = openSync(staging, \"wx\", 0o600);\n try {\n writeSync(fd, `${secret}\\n`);\n } finally {\n closeSync(fd);\n }\n try {\n chmodSync(staging, 0o600);\n renameSync(staging, path);\n chmodSync(path, 0o600);\n } finally {\n try { unlinkSync(staging); } catch {}\n }\n}\n\nfunction writeSecretIfAbsent(path: string, secret: string): string {\n mkdirSync(join(path, \"..\"), { recursive: true, mode: 0o700 });\n const staging = `${path}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`;\n const fd = openSync(staging, \"wx\", 0o600);\n try {\n writeSync(fd, `${secret}\\n`);\n } finally {\n closeSync(fd);\n }\n try {\n chmodSync(staging, 0o600);\n linkSync(staging, path);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"EEXIST\") throw err;\n } finally {\n try { unlinkSync(staging); } catch {}\n }\n const stored = readSecretFile(path);\n if (!stored) throw new Error(`Could not read stored API secret at ${path}.`);\n return stored;\n}\n\nexport function ensureServerSecret(env: NodeJS.ProcessEnv = process.env): {\n secret: string;\n source: \"env\" | \"file\" | \"generated\";\n} {\n const configured = explicitSecret(env);\n if (configured) return { secret: configured, source: \"env\" };\n\n const localPath = secretFilePath();\n const canonicalPath = canonicalSecretPath();\n const canonical = localPath === canonicalPath ? \"\" : readSecretFile(canonicalPath);\n if (canonical) {\n if (localPath !== canonicalPath && readSecretFile(localPath) !== canonical) {\n writeSecretFile(localPath, canonical);\n }\n return { secret: canonical, source: \"file\" };\n }\n\n const legacy = readSecretFile(localPath) || legacyDataDirSecretPaths(env).map(readSecretFile).find(Boolean) || \"\";\n const candidate = legacy || randomBytes(32).toString(\"hex\");\n const localSecret = writeSecretIfAbsent(localPath, candidate);\n const secret = localPath === canonicalPath\n ? localSecret\n : writeSecretIfAbsent(canonicalPath, localSecret);\n if (localPath !== canonicalPath && localSecret !== secret) writeSecretFile(localPath, secret);\n return { secret, source: legacy ? \"file\" : \"generated\" };\n}\n\nexport function bearerHeaders(\n baseUrl: string,\n env: NodeJS.ProcessEnv = process.env,\n): Record<string, string> {\n const secret = resolveClientSecret(baseUrl, env);\n return secret ? { Authorization: `Bearer ${secret}` } : {};\n}\n","import { isLoopbackUrl, resolveClientSecret } from \"../secret-store.js\";\n\nconst OPTION_PREFIX = \"CLAUDE_PLUGIN_OPTION_\";\nconst DEFAULT_URL = \"http://localhost:3111\";\n\nexport interface HookEndpoint {\n url: string;\n secret: string;\n blocked: boolean;\n}\n\nfunction filled(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n if (!trimmed || (trimmed.startsWith(\"${\") && trimmed.endsWith(\"}\"))) return undefined;\n return value;\n}\n\nexport function hookSetting(\n name: string,\n env: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n const optionKey = `${OPTION_PREFIX}${name}`;\n const option = filled(env[optionKey])?.trim();\n if (option) return option;\n if (name === \"AGENTMEMORY_SECRET\" && optionKey in env) return undefined;\n return filled(env[name]);\n}\n\nexport function resolveSettingSecret(url: string, env: NodeJS.ProcessEnv = process.env): string {\n const secretEnv: NodeJS.ProcessEnv = { ...env };\n const explicit = hookSetting(\"AGENTMEMORY_SECRET\", env);\n if (explicit === undefined) delete secretEnv[\"AGENTMEMORY_SECRET\"];\n else secretEnv[\"AGENTMEMORY_SECRET\"] = explicit;\n return resolveClientSecret(url, secretEnv);\n}\n\nexport function secretTransportAllowed(url: string): boolean {\n try {\n return new URL(url).protocol === \"https:\" || isLoopbackUrl(url);\n } catch {\n return false;\n }\n}\n\nexport function resolveHookEndpoint(env: NodeJS.ProcessEnv = process.env): HookEndpoint {\n const url = hookSetting(\"AGENTMEMORY_URL\", env) || DEFAULT_URL;\n const secret = resolveSettingSecret(url, env);\n const blocked = !secretTransportAllowed(url);\n return { url, secret: blocked ? \"\" : secret, blocked };\n}\n\nlet blockedNoticeShown = false;\n\nexport function reportBlockedTransport(endpoint: HookEndpoint): boolean {\n if (!endpoint.blocked) return false;\n if (!blockedNoticeShown) {\n blockedNoticeShown = true;\n process.stderr.write(\n \"[agentmemory] AGENTMEMORY_URL must use HTTPS unless it is a loopback address. Nothing was sent.\\n\",\n );\n }\n return true;\n}\n\nexport function hookAuthHeaders(endpoint: HookEndpoint): Record<string, string> {\n const h: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (endpoint.secret) h[\"Authorization\"] = `Bearer ${endpoint.secret}`;\n return h;\n}\n","#!/usr/bin/env node\nimport { hookAuthHeaders, reportBlockedTransport, resolveHookEndpoint } from \"./_endpoint.js\";\n\nfunction isSdkChildContext(payload: unknown): boolean {\n if (process.env[\"AGENTMEMORY_SDK_CHILD\"] === \"1\") return true;\n if (!payload || typeof payload !== \"object\") return false;\n return (payload as { entrypoint?: unknown }).entrypoint === \"sdk-ts\";\n}\n\nconst INJECT_CONTEXT = process.env[\"AGENTMEMORY_INJECT_CONTEXT\"] === \"true\";\n\nconst ENDPOINT = resolveHookEndpoint();\nconst REST_URL = ENDPOINT.url;\n\nfunction authHeaders(): Record<string, string> {\n return hookAuthHeaders(ENDPOINT);\n}\n\nasync function main() {\n // Default off: exit immediately so we don't even open stdin. This keeps\n // Claude Code's tool-call hot path as cheap as possible.\n if (!INJECT_CONTEXT) return;\n if (reportBlockedTransport(ENDPOINT)) return;\n\n let input = \"\";\n for await (const chunk of process.stdin) {\n input += chunk;\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(input);\n } catch {\n return;\n }\n\n if (!data || typeof data !== \"object\") return;\n if (isSdkChildContext(data)) return;\n\n const toolName =\n typeof data.tool_name === \"string\"\n ? data.tool_name\n : typeof data.toolName === \"string\"\n ? data.toolName\n : undefined;\n if (!toolName) return;\n\n const normalizedToolName = toolName.toLowerCase();\n const fileTools = [\"edit\", \"write\", \"create\", \"read\", \"view\", \"glob\", \"grep\"];\n if (!fileTools.includes(normalizedToolName)) return;\n\n const rawToolInput = data.tool_input ?? data.toolArgs;\n const toolInput =\n typeof rawToolInput === \"object\" &&\n rawToolInput !== null &&\n !Array.isArray(rawToolInput)\n ? (rawToolInput as Record<string, unknown>)\n : {};\n const files: string[] = [];\n const fileKeys =\n normalizedToolName === \"grep\"\n ? [\"path\", \"file\"]\n : [\"file_path\", \"path\", \"file\", \"pattern\"];\n for (const key of fileKeys) {\n const val = toolInput[key];\n if (typeof val === \"string\" && val.length > 0) files.push(val);\n }\n if (files.length === 0) return;\n\n const terms: string[] = [];\n if (normalizedToolName === \"grep\" || normalizedToolName === \"glob\") {\n const pattern = toolInput[\"pattern\"];\n if (typeof pattern === \"string\" && pattern.length > 0) {\n terms.push(pattern);\n }\n }\n\n const rawSessionId = data.session_id || data.sessionId || data.conversation_id;\n const sessionId =\n typeof rawSessionId === \"string\" && rawSessionId.length > 0\n ? rawSessionId\n : \"unknown\";\n const project =\n typeof data.project === \"string\" && data.project.trim().length > 0\n ? data.project.trim()\n : undefined;\n\n try {\n const res = await fetch(`${REST_URL}/agentmemory/enrich`, {\n method: \"POST\",\n headers: authHeaders(),\n body: JSON.stringify({\n sessionId,\n files,\n terms,\n toolName,\n ...(project !== undefined && { project }),\n }),\n signal: AbortSignal.timeout(2000),\n });\n\n if (res.ok) {\n const result = (await res.json()) as { context?: string };\n if (result.context) {\n process.stdout.write(result.context);\n }\n }\n } catch {\n // don't block tool execution\n }\n}\n\nmain().catch(() => process.exit(0));\n"],"mappings":"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"}
|
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
import { readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
4
|
import { basename, dirname, resolve } from "node:path";
|
|
5
|
-
import { REST_URL, authHeaders, captureObservation, isDrainChild, runDrainChild, withEventId } from "./_capture.mjs";
|
|
5
|
+
import { REST_URL, authHeaders, captureObservation, isDrainChild, runDrainChild, transportBlocked, withEventId } from "./_capture.mjs";
|
|
6
|
+
//#region src/hooks/_capture-filter.ts
|
|
7
|
+
function truncateText(value, max) {
|
|
8
|
+
if (value.length <= max) return value;
|
|
9
|
+
const end = /[\uD800-\uDBFF]/.test(value[max - 1] ?? "") ? max - 1 : max;
|
|
10
|
+
return value.slice(0, end);
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
6
13
|
//#region src/hooks/_project.ts
|
|
7
14
|
function resolveProject(cwd) {
|
|
8
15
|
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
|
|
@@ -85,7 +92,7 @@ function extractTranscriptPrompts(data) {
|
|
|
85
92
|
if (prompts.length >= 50) return prompts;
|
|
86
93
|
const match = msg.content.match(/<USER_REQUEST>\n?([\s\S]*?)\n?<\/USER_REQUEST>/);
|
|
87
94
|
const text = (match ? match[1] : msg.content).trim();
|
|
88
|
-
if (text) prompts.push({ prompt: text
|
|
95
|
+
if (text) prompts.push({ prompt: truncateText(text, 8e3) });
|
|
89
96
|
continue;
|
|
90
97
|
}
|
|
91
98
|
if (!isUserTurn(msg)) continue;
|
|
@@ -95,7 +102,7 @@ function extractTranscriptPrompts(data) {
|
|
|
95
102
|
for (const text of promptId ? [texts.join("\n\n")].filter(Boolean) : texts) {
|
|
96
103
|
if (prompts.length >= 50) return prompts;
|
|
97
104
|
prompts.push({
|
|
98
|
-
prompt: text
|
|
105
|
+
prompt: truncateText(text, 8e3),
|
|
99
106
|
promptId,
|
|
100
107
|
timestamp
|
|
101
108
|
});
|
|
@@ -138,6 +145,10 @@ async function main() {
|
|
|
138
145
|
prompt
|
|
139
146
|
}, { stable: true }), 3e3)));
|
|
140
147
|
}
|
|
148
|
+
if (transportBlocked()) {
|
|
149
|
+
setTimeout(() => process.exit(0), 1500).unref();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
141
152
|
fetch(`${REST_URL}/agentmemory/session/end`, {
|
|
142
153
|
method: "POST",
|
|
143
154
|
headers: authHeaders(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-end.mjs","names":[],"sources":["../../src/hooks/_project.ts","../../src/hooks/session-end.ts"],"sourcesContent":["import { execSync } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\n\n// Resolution order: AGENTMEMORY_PROJECT_NAME env → git repository basename → cwd basename.\nexport function resolveProject(cwd?: string): string {\n const explicit = process.env[\"AGENTMEMORY_PROJECT_NAME\"];\n if (explicit && explicit.trim()) return explicit.trim();\n const dir = cwd && cwd.trim() ? cwd : process.cwd();\n try {\n const [top, gitDir, commonDir] = execSync(\n \"git rev-parse --show-toplevel --git-dir --git-common-dir\",\n {\n cwd: dir,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n timeout: 500,\n },\n )\n .toString()\n .trim()\n .split(/\\r?\\n/);\n if (top) return basename(repositoryRoot(dir, top, gitDir, commonDir));\n } catch {}\n return basename(dir);\n}\n\nfunction repositoryRoot(dir: string, top: string, gitDir?: string, commonDir?: string): string {\n if (!gitDir || !commonDir) return top;\n const physical = realpathSync(dir);\n const common = resolve(physical, commonDir);\n if (resolve(physical, gitDir) === common || basename(common) !== \".git\") return top;\n return dirname(common);\n}\n\nexport function hookCwd(data: Record<string, unknown> | null | undefined): string | undefined {\n if (!data || typeof data !== \"object\") return undefined;\n if (typeof data.cwd === \"string\" && data.cwd.trim()) return data.cwd;\n const roots = data.workspace_roots;\n if (Array.isArray(roots)) {\n for (const root of roots) {\n if (typeof root === \"string\" && root.trim()) return root;\n }\n }\n const projectDir =\n process.env[\"DEVIN_PROJECT_DIR\"] || process.env[\"CLAUDE_PROJECT_DIR\"];\n if (projectDir && projectDir.trim()) return projectDir;\n return undefined;\n}\n","#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { resolveProject, hookCwd } from \"./_project.js\";\nimport { REST_URL, authHeaders, captureObservation, isDrainChild, runDrainChild, withEventId } from \"./_capture.js\";\n\nfunction isSdkChildContext(payload: unknown): boolean {\n if (process.env[\"AGENTMEMORY_SDK_CHILD\"] === \"1\") return true;\n if (!payload || typeof payload !== \"object\") return false;\n return (payload as { entrypoint?: unknown }).entrypoint === \"sdk-ts\";\n}\n\ntype TranscriptBlock = { type?: string; text?: string };\ntype TranscriptLine = {\n role?: string;\n type?: string;\n source?: string;\n content?: unknown;\n isSidechain?: boolean;\n isMeta?: boolean;\n isCompactSummary?: boolean;\n promptId?: string;\n timestamp?: string;\n message?: { role?: string; content?: string | TranscriptBlock[] };\n};\n\ntype TranscriptPrompt = { prompt: string; promptId?: string; timestamp?: string };\n\nconst HARNESS_TEXT =\n /^(?:<(?:command-name|command-message|command-args|local-command-stdout|local-command-stderr|local-command-caveat|bash-input|bash-stdout|bash-stderr|task-notification|system-reminder|ci-monitor-event|cross-session-message|scheduled-task)(?=[\\s>])|\\[Request interrupted)/;\n\nfunction isUserTurn(msg: TranscriptLine): boolean {\n if (msg.isSidechain || msg.isMeta || msg.isCompactSummary) return false;\n return (\n msg.role === \"user\" || msg.type === \"user\" || msg.message?.role === \"user\"\n );\n}\n\nfunction promptText(raw: string): string {\n const m = raw.match(/<user_query>\\n?([\\s\\S]*?)\\n?<\\/user_query>/);\n const text = (m ? m[1] : raw).trim();\n return HARNESS_TEXT.test(text) ? \"\" : text;\n}\n\nfunction turnTexts(content: string | TranscriptBlock[] | undefined): string[] {\n if (typeof content === \"string\") return [content];\n if (!Array.isArray(content)) return [];\n const texts: string[] = [];\n for (const block of content) {\n if (block?.type === \"text\" && typeof block.text === \"string\") {\n texts.push(block.text);\n }\n }\n return texts;\n}\n\nfunction extractTranscriptPrompts(data: Record<string, unknown>): TranscriptPrompt[] {\n const path = data.transcript_path;\n if (typeof path !== \"string\" || !path.endsWith(\".jsonl\")) return [];\n let raw: string;\n try {\n raw = readFileSync(path, \"utf-8\");\n } catch {\n return [];\n }\n const prompts: TranscriptPrompt[] = [];\n for (const line of raw.split(\"\\n\")) {\n if (!line.trim()) continue;\n let msg: TranscriptLine;\n try {\n msg = JSON.parse(line);\n } catch {\n continue;\n }\n if (msg.type === \"USER_INPUT\" && msg.source === \"USER_EXPLICIT\" && typeof msg.content === \"string\") {\n if (prompts.length >= 50) return prompts;\n const match = msg.content.match(/<USER_REQUEST>\\n?([\\s\\S]*?)\\n?<\\/USER_REQUEST>/);\n const text = (match ? match[1] : msg.content).trim();\n if (text) prompts.push({ prompt: text.slice(0, 8000) });\n continue;\n }\n if (!isUserTurn(msg)) continue;\n const texts = turnTexts(msg.message?.content).map(promptText).filter(Boolean);\n const promptId = typeof msg.promptId === \"string\" ? msg.promptId : undefined;\n const timestamp = typeof msg.timestamp === \"string\" ? msg.timestamp : undefined;\n for (const text of promptId ? [texts.join(\"\\n\\n\")].filter(Boolean) : texts) {\n if (prompts.length >= 50) return prompts;\n prompts.push({ prompt: text.slice(0, 8000), promptId, timestamp });\n }\n }\n return prompts;\n}\n\nasync function main() {\n if (isDrainChild()) return runDrainChild();\n let input = \"\";\n for await (const chunk of process.stdin) {\n input += chunk;\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(input);\n } catch {\n return;\n }\n\n if (!data || typeof data !== \"object\") return;\n if (isSdkChildContext(data)) return;\n\n const sessionId = ((data.session_id || data.sessionId || data.conversation_id) as string) || \"unknown\";\n\n const transcriptPrompts = extractTranscriptPrompts(data);\n if (transcriptPrompts.length > 0) {\n const cwd = hookCwd(data) || process.cwd();\n const project = resolveProject(cwd);\n const timestamp = new Date().toISOString();\n await Promise.allSettled(\n transcriptPrompts.map(({ prompt, promptId, timestamp: at }, index) =>\n captureObservation(\n withEventId(\n { hookType: \"prompt_submit\", sessionId, project, cwd, timestamp: at ?? timestamp, data: { prompt, backfill: true } },\n promptId ? { prompt_id: promptId } : {},\n { source: \"transcript\", transcript: data.transcript_path, index, prompt },\n { stable: true },\n ),\n 3000,\n ),\n ),\n );\n }\n\n fetch(`${REST_URL}/agentmemory/session/end`, {\n method: \"POST\",\n headers: authHeaders(),\n body: JSON.stringify({ sessionId, final: true }),\n signal: AbortSignal.timeout(30000),\n }).catch(() => {});\n\n if (process.env[\"CLAUDE_MEMORY_BRIDGE\"] === \"true\") {\n fetch(`${REST_URL}/agentmemory/claude-bridge/sync`, {\n method: \"POST\",\n headers: authHeaders(),\n signal: AbortSignal.timeout(30000),\n }).catch(() => {});\n }\n\n setTimeout(() => process.exit(0), 1500).unref();\n}\n\nmain().catch(() => process.exit(0));\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session-end.mjs","names":[],"sources":["../../src/hooks/_capture-filter.ts","../../src/hooks/_project.ts","../../src/hooks/session-end.ts"],"sourcesContent":["const DEFAULT_DENY_PATTERNS = [\n \"memory_*\",\n \"toolsearch\",\n \"listmcpresources\",\n \"fetchmcpresource\",\n];\n\nfunction parseEnvList(raw: string | undefined): string[] | undefined {\n if (!raw?.trim()) return undefined;\n return raw\n .split(/[,\\s]+/)\n .map((part) => part.trim())\n .filter(Boolean);\n}\n\nexport function bareToolName(toolName: string): string {\n const trimmed = toolName.trim();\n if (/^mcp__/i.test(trimmed)) {\n const parts = trimmed.split(\"__\");\n if (parts.length >= 3) return parts[parts.length - 1]!;\n }\n return trimmed;\n}\n\nfunction normalizePattern(pattern: string): string {\n return pattern.trim().toLowerCase();\n}\n\nfunction matchesPattern(toolName: string, pattern: string): boolean {\n const bare = bareToolName(toolName).toLowerCase();\n const full = toolName.trim().toLowerCase();\n const pat = normalizePattern(pattern);\n if (!pat.includes(\"*\")) return bare === pat || full === pat;\n const escaped = pat.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const re = new RegExp(`^${escaped.replace(/\\*/g, \".*\")}$`);\n return re.test(bare) || re.test(full);\n}\n\nfunction matchesAny(toolName: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchesPattern(toolName, pattern));\n}\n\nexport function shouldCaptureTool(toolName: unknown): boolean {\n if (typeof toolName !== \"string\" || !toolName.trim()) return true;\n\n const allow = parseEnvList(process.env[\"AGENTMEMORY_CAPTURE_ALLOW\"]);\n if (allow) return matchesAny(toolName, allow);\n\n const deny = [\n ...DEFAULT_DENY_PATTERNS,\n ...(parseEnvList(process.env[\"AGENTMEMORY_CAPTURE_DENY\"]) ?? []),\n ];\n return !matchesAny(toolName, deny);\n}\n\nexport function captureOutputMax(): number {\n const raw = process.env[\"AGENTMEMORY_CAPTURE_OUTPUT_MAX\"];\n if (!raw?.trim()) return 8000;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 8000;\n}\n\nexport function truncateText(value: string, max: number): string {\n if (value.length <= max) return value;\n const end = /[\\uD800-\\uDBFF]/.test(value[max - 1] ?? \"\") ? max - 1 : max;\n return value.slice(0, end);\n}\n\nexport function truncateCaptureOutput(value: unknown, max: number): unknown {\n if (typeof value === \"string\" && value.length > max) {\n const suffix = \"\\n[...truncated]\";\n return max <= suffix.length\n ? suffix.slice(0, max)\n : truncateText(value, max - suffix.length) + suffix;\n }\n if (typeof value === \"object\" && value !== null) {\n const str = JSON.stringify(value);\n if (str.length > max) {\n const suffix = \"...[truncated]\";\n return max <= suffix.length\n ? suffix.slice(0, max)\n : truncateText(str, max - suffix.length) + suffix;\n }\n return value;\n }\n return value;\n}\n\nexport function preCompactBudget(): number {\n const raw = process.env[\"AGENTMEMORY_PRE_COMPACT_BUDGET\"];\n if (raw?.trim() === \"0\") return 0;\n if (!raw?.trim()) return 1500;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : 1500;\n}\n","import { execSync } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\n\n// Resolution order: AGENTMEMORY_PROJECT_NAME env → git repository basename → cwd basename.\nexport function resolveProject(cwd?: string): string {\n const explicit = process.env[\"AGENTMEMORY_PROJECT_NAME\"];\n if (explicit && explicit.trim()) return explicit.trim();\n const dir = cwd && cwd.trim() ? cwd : process.cwd();\n try {\n const [top, gitDir, commonDir] = execSync(\n \"git rev-parse --show-toplevel --git-dir --git-common-dir\",\n {\n cwd: dir,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n timeout: 500,\n },\n )\n .toString()\n .trim()\n .split(/\\r?\\n/);\n if (top) return basename(repositoryRoot(dir, top, gitDir, commonDir));\n } catch {}\n return basename(dir);\n}\n\nfunction repositoryRoot(dir: string, top: string, gitDir?: string, commonDir?: string): string {\n if (!gitDir || !commonDir) return top;\n const physical = realpathSync(dir);\n const common = resolve(physical, commonDir);\n if (resolve(physical, gitDir) === common || basename(common) !== \".git\") return top;\n return dirname(common);\n}\n\nexport function hookCwd(data: Record<string, unknown> | null | undefined): string | undefined {\n if (!data || typeof data !== \"object\") return undefined;\n if (typeof data.cwd === \"string\" && data.cwd.trim()) return data.cwd;\n const roots = data.workspace_roots;\n if (Array.isArray(roots)) {\n for (const root of roots) {\n if (typeof root === \"string\" && root.trim()) return root;\n }\n }\n const projectDir =\n process.env[\"DEVIN_PROJECT_DIR\"] || process.env[\"CLAUDE_PROJECT_DIR\"];\n if (projectDir && projectDir.trim()) return projectDir;\n return undefined;\n}\n","#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { truncateText } from \"./_capture-filter.js\";\nimport { resolveProject, hookCwd } from \"./_project.js\";\nimport { REST_URL, authHeaders, captureObservation, isDrainChild, runDrainChild, transportBlocked, withEventId } from \"./_capture.js\";\n\nfunction isSdkChildContext(payload: unknown): boolean {\n if (process.env[\"AGENTMEMORY_SDK_CHILD\"] === \"1\") return true;\n if (!payload || typeof payload !== \"object\") return false;\n return (payload as { entrypoint?: unknown }).entrypoint === \"sdk-ts\";\n}\n\ntype TranscriptBlock = { type?: string; text?: string };\ntype TranscriptLine = {\n role?: string;\n type?: string;\n source?: string;\n content?: unknown;\n isSidechain?: boolean;\n isMeta?: boolean;\n isCompactSummary?: boolean;\n promptId?: string;\n timestamp?: string;\n message?: { role?: string; content?: string | TranscriptBlock[] };\n};\n\ntype TranscriptPrompt = { prompt: string; promptId?: string; timestamp?: string };\n\nconst HARNESS_TEXT =\n /^(?:<(?:command-name|command-message|command-args|local-command-stdout|local-command-stderr|local-command-caveat|bash-input|bash-stdout|bash-stderr|task-notification|system-reminder|ci-monitor-event|cross-session-message|scheduled-task)(?=[\\s>])|\\[Request interrupted)/;\n\nfunction isUserTurn(msg: TranscriptLine): boolean {\n if (msg.isSidechain || msg.isMeta || msg.isCompactSummary) return false;\n return (\n msg.role === \"user\" || msg.type === \"user\" || msg.message?.role === \"user\"\n );\n}\n\nfunction promptText(raw: string): string {\n const m = raw.match(/<user_query>\\n?([\\s\\S]*?)\\n?<\\/user_query>/);\n const text = (m ? m[1] : raw).trim();\n return HARNESS_TEXT.test(text) ? \"\" : text;\n}\n\nfunction turnTexts(content: string | TranscriptBlock[] | undefined): string[] {\n if (typeof content === \"string\") return [content];\n if (!Array.isArray(content)) return [];\n const texts: string[] = [];\n for (const block of content) {\n if (block?.type === \"text\" && typeof block.text === \"string\") {\n texts.push(block.text);\n }\n }\n return texts;\n}\n\nfunction extractTranscriptPrompts(data: Record<string, unknown>): TranscriptPrompt[] {\n const path = data.transcript_path;\n if (typeof path !== \"string\" || !path.endsWith(\".jsonl\")) return [];\n let raw: string;\n try {\n raw = readFileSync(path, \"utf-8\");\n } catch {\n return [];\n }\n const prompts: TranscriptPrompt[] = [];\n for (const line of raw.split(\"\\n\")) {\n if (!line.trim()) continue;\n let msg: TranscriptLine;\n try {\n msg = JSON.parse(line);\n } catch {\n continue;\n }\n if (msg.type === \"USER_INPUT\" && msg.source === \"USER_EXPLICIT\" && typeof msg.content === \"string\") {\n if (prompts.length >= 50) return prompts;\n const match = msg.content.match(/<USER_REQUEST>\\n?([\\s\\S]*?)\\n?<\\/USER_REQUEST>/);\n const text = (match ? match[1] : msg.content).trim();\n if (text) prompts.push({ prompt: truncateText(text, 8000) });\n continue;\n }\n if (!isUserTurn(msg)) continue;\n const texts = turnTexts(msg.message?.content).map(promptText).filter(Boolean);\n const promptId = typeof msg.promptId === \"string\" ? msg.promptId : undefined;\n const timestamp = typeof msg.timestamp === \"string\" ? msg.timestamp : undefined;\n for (const text of promptId ? [texts.join(\"\\n\\n\")].filter(Boolean) : texts) {\n if (prompts.length >= 50) return prompts;\n prompts.push({ prompt: truncateText(text, 8000), promptId, timestamp });\n }\n }\n return prompts;\n}\n\nasync function main() {\n if (isDrainChild()) return runDrainChild();\n let input = \"\";\n for await (const chunk of process.stdin) {\n input += chunk;\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(input);\n } catch {\n return;\n }\n\n if (!data || typeof data !== \"object\") return;\n if (isSdkChildContext(data)) return;\n\n const sessionId = ((data.session_id || data.sessionId || data.conversation_id) as string) || \"unknown\";\n\n const transcriptPrompts = extractTranscriptPrompts(data);\n if (transcriptPrompts.length > 0) {\n const cwd = hookCwd(data) || process.cwd();\n const project = resolveProject(cwd);\n const timestamp = new Date().toISOString();\n await Promise.allSettled(\n transcriptPrompts.map(({ prompt, promptId, timestamp: at }, index) =>\n captureObservation(\n withEventId(\n { hookType: \"prompt_submit\", sessionId, project, cwd, timestamp: at ?? timestamp, data: { prompt, backfill: true } },\n promptId ? { prompt_id: promptId } : {},\n { source: \"transcript\", transcript: data.transcript_path, index, prompt },\n { stable: true },\n ),\n 3000,\n ),\n ),\n );\n }\n\n if (transportBlocked()) {\n setTimeout(() => process.exit(0), 1500).unref();\n return;\n }\n\n fetch(`${REST_URL}/agentmemory/session/end`, {\n method: \"POST\",\n headers: authHeaders(),\n body: JSON.stringify({ sessionId, final: true }),\n signal: AbortSignal.timeout(30000),\n }).catch(() => {});\n\n if (process.env[\"CLAUDE_MEMORY_BRIDGE\"] === \"true\") {\n fetch(`${REST_URL}/agentmemory/claude-bridge/sync`, {\n method: \"POST\",\n headers: authHeaders(),\n signal: AbortSignal.timeout(30000),\n }).catch(() => {});\n }\n\n setTimeout(() => process.exit(0), 1500).unref();\n}\n\nmain().catch(() => process.exit(0));\n"],"mappings":"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"}
|
|
@@ -90,6 +90,60 @@ function canonicalSecretPath() {
|
|
|
90
90
|
return join(homedir(), ".agentmemory", "secret");
|
|
91
91
|
}
|
|
92
92
|
//#endregion
|
|
93
|
+
//#region src/hooks/_endpoint.ts
|
|
94
|
+
const OPTION_PREFIX = "CLAUDE_PLUGIN_OPTION_";
|
|
95
|
+
const DEFAULT_URL = "http://localhost:3111";
|
|
96
|
+
function filled(value) {
|
|
97
|
+
const trimmed = value?.trim();
|
|
98
|
+
if (!trimmed || trimmed.startsWith("${") && trimmed.endsWith("}")) return void 0;
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
function hookSetting(name, env = process.env) {
|
|
102
|
+
const optionKey = `${OPTION_PREFIX}${name}`;
|
|
103
|
+
const option = filled(env[optionKey])?.trim();
|
|
104
|
+
if (option) return option;
|
|
105
|
+
if (name === "AGENTMEMORY_SECRET" && optionKey in env) return void 0;
|
|
106
|
+
return filled(env[name]);
|
|
107
|
+
}
|
|
108
|
+
function resolveSettingSecret(url, env = process.env) {
|
|
109
|
+
const secretEnv = { ...env };
|
|
110
|
+
const explicit = hookSetting("AGENTMEMORY_SECRET", env);
|
|
111
|
+
if (explicit === void 0) delete secretEnv["AGENTMEMORY_SECRET"];
|
|
112
|
+
else secretEnv["AGENTMEMORY_SECRET"] = explicit;
|
|
113
|
+
return resolveClientSecret(url, secretEnv);
|
|
114
|
+
}
|
|
115
|
+
function secretTransportAllowed(url) {
|
|
116
|
+
try {
|
|
117
|
+
return new URL(url).protocol === "https:" || isLoopbackUrl(url);
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function resolveHookEndpoint(env = process.env) {
|
|
123
|
+
const url = hookSetting("AGENTMEMORY_URL", env) || DEFAULT_URL;
|
|
124
|
+
const secret = resolveSettingSecret(url, env);
|
|
125
|
+
const blocked = !secretTransportAllowed(url);
|
|
126
|
+
return {
|
|
127
|
+
url,
|
|
128
|
+
secret: blocked ? "" : secret,
|
|
129
|
+
blocked
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
let blockedNoticeShown = false;
|
|
133
|
+
function reportBlockedTransport(endpoint) {
|
|
134
|
+
if (!endpoint.blocked) return false;
|
|
135
|
+
if (!blockedNoticeShown) {
|
|
136
|
+
blockedNoticeShown = true;
|
|
137
|
+
process.stderr.write("[agentmemory] AGENTMEMORY_URL must use HTTPS unless it is a loopback address. Nothing was sent.\n");
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
function hookAuthHeaders(endpoint) {
|
|
142
|
+
const h = { "Content-Type": "application/json" };
|
|
143
|
+
if (endpoint.secret) h["Authorization"] = `Bearer ${endpoint.secret}`;
|
|
144
|
+
return h;
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
93
147
|
//#region src/hooks/_project.ts
|
|
94
148
|
function resolveProject(cwd) {
|
|
95
149
|
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
|
|
@@ -134,14 +188,12 @@ function isSdkChildContext(payload) {
|
|
|
134
188
|
return payload.entrypoint === "sdk-ts";
|
|
135
189
|
}
|
|
136
190
|
const INJECT_CONTEXT = process.env["AGENTMEMORY_INJECT_CONTEXT"] === "true";
|
|
137
|
-
const
|
|
138
|
-
const
|
|
191
|
+
const ENDPOINT = resolveHookEndpoint();
|
|
192
|
+
const REST_URL = ENDPOINT.url;
|
|
139
193
|
const INJECT_TIMEOUT_MS = 1500;
|
|
140
194
|
const REGISTER_TIMEOUT_MS = 800;
|
|
141
195
|
function authHeaders() {
|
|
142
|
-
|
|
143
|
-
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
|
144
|
-
return h;
|
|
196
|
+
return hookAuthHeaders(ENDPOINT);
|
|
145
197
|
}
|
|
146
198
|
function isPlainTextHost() {
|
|
147
199
|
return Boolean(process.env["FACTORY_PROJECT_DIR"] || process.env["DROID_PLUGIN_ROOT"]);
|
|
@@ -170,6 +222,7 @@ async function main() {
|
|
|
170
222
|
}
|
|
171
223
|
if (!data || typeof data !== "object") return;
|
|
172
224
|
if (isSdkChildContext(data)) return;
|
|
225
|
+
if (reportBlockedTransport(ENDPOINT)) return;
|
|
173
226
|
const sessionId = data.session_id || data.sessionId || data.conversation_id || `ses_${Date.now().toString(36)}`;
|
|
174
227
|
const cwd = hookCwd(data) || process.cwd();
|
|
175
228
|
const project = resolveProject(cwd);
|