agent-working-memory 0.14.5 → 0.15.4

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.
Files changed (54) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/README.md +131 -122
  3. package/dist/adapters/claude-code.d.ts +9 -0
  4. package/dist/adapters/claude-code.d.ts.map +1 -1
  5. package/dist/adapters/claude-code.js +235 -132
  6. package/dist/adapters/claude-code.js.map +1 -1
  7. package/dist/adapters/common.d.ts +47 -5
  8. package/dist/adapters/common.d.ts.map +1 -1
  9. package/dist/adapters/common.js +111 -41
  10. package/dist/adapters/common.js.map +1 -1
  11. package/dist/adapters/hook-scripts.d.ts +54 -0
  12. package/dist/adapters/hook-scripts.d.ts.map +1 -0
  13. package/dist/adapters/hook-scripts.js +340 -0
  14. package/dist/adapters/hook-scripts.js.map +1 -0
  15. package/dist/adapters/types.d.ts +13 -3
  16. package/dist/adapters/types.d.ts.map +1 -1
  17. package/dist/cli.js +74 -6
  18. package/dist/cli.js.map +1 -1
  19. package/dist/core/agent-id.d.ts +19 -0
  20. package/dist/core/agent-id.d.ts.map +1 -0
  21. package/dist/core/agent-id.js +28 -0
  22. package/dist/core/agent-id.js.map +1 -0
  23. package/dist/core/write-pipeline.js +17 -0
  24. package/dist/core/write-pipeline.js.map +1 -1
  25. package/dist/hooks/prime.d.ts +10 -4
  26. package/dist/hooks/prime.d.ts.map +1 -1
  27. package/dist/hooks/prime.js +16 -6
  28. package/dist/hooks/prime.js.map +1 -1
  29. package/dist/mcp.js +5 -12
  30. package/dist/mcp.js.map +1 -1
  31. package/dist/storage/factory.d.ts.map +1 -1
  32. package/dist/storage/factory.js +20 -8
  33. package/dist/storage/factory.js.map +1 -1
  34. package/package.json +100 -90
  35. package/plugin/.claude-plugin/plugin.json +34 -0
  36. package/plugin/README.md +36 -0
  37. package/plugin/bin/awm-mcp-launcher.cjs +109 -0
  38. package/plugin/hooks/awm-checkpoint.cjs +55 -0
  39. package/plugin/hooks/awm-db-mutation-reminder.cjs +25 -0
  40. package/plugin/hooks/awm-find-sidecar.cjs +145 -0
  41. package/plugin/hooks/awm-prime.cjs +97 -0
  42. package/plugin/hooks/hooks.json +66 -0
  43. package/plugin/skills/awm-memory/SKILL.md +445 -0
  44. package/src/adapters/claude-code.ts +224 -126
  45. package/src/adapters/common.ts +119 -42
  46. package/src/adapters/hook-scripts.ts +370 -0
  47. package/src/adapters/types.ts +14 -3
  48. package/src/cli.ts +76 -5
  49. package/src/core/agent-id.ts +28 -0
  50. package/src/core/write-pipeline.ts +18 -0
  51. package/src/hooks/prime.ts +22 -7
  52. package/src/mcp.ts +5 -11
  53. package/src/plugin/awm-mcp-launcher.cjs +109 -0
  54. package/src/storage/factory.ts +19 -7
@@ -0,0 +1,340 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Claude Code hook scripts shipped by `awm setup` (0.14.6).
5
+ *
6
+ * WHY SCRIPTS AND NOT INLINE CURL
7
+ * -------------------------------
8
+ * Until 0.14.5 `awm setup` wrote PreCompact/SessionEnd hooks as one-line curl
9
+ * commands with the sidecar port and the bearer secret baked into
10
+ * settings.json. Three things broke, each silently:
11
+ *
12
+ * 1. The port was fixed (8401, falling back to 8402). Since 0.14.2 every MCP
13
+ * process binds the first free port in 8401..8410, so a session that lands
14
+ * on 8403 had no hooks at all — and with two pools open, the fallback
15
+ * posted a work session's checkpoint into the personal pool's process.
16
+ * 2. The secret was a literal in settings.json. When the secret file was
17
+ * regenerated the hooks got 401s, and `curl -sf` hid them. Observed twice.
18
+ * 3. There was no prime hook. The only mechanism that injects memory without
19
+ * the model choosing to call recall was a documentation page.
20
+ *
21
+ * These scripts resolve everything at run time: which agent this session is
22
+ * (from the MCP config that governs its cwd), which sidecar belongs to that
23
+ * agent (by probing the port range and reading /health), and the secret (from
24
+ * the same config, or the secret file next to the database). Nothing here is
25
+ * baked in at setup time except a fallback record in ~/.claude/hooks/awm-hooks.json.
26
+ *
27
+ * Every script fails OPEN: any problem prints nothing and exits 0. A hook that
28
+ * blocks a prompt or a compaction is worse than no hook.
29
+ *
30
+ * The JavaScript bodies deliberately avoid template literals so they can live
31
+ * inside these TypeScript template strings without escaping.
32
+ */
33
+ /** Bumped when any shipped script changes; written into awm-hooks.json and the
34
+ * script headers so `awm doctor` can tell a stale install from a current one. */
35
+ export const AWM_HOOKS_VERSION = '0.14.6';
36
+ /** File whose presence turns the prime hook off without uninstalling it. */
37
+ export const PRIME_DISABLE_FILE = 'awm-prime.disabled';
38
+ const HEADER = (name, purpose) => `#!/usr/bin/env node
39
+ // ${name} — installed by \`awm setup\` (hooks v${AWM_HOOKS_VERSION}). ${purpose}
40
+ // Re-running \`awm setup\` rewrites this file; put local changes elsewhere.
41
+ // Fails open: on any problem it prints nothing and exits 0.
42
+ `;
43
+ // ─── awm-find-sidecar.cjs ────────────────────────────────────────────────────
44
+ export const FIND_SIDECAR_SCRIPT = HEADER('awm-find-sidecar.cjs', 'Shared resolver: which AWM sidecar belongs to THIS session.') + `
45
+ // Since AWM 0.14.2 each MCP process binds the first free port in
46
+ // [AWM_HOOK_PORT, +AWM_HOOK_PORT_RANGE), so "the sidecar is on 8401" is not a
47
+ // safe assumption: with N sessions open there are N sidecars and the one on
48
+ // 8401 may serve a different agent pool. This module (1) works out which agent
49
+ // a session started in \`cwd\` belongs to, from the MCP config that governs that
50
+ // directory, (2) probes the whole range in parallel (local, ~10 ms), and
51
+ // (3) returns the responder whose agentId matches — newest version first, then
52
+ // lowest port. No match → null, and callers fail open.
53
+ 'use strict';
54
+ const fs = require('fs');
55
+ const os = require('os');
56
+ const path = require('path');
57
+ const http = require('http');
58
+
59
+ const SERVER_NAME = 'agent-working-memory';
60
+ const PROBE_TIMEOUT_MS = 400;
61
+
62
+ function norm(p) { return String(p || '').replace(/\\\\/g, '/').replace(/\\/+$/, '').toLowerCase(); }
63
+ function readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) { return null; } }
64
+ function readText(p) { try { return fs.readFileSync(p, 'utf8').trim(); } catch (e) { return ''; } }
65
+
66
+ // MIRROR of deriveAgentFromDir() in src/core/agent-id.ts — the server's own
67
+ // fallback when AWM_AGENT_ID is unset. Kept in sync by tests/adapters/hook-scripts.test.ts.
68
+ function deriveAgentFromDir(dir) {
69
+ const d = String(dir || '').replace(/\\\\/g, '/');
70
+ if (/\\/AgentSynapse\\//i.test(d)) return 'work';
71
+ return /\\/Personal-Projects(\\/|$)/i.test(d) ? 'personal' : 'work';
72
+ }
73
+
74
+ // The AWM entry inside one MCP config file, if any. ~/.claude.json keeps
75
+ // project-scoped servers under projects[<path>].mcpServers.
76
+ function awmEntry(json, cwd) {
77
+ if (!json) return null;
78
+ if (json.mcpServers && json.mcpServers[SERVER_NAME]) return json.mcpServers[SERVER_NAME];
79
+ if (json.projects && cwd) {
80
+ for (const k of Object.keys(json.projects)) {
81
+ if (norm(k) !== norm(cwd)) continue;
82
+ const s = json.projects[k] && json.projects[k].mcpServers;
83
+ if (s && s[SERVER_NAME]) return s[SERVER_NAME];
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ /**
90
+ * Resolve the configuration that governs a session started in \`cwd\`:
91
+ * { agentId, secret, port, range, dbPath, source }.
92
+ *
93
+ * Order: the nearest .mcp.json / .claude/mcp.json walking up from cwd, then
94
+ * ~/.mcp.json, then ~/.claude.json, then the record \`awm setup\` left in
95
+ * ~/.claude/hooks/awm-hooks.json. Process env always wins for a given key.
96
+ */
97
+ function resolveConfig(cwd) {
98
+ cwd = cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
99
+ const home = os.homedir();
100
+ const candidates = [];
101
+ let dir = path.resolve(cwd);
102
+ for (let i = 0; i < 16; i++) {
103
+ candidates.push(path.join(dir, '.mcp.json'), path.join(dir, '.claude', 'mcp.json'));
104
+ const parent = path.dirname(dir);
105
+ if (parent === dir) break;
106
+ dir = parent;
107
+ }
108
+ candidates.push(path.join(home, '.mcp.json'), path.join(home, '.claude.json'));
109
+
110
+ let entry = null, source = null;
111
+ for (const c of candidates) {
112
+ const e = awmEntry(readJson(c), cwd);
113
+ if (e) { entry = e; source = c; break; }
114
+ }
115
+ const env = (entry && entry.env) || {};
116
+ const fallback = readJson(path.join(home, '.claude', 'hooks', 'awm-hooks.json')) || {};
117
+
118
+ const agentId = process.env.AWM_AGENT_ID || env.AWM_AGENT_ID
119
+ || (entry ? deriveAgentFromDir(cwd) : (fallback.agentId || deriveAgentFromDir(cwd)));
120
+ const dbPath = env.AWM_DB_PATH || fallback.dbPath || null;
121
+ let secret = process.env.AWM_HOOK_SECRET || env.AWM_HOOK_SECRET || '';
122
+ if (!secret && dbPath) secret = readText(path.join(path.dirname(dbPath), '.awm-hook-secret'));
123
+ if (!secret && fallback.secretPath) secret = readText(fallback.secretPath);
124
+ const port = parseInt(process.env.AWM_HOOK_PORT || env.AWM_HOOK_PORT || fallback.hookPort || '8401', 10) || 8401;
125
+ const range = Math.max(1, parseInt(process.env.AWM_HOOK_PORT_RANGE || env.AWM_HOOK_PORT_RANGE || fallback.hookPortRange || '10', 10) || 10);
126
+ return { agentId: agentId, secret: secret, port: port, range: range, dbPath: dbPath, source: source };
127
+ }
128
+
129
+ function probe(port) {
130
+ return new Promise(function (resolve) {
131
+ const req = http.get({ host: '127.0.0.1', port: port, path: '/health', timeout: PROBE_TIMEOUT_MS }, function (res) {
132
+ let data = '';
133
+ res.on('data', function (c) { data += c; });
134
+ res.on('end', function () {
135
+ try {
136
+ const j = JSON.parse(data);
137
+ resolve(j && j.sidecar ? { port: port, agentId: j.agentId || null, version: j.version || '0.0.0', pid: j.pid || null } : null);
138
+ } catch (e) { resolve(null); }
139
+ });
140
+ });
141
+ req.on('error', function () { resolve(null); });
142
+ req.on('timeout', function () { req.destroy(); resolve(null); });
143
+ });
144
+ }
145
+
146
+ function versionKey(v) { return String(v || '0').split('.').map(function (n) { return parseInt(n, 10) || 0; }); }
147
+ function newer(a, b) {
148
+ const ka = versionKey(a), kb = versionKey(b);
149
+ for (let i = 0; i < Math.max(ka.length, kb.length); i++) {
150
+ const d = (ka[i] || 0) - (kb[i] || 0);
151
+ if (d) return d > 0;
152
+ }
153
+ return false;
154
+ }
155
+
156
+ /** Every live sidecar in the configured range, regardless of agent. */
157
+ async function listSidecars(cfg) {
158
+ const ports = [];
159
+ for (let i = 0; i < cfg.range; i++) ports.push(cfg.port + i);
160
+ return (await Promise.all(ports.map(probe))).filter(Boolean);
161
+ }
162
+
163
+ /**
164
+ * The sidecar for this session, or null. Resolves to
165
+ * { sidecar: {port, agentId, version, pid} | null, config, all: [...] }.
166
+ */
167
+ async function findSidecar(cwd) {
168
+ const cfg = resolveConfig(cwd);
169
+ const all = await listSidecars(cfg);
170
+ const mine = all.filter(function (s) { return s.agentId === cfg.agentId; });
171
+ mine.sort(function (a, b) { return newer(a.version, b.version) ? -1 : newer(b.version, a.version) ? 1 : a.port - b.port; });
172
+ return { sidecar: mine[0] || null, config: cfg, all: all };
173
+ }
174
+
175
+ /** Optional trace for troubleshooting: AWM_HOOK_DEBUG=1 appends to ~/.claude/hooks/awm-hooks.log. */
176
+ function debugLog(hook, fields) {
177
+ if (!process.env.AWM_HOOK_DEBUG) return;
178
+ try {
179
+ const line = Object.assign({ ts: new Date().toISOString(), hook: hook }, fields);
180
+ fs.appendFileSync(path.join(os.homedir(), '.claude', 'hooks', 'awm-hooks.log'), JSON.stringify(line) + '\\n');
181
+ } catch (e) { /* never matters */ }
182
+ }
183
+
184
+ module.exports = { resolveConfig, findSidecar, listSidecars, deriveAgentFromDir, debugLog, PROBE_TIMEOUT_MS };
185
+ `;
186
+ // ─── awm-checkpoint.cjs ──────────────────────────────────────────────────────
187
+ export const CHECKPOINT_HOOK_SCRIPT = HEADER('awm-checkpoint.cjs', 'PreCompact / SessionEnd: checkpoint this session into ITS agent\'s AWM.') + `
188
+ // Forwards the whole hook payload (hook_event_name, transcript_path, cwd,
189
+ // session_id) to POST /hooks/checkpoint on the sidecar that belongs to this
190
+ // session's agent. transcript_path matters: the sidecar parses it for the
191
+ // current task and active files, so the checkpoint is a real one, not a stub.
192
+ 'use strict';
193
+ const http = require('http');
194
+
195
+ let finder = null;
196
+ try { finder = require('./awm-find-sidecar.cjs'); } catch (e) { process.exit(0); }
197
+
198
+ const TIMEOUT_MS = 5000;
199
+
200
+ let raw = '';
201
+ process.stdin.on('data', function (d) { raw += d; });
202
+ process.stdin.on('end', function () {
203
+ let payload = {};
204
+ try { payload = JSON.parse(raw || '{}'); } catch (e) { payload = {}; }
205
+ if (!payload.hook_event_name) payload.hook_event_name = 'PreCompact';
206
+ main(payload).catch(function () { process.exit(0); });
207
+ });
208
+ // A hook with nothing on stdin must still exit.
209
+ process.stdin.on('error', function () { process.exit(0); });
210
+
211
+ async function main(payload) {
212
+ const found = await finder.findSidecar(payload.cwd);
213
+ const sc = found.sidecar;
214
+ if (!sc) {
215
+ finder.debugLog('checkpoint', { event: payload.hook_event_name, agent: found.config.agentId, sidecar: null, live: found.all.length });
216
+ process.exit(0);
217
+ }
218
+ const body = JSON.stringify(payload);
219
+ const req = http.request({
220
+ host: '127.0.0.1', port: sc.port, path: '/hooks/checkpoint', method: 'POST',
221
+ headers: {
222
+ 'Content-Type': 'application/json',
223
+ 'Content-Length': Buffer.byteLength(body),
224
+ 'Authorization': 'Bearer ' + found.config.secret,
225
+ },
226
+ timeout: TIMEOUT_MS,
227
+ }, function (res) {
228
+ res.resume();
229
+ res.on('end', function () {
230
+ finder.debugLog('checkpoint', { event: payload.hook_event_name, agent: found.config.agentId, port: sc.port, status: res.statusCode });
231
+ process.exit(0);
232
+ });
233
+ });
234
+ req.on('error', function () { process.exit(0); });
235
+ req.on('timeout', function () { req.destroy(); process.exit(0); });
236
+ req.end(body);
237
+ }
238
+ `;
239
+ // ─── awm-prime.cjs ───────────────────────────────────────────────────────────
240
+ export const PRIME_HOOK_SCRIPT = HEADER('awm-prime.cjs', 'UserPromptSubmit: prime the turn with relevant memories, or stay silent.') + `
241
+ // The PRIME phase of the AWM-native harness (docs/patterns/awm-native-harness.md).
242
+ // Posts the prompt to POST /hooks/prime on this session's sidecar and injects the
243
+ // returned text as additionalContext. The sidecar decides what to inject, formats
244
+ // it, abstains when confidence is low and caps the token budget — see
245
+ // src/hooks/prime.ts — so this file stays thin.
246
+ //
247
+ // Turn it off without uninstalling: create ~/.claude/hooks/${PRIME_DISABLE_FILE}
248
+ // (an empty file). Remove the file to turn it back on. \`awm doctor\` reports which.
249
+ 'use strict';
250
+ const fs = require('fs');
251
+ const os = require('os');
252
+ const path = require('path');
253
+ const http = require('http');
254
+
255
+ let finder = null;
256
+ try { finder = require('./awm-find-sidecar.cjs'); } catch (e) { process.exit(0); }
257
+
258
+ // A slow AWM must never delay a prompt. Warm recall with rerank measures 1.4–1.8 s
259
+ // on a 30k-memory store; 4 s leaves margin. Connection refused returns in ~100 ms.
260
+ const TIMEOUT_MS = 4000;
261
+ const MIN_PROMPT_LEN = 15; // skip "yes" / "ok" / "go on" turns
262
+ // Set-level confidence below which the sidecar injects nothing. 0.10 rather than
263
+ // the 0.25 once documented as "balanced": measured 2026-09-11, 0.25 silenced
264
+ // specific prompts with one clear winner (top score 0.26–0.38) while passing a
265
+ // vague one at 0.92. The gate is on the shape of the score distribution, not on
266
+ // relevance, so specific questions are exactly the ones a high threshold loses.
267
+ const MIN_CONFIDENCE = 0.10;
268
+ const DISABLE_FILE = path.join(os.homedir(), '.claude', 'hooks', '${PRIME_DISABLE_FILE}');
269
+
270
+ let raw = '';
271
+ process.stdin.on('data', function (d) { raw += d; });
272
+ process.stdin.on('end', function () {
273
+ let payload = {};
274
+ try { payload = JSON.parse(raw || '{}'); } catch (e) { process.exit(0); }
275
+ main(payload).catch(function () { process.exit(0); });
276
+ });
277
+ process.stdin.on('error', function () { process.exit(0); });
278
+
279
+ function extractPrompt(j) {
280
+ for (const key of ['prompt', 'message', 'user_prompt', 'text', 'input']) {
281
+ if (typeof j[key] === 'string' && j[key].trim()) return j[key];
282
+ }
283
+ return null;
284
+ }
285
+
286
+ async function main(payload) {
287
+ if (fs.existsSync(DISABLE_FILE)) process.exit(0);
288
+ const prompt = extractPrompt(payload);
289
+ if (!prompt || prompt.trim().length < MIN_PROMPT_LEN) process.exit(0);
290
+
291
+ const found = await finder.findSidecar(payload.cwd);
292
+ const sc = found.sidecar;
293
+ if (!sc) {
294
+ finder.debugLog('prime', { agent: found.config.agentId, sidecar: null, live: found.all.length });
295
+ process.exit(0);
296
+ }
297
+
298
+ const body = JSON.stringify({
299
+ hook_event_name: 'UserPromptSubmit',
300
+ session_id: payload.session_id,
301
+ cwd: payload.cwd,
302
+ prompt: prompt,
303
+ minConfidence: MIN_CONFIDENCE,
304
+ });
305
+ const req = http.request({
306
+ host: '127.0.0.1', port: sc.port, path: '/hooks/prime', method: 'POST',
307
+ headers: {
308
+ 'Content-Type': 'application/json',
309
+ 'Content-Length': Buffer.byteLength(body),
310
+ 'Authorization': 'Bearer ' + found.config.secret,
311
+ },
312
+ timeout: TIMEOUT_MS,
313
+ }, function (res) {
314
+ let data = '';
315
+ res.on('data', function (c) { data += c; });
316
+ res.on('end', function () {
317
+ let out = null;
318
+ try { out = JSON.parse(data); } catch (e) { out = null; }
319
+ const inject = out && typeof out.inject === 'string' ? out.inject : '';
320
+ finder.debugLog('prime', { agent: found.config.agentId, port: sc.port, status: res.statusCode, kept: out && out.kept, total: out && out.total, reason: out && out.reason });
321
+ if (res.statusCode === 200 && inject) {
322
+ process.stdout.write(JSON.stringify({
323
+ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: inject },
324
+ }));
325
+ }
326
+ process.exit(0);
327
+ });
328
+ });
329
+ req.on('error', function () { process.exit(0); });
330
+ req.on('timeout', function () { req.destroy(); process.exit(0); });
331
+ req.end(body);
332
+ }
333
+ `;
334
+ /** Everything `awm setup` places in ~/.claude/hooks/. */
335
+ export const HOOK_SCRIPTS = [
336
+ { file: 'awm-find-sidecar.cjs', source: FIND_SIDECAR_SCRIPT },
337
+ { file: 'awm-checkpoint.cjs', source: CHECKPOINT_HOOK_SCRIPT },
338
+ { file: 'awm-prime.cjs', source: PRIME_HOOK_SCRIPT },
339
+ ];
340
+ //# sourceMappingURL=hook-scripts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-scripts.js","sourceRoot":"","sources":["../../src/adapters/hook-scripts.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,sCAAsC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH;kFACkF;AAClF,MAAM,CAAC,MAAM,iBAAiB,GAAG,QAAQ,CAAC;AAE1C,4EAA4E;AAC5E,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAEvD,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,OAAe,EAAE,EAAE,CAAC;KAC7C,IAAI,yCAAyC,iBAAiB,MAAM,OAAO;;;CAG/E,CAAC;AAEF,gFAAgF;AAEhF,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CACvC,sBAAsB,EACtB,6DAA6D,CAC9D,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6IH,CAAC;AAEF,gFAAgF;AAEhF,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAC1C,oBAAoB,EACpB,yEAAyE,CAC1E,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDH,CAAC;AAEF,gFAAgF;AAEhF,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CACrC,eAAe,EACf,0EAA0E,CAC3E,GAAG;;;;;;;8DAO0D,kBAAkB;;;;;;;;;;;;;;;;;;;;;oEAqBZ,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiErF,CAAC;AAEF,yDAAyD;AACzD,MAAM,CAAC,MAAM,YAAY,GAAoD;IAC3E,EAAE,IAAI,EAAE,sBAAsB,EAAE,MAAM,EAAE,mBAAmB,EAAE;IAC7D,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,EAAE,sBAAsB,EAAE;IAC9D,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,iBAAiB,EAAE;CACrD,CAAC"}
@@ -24,8 +24,12 @@ export interface SetupContext {
24
24
  hasDist: boolean;
25
25
  /** Hook sidecar secret token */
26
26
  hookSecret: string;
27
- /** Hook sidecar port */
27
+ /** Hook sidecar port (preferred; the sidecar walks upward from here when busy) */
28
28
  hookPort: string;
29
+ /** How many ports upward from hookPort the sidecar may try (0.14.2) */
30
+ hookPortRange: string;
31
+ /** Install the UserPromptSubmit prime hook (0.14.6; `--no-prime` turns it off) */
32
+ installPrime: boolean;
29
33
  /** Whether to use global scope */
30
34
  isGlobal: boolean;
31
35
  /** Windows platform */
@@ -59,7 +63,13 @@ export interface CLIAdapter {
59
63
  writeInstructions(ctx: SetupContext, skip: boolean): string;
60
64
  /** Configure hooks (if supported). Returns action summary. */
61
65
  writeHooks(ctx: SetupContext, skip: boolean): string;
62
- /** Validate that the setup is healthy. */
63
- diagnose(ctx: SetupContext): DiagnosticResult[];
66
+ /** Validate that the setup is healthy. May probe the network, hence possibly async. */
67
+ diagnose(ctx: SetupContext): DiagnosticResult[] | Promise<DiagnosticResult[]>;
68
+ /**
69
+ * The env block of an AWM server entry this adapter previously wrote, if any.
70
+ * `awm setup` re-runs preserve these values unless a flag overrides them, so an
71
+ * upgrade never repoints the database or renames the agent (0.14.6).
72
+ */
73
+ readExistingEnv?(isGlobal: boolean, cwd: string): Record<string, string> | null;
64
74
  }
65
75
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AAEH,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,OAAO,EAAE,OAAO,CAAC;IACjB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,QAAQ,EAAE,OAAO,CAAC;IAClB,uBAAuB;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,iEAAiE;IACjE,EAAE,EAAE,MAAM,CAAC;IACX,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,oBAAoB,EAAE,OAAO,CAAC;IAC9B,8CAA8C;IAC9C,mBAAmB,EAAE,OAAO,CAAC;IAE7B,wEAAwE;IACxE,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC;IAE1C,gEAAgE;IAChE,iBAAiB,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IAE5D,8DAA8D;IAC9D,UAAU,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IAErD,0CAA0C;IAC1C,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAAC;CACjD"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AAEH,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,OAAO,EAAE,OAAO,CAAC;IACjB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,aAAa,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,YAAY,EAAE,OAAO,CAAC;IACtB,kCAAkC;IAClC,QAAQ,EAAE,OAAO,CAAC;IAClB,uBAAuB;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,iEAAiE;IACjE,EAAE,EAAE,MAAM,CAAC;IACX,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,oBAAoB,EAAE,OAAO,CAAC;IAC9B,8CAA8C;IAC9C,mBAAmB,EAAE,OAAO,CAAC;IAE7B,wEAAwE;IACxE,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC;IAE1C,gEAAgE;IAChE,iBAAiB,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IAE5D,8DAA8D;IAC9D,UAAU,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IAErD,uFAAuF;IACvF,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,gBAAgB,EAAE,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAE9E;;;;OAIG;IACH,eAAe,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CACjF"}
package/dist/cli.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * awm health — check if a running server is healthy
12
12
  */
13
13
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
14
- import { resolve, dirname, basename } from 'node:path';
14
+ import { resolve, join, dirname, basename } from 'node:path';
15
15
  import { execSync } from 'node:child_process';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { VERSION } from './version.js';
@@ -44,6 +44,7 @@ AgentWorkingMemory — Cognitive memory for AI agents
44
44
 
45
45
  Usage:
46
46
  awm setup [target] [options] Configure AWM for an AI CLI
47
+ awm plugin Install as a Claude Code plugin (local, no GitHub)
47
48
  awm doctor [target|--all] Validate AWM integrations
48
49
  awm mcp Start MCP server (stdio)
49
50
  awm serve [--port <port>] Start HTTP API server
@@ -71,7 +72,10 @@ Setup options:
71
72
  --no-instructions Skip instruction file (CLAUDE.md, AGENTS.md, etc.)
72
73
  --no-claude-md Alias for --no-instructions
73
74
  --no-hooks Skip hook installation
74
- --hook-port PORT Sidecar port for hooks (default: 8401)
75
+ --no-prime Skip the UserPromptSubmit prime hook (auto-inject memories per prompt)
76
+ --hook-port PORT Preferred sidecar port (default: 8401; each session walks upward when busy)
77
+ --hook-port-range N Ports to try upward from --hook-port (default: 10)
78
+ Re-running setup keeps your existing agent id, db path, port and any env you added.
75
79
 
76
80
  Examples:
77
81
  awm setup --global Claude Code, global (recommended)
@@ -90,7 +94,9 @@ async function setup() {
90
94
  let skipInstructions = false;
91
95
  let isGlobal = false;
92
96
  let skipHooks = false;
93
- let hookPort = '8401';
97
+ let installPrime = true;
98
+ let hookPort;
99
+ let hookPortRange;
94
100
  for (let i = 1; i < args.length; i++) {
95
101
  if (args[i] === '--agent-id' && args[i + 1]) {
96
102
  agentId = args[++i];
@@ -104,9 +110,15 @@ async function setup() {
104
110
  else if (args[i] === '--no-hooks') {
105
111
  skipHooks = true;
106
112
  }
113
+ else if (args[i] === '--no-prime') {
114
+ installPrime = false;
115
+ }
107
116
  else if (args[i] === '--hook-port' && args[i + 1]) {
108
117
  hookPort = args[++i];
109
118
  }
119
+ else if (args[i] === '--hook-port-range' && args[i + 1]) {
120
+ hookPortRange = args[++i];
121
+ }
110
122
  else if (args[i] === '--global') {
111
123
  isGlobal = true;
112
124
  }
@@ -130,8 +142,11 @@ async function setup() {
130
142
  if (!adapter.supportsProjectScope && !isGlobal) {
131
143
  isGlobal = true;
132
144
  }
133
- // Build context
134
- const ctx = buildSetupContext({ agentId, dbPath, isGlobal, hookPort });
145
+ // Build context. A previous install's values survive unless a flag overrides them
146
+ // (0.14.6) — an upgrade must never repoint the database or rename the agent.
147
+ const existingEnv = adapter.readExistingEnv?.(isGlobal, process.cwd()) ?? null;
148
+ // `awm setup claude-code` -> client=claude-code, cursor -> client=cursor, and so on.
149
+ const ctx = buildSetupContext({ agentId, dbPath, isGlobal, hookPort, hookPortRange, installPrime, existingEnv, client: target });
135
150
  // Run adapter
136
151
  const configAction = adapter.writeMcpConfig(ctx);
137
152
  const instructionsAction = adapter.writeInstructions(ctx, skipInstructions);
@@ -151,8 +166,56 @@ AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
151
166
  Next steps:
152
167
  1. Restart ${adapter.name} to pick up the MCP server
153
168
  2. Memory tools will appear automatically${adapter.id === 'codex' ? ' (verify with /mcp)' : ''}
169
+ 3. \`awm doctor\` checks the install and lists the live sidecars
154
170
  `.trim());
155
171
  }
172
+ // ─── PLUGIN ──────────────────────────────────────
173
+ /**
174
+ * Print the two lines that install AWM as a Claude Code plugin FROM THIS INSTALL.
175
+ *
176
+ * Most users are on Windows and will never clone the repository, so
177
+ * `/plugin marketplace add CompleteIdeas/agent-working-memory` — which needs GitHub — is the
178
+ * wrong default for them. npm has already put a complete marketplace on their disk; they
179
+ * just have no way to know where it is. This tells them, with the real resolved path.
180
+ */
181
+ function pluginInfo() {
182
+ // dist/cli.js -> package root
183
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
184
+ const marketplace = join(pkgRoot, '.claude-plugin', 'marketplace.json');
185
+ if (!existsSync(marketplace)) {
186
+ console.log(`
187
+ AWM ${VERSION} — plugin not found in this install
188
+
189
+ Looked for: ${marketplace}
190
+
191
+ This happens when AWM is running from a source checkout that has not been built,
192
+ or from a package published before the plugin was bundled (< 0.15.3).
193
+
194
+ From a checkout: npm run build && npm run build:plugin
195
+ Otherwise: npm install -g agent-working-memory@latest
196
+ `.trimStart());
197
+ return;
198
+ }
199
+ console.log(`
200
+ AWM ${VERSION} — install as a Claude Code plugin
201
+
202
+ Paste these two lines into Claude Code:
203
+
204
+ /plugin marketplace add ${pkgRoot}
205
+ /plugin install awm@agent-working-memory
206
+
207
+ Then restart Claude Code. You get 19 memory tools, the session hooks, and the
208
+ usage guidance as a skill.
209
+
210
+ No GitHub access needed — the marketplace is this npm install, already on disk.
211
+
212
+ The store defaults to the same file \`awm setup --global\` uses, so a plugin
213
+ install and a CLI install share one memory rather than quietly keeping two.
214
+
215
+ Check it worked: awm doctor claude-code
216
+ More: https://github.com/CompleteIdeas/agent-working-memory/blob/master/docs/plugin.md
217
+ `.trimStart());
218
+ }
156
219
  // ─── DOCTOR ──────────────────────────────────────
157
220
  async function doctor() {
158
221
  const { getAdapter, listAdapters } = await import('./adapters/index.js');
@@ -185,7 +248,9 @@ async function doctor() {
185
248
  continue;
186
249
  }
187
250
  console.log(` ${adapter.name}:`);
188
- const results = adapter.diagnose(ctx);
251
+ const existingEnv = adapter.readExistingEnv?.(true, process.cwd()) ?? null;
252
+ const ctx = buildSetupContext({ isGlobal: true, existingEnv });
253
+ const results = await adapter.diagnose(ctx);
189
254
  for (const r of results) {
190
255
  const icon = r.status === 'ok' ? '+' : r.status === 'warn' ? '~' : 'x';
191
256
  console.log(` [${icon}] ${r.check}: ${r.message}`);
@@ -838,6 +903,9 @@ switch (command) {
838
903
  case 'setup':
839
904
  await setup();
840
905
  break;
906
+ case 'plugin':
907
+ pluginInfo();
908
+ break;
841
909
  case 'doctor':
842
910
  await doctor();
843
911
  break;