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
@@ -28,8 +28,12 @@ export interface SetupContext {
28
28
  hasDist: boolean;
29
29
  /** Hook sidecar secret token */
30
30
  hookSecret: string;
31
- /** Hook sidecar port */
31
+ /** Hook sidecar port (preferred; the sidecar walks upward from here when busy) */
32
32
  hookPort: string;
33
+ /** How many ports upward from hookPort the sidecar may try (0.14.2) */
34
+ hookPortRange: string;
35
+ /** Install the UserPromptSubmit prime hook (0.14.6; `--no-prime` turns it off) */
36
+ installPrime: boolean;
33
37
  /** Whether to use global scope */
34
38
  isGlobal: boolean;
35
39
  /** Windows platform */
@@ -70,6 +74,13 @@ export interface CLIAdapter {
70
74
  /** Configure hooks (if supported). Returns action summary. */
71
75
  writeHooks(ctx: SetupContext, skip: boolean): string;
72
76
 
73
- /** Validate that the setup is healthy. */
74
- diagnose(ctx: SetupContext): DiagnosticResult[];
77
+ /** Validate that the setup is healthy. May probe the network, hence possibly async. */
78
+ diagnose(ctx: SetupContext): DiagnosticResult[] | Promise<DiagnosticResult[]>;
79
+
80
+ /**
81
+ * The env block of an AWM server entry this adapter previously wrote, if any.
82
+ * `awm setup` re-runs preserve these values unless a flag overrides them, so an
83
+ * upgrade never repoints the database or renames the agent (0.14.6).
84
+ */
85
+ readExistingEnv?(isGlobal: boolean, cwd: string): Record<string, string> | null;
75
86
  }
package/src/cli.ts CHANGED
@@ -47,6 +47,7 @@ AgentWorkingMemory — Cognitive memory for AI agents
47
47
 
48
48
  Usage:
49
49
  awm setup [target] [options] Configure AWM for an AI CLI
50
+ awm plugin Install as a Claude Code plugin (local, no GitHub)
50
51
  awm doctor [target|--all] Validate AWM integrations
51
52
  awm mcp Start MCP server (stdio)
52
53
  awm serve [--port <port>] Start HTTP API server
@@ -74,7 +75,10 @@ Setup options:
74
75
  --no-instructions Skip instruction file (CLAUDE.md, AGENTS.md, etc.)
75
76
  --no-claude-md Alias for --no-instructions
76
77
  --no-hooks Skip hook installation
77
- --hook-port PORT Sidecar port for hooks (default: 8401)
78
+ --no-prime Skip the UserPromptSubmit prime hook (auto-inject memories per prompt)
79
+ --hook-port PORT Preferred sidecar port (default: 8401; each session walks upward when busy)
80
+ --hook-port-range N Ports to try upward from --hook-port (default: 10)
81
+ Re-running setup keeps your existing agent id, db path, port and any env you added.
78
82
 
79
83
  Examples:
80
84
  awm setup --global Claude Code, global (recommended)
@@ -95,7 +99,9 @@ async function setup() {
95
99
  let skipInstructions = false;
96
100
  let isGlobal = false;
97
101
  let skipHooks = false;
98
- let hookPort = '8401';
102
+ let installPrime = true;
103
+ let hookPort: string | undefined;
104
+ let hookPortRange: string | undefined;
99
105
 
100
106
  for (let i = 1; i < args.length; i++) {
101
107
  if (args[i] === '--agent-id' && args[i + 1]) {
@@ -106,8 +112,12 @@ async function setup() {
106
112
  skipInstructions = true;
107
113
  } else if (args[i] === '--no-hooks') {
108
114
  skipHooks = true;
115
+ } else if (args[i] === '--no-prime') {
116
+ installPrime = false;
109
117
  } else if (args[i] === '--hook-port' && args[i + 1]) {
110
118
  hookPort = args[++i];
119
+ } else if (args[i] === '--hook-port-range' && args[i + 1]) {
120
+ hookPortRange = args[++i];
111
121
  } else if (args[i] === '--global') {
112
122
  isGlobal = true;
113
123
  } else if (!args[i].startsWith('--')) {
@@ -133,8 +143,11 @@ async function setup() {
133
143
  isGlobal = true;
134
144
  }
135
145
 
136
- // Build context
137
- const ctx = buildSetupContext({ agentId, dbPath, isGlobal, hookPort });
146
+ // Build context. A previous install's values survive unless a flag overrides them
147
+ // (0.14.6) — an upgrade must never repoint the database or rename the agent.
148
+ const existingEnv = adapter.readExistingEnv?.(isGlobal, process.cwd()) ?? null;
149
+ // `awm setup claude-code` -> client=claude-code, cursor -> client=cursor, and so on.
150
+ const ctx = buildSetupContext({ agentId, dbPath, isGlobal, hookPort, hookPortRange, installPrime, existingEnv, client: target });
138
151
 
139
152
  // Run adapter
140
153
  const configAction = adapter.writeMcpConfig(ctx);
@@ -157,9 +170,62 @@ AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
157
170
  Next steps:
158
171
  1. Restart ${adapter.name} to pick up the MCP server
159
172
  2. Memory tools will appear automatically${adapter.id === 'codex' ? ' (verify with /mcp)' : ''}
173
+ 3. \`awm doctor\` checks the install and lists the live sidecars
160
174
  `.trim());
161
175
  }
162
176
 
177
+ // ─── PLUGIN ──────────────────────────────────────
178
+
179
+ /**
180
+ * Print the two lines that install AWM as a Claude Code plugin FROM THIS INSTALL.
181
+ *
182
+ * Most users are on Windows and will never clone the repository, so
183
+ * `/plugin marketplace add CompleteIdeas/agent-working-memory` — which needs GitHub — is the
184
+ * wrong default for them. npm has already put a complete marketplace on their disk; they
185
+ * just have no way to know where it is. This tells them, with the real resolved path.
186
+ */
187
+ function pluginInfo(): void {
188
+ // dist/cli.js -> package root
189
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
190
+ const marketplace = join(pkgRoot, '.claude-plugin', 'marketplace.json');
191
+
192
+ if (!existsSync(marketplace)) {
193
+ console.log(`
194
+ AWM ${VERSION} — plugin not found in this install
195
+
196
+ Looked for: ${marketplace}
197
+
198
+ This happens when AWM is running from a source checkout that has not been built,
199
+ or from a package published before the plugin was bundled (< 0.15.3).
200
+
201
+ From a checkout: npm run build && npm run build:plugin
202
+ Otherwise: npm install -g agent-working-memory@latest
203
+ `.trimStart());
204
+ return;
205
+ }
206
+
207
+ console.log(`
208
+ AWM ${VERSION} — install as a Claude Code plugin
209
+
210
+ Paste these two lines into Claude Code:
211
+
212
+ /plugin marketplace add ${pkgRoot}
213
+ /plugin install awm@agent-working-memory
214
+
215
+ Then restart Claude Code. You get 19 memory tools, the session hooks, and the
216
+ usage guidance as a skill.
217
+
218
+ No GitHub access needed — the marketplace is this npm install, already on disk.
219
+
220
+ The store defaults to the same file \`awm setup --global\` uses, so a plugin
221
+ install and a CLI install share one memory rather than quietly keeping two.
222
+
223
+ Check it worked: awm doctor claude-code
224
+ More: https://github.com/CompleteIdeas/agent-working-memory/blob/master/docs/plugin.md
225
+ `.trimStart());
226
+ }
227
+
228
+
163
229
  // ─── DOCTOR ──────────────────────────────────────
164
230
 
165
231
  async function doctor() {
@@ -197,7 +263,9 @@ async function doctor() {
197
263
  }
198
264
 
199
265
  console.log(` ${adapter.name}:`);
200
- const results = adapter.diagnose(ctx);
266
+ const existingEnv = adapter.readExistingEnv?.(true, process.cwd()) ?? null;
267
+ const ctx = buildSetupContext({ isGlobal: true, existingEnv });
268
+ const results = await adapter.diagnose(ctx);
201
269
  for (const r of results) {
202
270
  const icon = r.status === 'ok' ? '+' : r.status === 'warn' ? '~' : 'x';
203
271
  console.log(` [${icon}] ${r.check}: ${r.message}`);
@@ -789,6 +857,9 @@ switch (command) {
789
857
  case 'setup':
790
858
  await setup();
791
859
  break;
860
+ case 'plugin':
861
+ pluginInfo();
862
+ break;
792
863
  case 'doctor':
793
864
  await doctor();
794
865
  break;
@@ -0,0 +1,28 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Fallback agent identity when AWM_AGENT_ID / WORKER_NAME are unset.
6
+ *
7
+ * Derived from the project directory so a plain `claude` launch still binds to
8
+ * the right store: `Personal-Projects` → 'personal'; everything else → 'work'.
9
+ * The AWM package itself lives under Personal-Projects, so an AgentSynapse
10
+ * checkout is guarded first — a stray server cwd must not mis-bind.
11
+ *
12
+ * ONE definition, three consumers that must agree:
13
+ * - src/mcp.ts — what the server actually binds to
14
+ * - src/adapters/common.ts — what `awm setup` reports and seeds
15
+ * - the shipped hook scripts (src/adapters/hook-scripts.ts) carry a
16
+ * line-for-line JavaScript mirror, because a hook cannot import from dist.
17
+ * tests/adapters/hook-scripts.test.ts asserts the mirror agrees with this.
18
+ */
19
+ export function deriveAgentFromDir(dir: string): string {
20
+ const d = String(dir ?? '').replace(/\\/g, '/');
21
+ if (/\/AgentSynapse\//i.test(d)) return 'work';
22
+ return /\/Personal-Projects(\/|$)/i.test(d) ? 'personal' : 'work';
23
+ }
24
+
25
+ /** The agent id a server process would resolve to for `dir`, honouring the env first. */
26
+ export function resolveAgentId(dir: string, env: NodeJS.ProcessEnv = process.env): string {
27
+ return env.AWM_AGENT_ID ?? env.WORKER_NAME ?? deriveAgentFromDir(dir);
28
+ }
@@ -491,6 +491,24 @@ async function createNewEngram(
491
491
 
492
492
  const tags = [...(input.tags ?? [])];
493
493
  if (isLowSalience && !tags.includes('low-salience')) tags.push('low-salience');
494
+
495
+ // Which CLIENT wrote this: `client=claude-code`, `client=claude-desktop`, `client=cursor`.
496
+ //
497
+ // Deliberately not called `surface`: memory_whoami already reports a `surface` field
498
+ // meaning the TRANSPORT (mcp | http), and one word for two concepts is how you lose an
499
+ // hour six months from now. Transport is how the request arrived; client is which
500
+ // application it arrived from.
501
+ // AWM_CLIENT is set by the thing that launches the server (the Claude Code plugin, the
502
+ // Desktop extension, `awm setup`), never by the caller, so it records where a memory came
503
+ // from rather than what the writer claims.
504
+ //
505
+ // Captured by default rather than behind a flag, because provenance is one-way: a tag can
506
+ // be ignored or stripped later, but the origin of a memory written without one is gone for
507
+ // good. An explicit `client=` in the caller's tags always wins.
508
+ const client = (process.env.AWM_CLIENT ?? '').trim();
509
+ if (client && !tags.some(t => t.toLowerCase().startsWith('client='))) {
510
+ tags.push(`client=${client}`);
511
+ }
494
512
 
495
513
  // Auto-tag meta-tags (default-OFF, AWM_AUTOTAG=1). extractMetaTags emits
496
514
  // `entity:<ProperNoun>` and `cat:<category>` tags from concept+content. These
@@ -36,7 +36,12 @@ import { packRecallByBudget, estimateTokens, type PackedRecall } from '../core/t
36
36
 
37
37
  /** Shape returned by the sidecar's `activate` dependency. */
38
38
  export interface PrimeCandidate {
39
- engram: { id: string; concept: string; content: string; memoryClass?: string; validTo?: string | null };
39
+ engram: {
40
+ id: string; concept: string; content: string; memoryClass?: string;
41
+ validTo?: string | null;
42
+ /** Used for the age label; Date from the store, string over HTTP. */
43
+ createdAt?: Date | string | null;
44
+ };
40
45
  score: number;
41
46
  summary?: string;
42
47
  confidence?: number;
@@ -64,21 +69,31 @@ export interface PrimeResult {
64
69
  export const PRIME_DEFAULTS = {
65
70
  maxTokens: 600,
66
71
  /**
67
- * 0.25 = the "balanced" threshold AWM's own recall docs recommend for
68
- * acting on a memory. Priming is acting on it without being asked, so it
69
- * should not be looser than the value the docs suggest for deliberate use.
72
+ * 0.10 (was 0.25 until 0.14.6). The confidence this gates on describes the
73
+ * SHAPE of the score distribution — how far the top results stand out from
74
+ * the rest — not relevance. Measured 2026-09-11 on a 30k-memory store: at
75
+ * 0.25 two specific prompts with one clear winner (top score 0.26 and 0.38)
76
+ * were silenced while a vague prompt passed at 0.92. Those specific prompts
77
+ * are exactly what priming exists for. 0.10 passed all three and still
78
+ * abstains on the garbage case (confidence ≈ 0.05).
70
79
  */
71
- minConfidence: 0.25,
80
+ minConfidence: 0.10,
72
81
  minScore: 0.10,
73
82
  } as const;
74
83
 
75
84
  /** One injected line. Deliberately terser than the MCP recall format — this is
76
85
  * context the agent didn't ask for, so it should read as a brief note, not a
77
86
  * report. The id is retained so the agent can act on it (feedback, supersede). */
78
- function formatPrimeLine(c: PrimeCandidate): string {
87
+ function formatPrimeLine(c: PrimeCandidate, now: number = Date.now()): string {
79
88
  const body = c.summary ?? c.engram.content;
80
89
  const validity = c.engram.validTo ? ` [valid until ${c.engram.validTo}]` : '';
81
- return `- ${c.engram.concept}${validity} [${c.engram.id}]: ${body}`;
90
+ // Class + age make the volatility rubric mechanical: a 90-day-old working
91
+ // memory reads differently from a 2-day-old canonical one, and the agent
92
+ // should not have to look either up before deciding how much to trust it.
93
+ const cls = c.engram.memoryClass ?? 'working';
94
+ const created = c.engram.createdAt ? new Date(c.engram.createdAt as any).getTime() : NaN;
95
+ const age = Number.isFinite(created) ? ` · ${Math.max(0, Math.round((now - created) / 86_400_000))}d` : '';
96
+ return `- [${cls}${age}] ${c.engram.concept}${validity} [${c.engram.id}]: ${body}`;
82
97
  }
83
98
 
84
99
  /**
package/src/mcp.ts CHANGED
@@ -30,6 +30,7 @@
30
30
  */
31
31
 
32
32
  import { readFileSync } from 'node:fs';
33
+ import { deriveAgentFromDir as deriveAgentFromDirShared } from './core/agent-id.js';
33
34
  import { resolve, basename } from 'node:path';
34
35
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
35
36
 
@@ -111,17 +112,10 @@ const BACKEND: StoreBackend = getConfiguredBackend();
111
112
  const DB_PATH = process.env.AWM_DB_PATH ?? (BACKEND === 'pglite' ? 'memory-pglite' : 'memory.db');
112
113
 
113
114
  // Fallback agent selection when AWM_AGENT_ID/WORKER_NAME are unset: derive from
114
- // the project directory so plain `claude` launches still bind to the right
115
- // store. Personal-Projects -> 'personal'; everything else -> 'work' (the
116
- // primary store). MUST stay in sync with the SessionStart hook
117
- // (~/.claude/hooks/awm-session-start.ps1) so the hook's restore and the
118
- // server's reads/writes never diverge. Guard the AWM package's own path
119
- // (it lives under Personal-Projects) so a stray server cwd can't mis-bind.
120
- function deriveAgentFromDir(): string {
121
- const dir = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replace(/\\/g, '/');
122
- if (/\/AgentSynapse\//i.test(dir)) return 'work';
123
- return /\/Personal-Projects(\/|$)/i.test(dir) ? 'personal' : 'work';
124
- }
115
+ // the project directory (core/agent-id.ts — one definition shared with `awm setup`
116
+ // and mirrored in the shipped hook scripts, so the server, the installer and the
117
+ // hooks always name the same pool for a given directory).
118
+ const deriveAgentFromDir = () => deriveAgentFromDirShared(process.env.CLAUDE_PROJECT_DIR ?? process.cwd());
125
119
  const AGENT_ID = process.env.AWM_AGENT_ID ?? process.env.WORKER_NAME ?? deriveAgentFromDir();
126
120
  const HOOK_PORT = parseInt(process.env.AWM_HOOK_PORT ?? '8401', 10);
127
121
  // 0.14.2: ports to try upward from HOOK_PORT when it is busy (see sidecar.ts).
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ // Copyright 2026 Robert Winter / Complete Ideas
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+ // Entry point for the Claude Code plugin's MCP server.
6
+ //
7
+ // This is a REAL source file, not a string inside the generator, because it is real code:
8
+ // it gets syntax-checked, linted and tested like everything else. `npm run build:plugin`
9
+ // copies it to plugin/bin/ and substitutes __AWM_VERSION__.
10
+ //
11
+ // Two jobs a bare `mcpServers` entry in plugin.json cannot do:
12
+ //
13
+ // 1. Default the store to ~/.awm/memory.db — the SAME file `awm setup --global` writes.
14
+ // Without this the plugin opens its own empty database and "your memory follows you
15
+ // between Claude Code, Desktop and Cowork" is simply false.
16
+ //
17
+ // 2. Find the server. A plugin does not know whether the package is installed globally,
18
+ // sits in a local node_modules, or is a checkout on disk.
19
+ //
20
+ // Fails loudly on stderr. A silent exit here looks like "the memory tools just aren't there",
21
+ // which is the least debuggable failure this could have.
22
+ 'use strict';
23
+
24
+ const { join, resolve, sep } = require('node:path');
25
+ const { homedir } = require('node:os');
26
+ const { existsSync } = require('node:fs');
27
+ const { pathToFileURL } = require('node:url');
28
+ const { spawn, execFileSync } = require('node:child_process');
29
+
30
+ const NL = '\n';
31
+
32
+ // Claude Desktop substitutes user_config values into env, and a field the user cleared
33
+ // arrives as an empty string rather than being absent. An empty AWM_AGENT_ID would override
34
+ // the server's directory-derived default with nothing, so drop empties before anything reads
35
+ // them and let the normal defaults apply.
36
+ for (const k of ['AWM_DB_PATH', 'AWM_AGENT_ID', 'AWM_HOOK_PORT', 'AWM_PACKAGE_ROOT']) {
37
+ if (process.env[k] !== undefined && String(process.env[k]).trim() === '') delete process.env[k];
38
+ }
39
+
40
+ // An explicit AWM_DB_PATH always wins, so per-project pools still work.
41
+ if (!process.env.AWM_DB_PATH) {
42
+ process.env.AWM_DB_PATH = join(homedir(), '.awm', 'memory.db').split(sep).join('/');
43
+ }
44
+
45
+ const tried = [];
46
+ function candidate(p) {
47
+ if (!p) return null;
48
+ tried.push(p);
49
+ return existsSync(p) ? p : null;
50
+ }
51
+
52
+ // In priority order. npx is last: on a cold cache it rebuilds better-sqlite3, which takes
53
+ // minutes and is indistinguishable from a hang.
54
+ let found =
55
+ // 1. An explicit override.
56
+ candidate(process.env.AWM_PACKAGE_ROOT && join(process.env.AWM_PACKAGE_ROOT, 'dist', 'mcp.js')) ||
57
+ // 2. The plugin sitting inside an AWM checkout (plugin/bin -> ../../dist).
58
+ candidate(resolve(__dirname, '..', '..', 'dist', 'mcp.js')) ||
59
+ // 3. A local dependency of the project being worked on.
60
+ candidate(join(process.cwd(), 'node_modules', 'agent-working-memory', 'dist', 'mcp.js'));
61
+
62
+ // 4. A global install. require.resolve does not search global roots, so ask npm once.
63
+ if (!found) {
64
+ try {
65
+ const isWin = process.platform === 'win32';
66
+ const root = execFileSync(isWin ? 'npm.cmd' : 'npm', ['root', '-g'], {
67
+ encoding: 'utf-8',
68
+ shell: isWin,
69
+ stdio: ['ignore', 'pipe', 'ignore'],
70
+ }).trim();
71
+ found = candidate(root && join(root, 'agent-working-memory', 'dist', 'mcp.js'));
72
+ } catch {
73
+ /* npm not on PATH — fall through */
74
+ }
75
+ }
76
+
77
+ // 5. Anywhere node can resolve it from here.
78
+ if (!found) {
79
+ try {
80
+ found = require.resolve('agent-working-memory/dist/mcp.js');
81
+ } catch {
82
+ /* not installed where this script can see it */
83
+ }
84
+ }
85
+
86
+ if (found) {
87
+ // dist/ is ESM ("type": "module"), so import() — require() throws ERR_REQUIRE_ESM.
88
+ import(pathToFileURL(found).href).catch((e) => {
89
+ process.stderr.write('[awm] failed to load ' + found + ': ' + e.message + NL);
90
+ process.exit(1);
91
+ });
92
+ } else {
93
+ // Last resort, pinned so the tools match the hook scripts shipped beside them.
94
+ // shell:true on Windows: spawning npx.cmd directly throws EINVAL since Node 18.20/20.12.
95
+ const isWin = process.platform === 'win32';
96
+ const child = spawn(
97
+ isWin ? 'npx.cmd' : 'npx',
98
+ ['-y', '-p', 'agent-working-memory@__AWM_VERSION__', 'awm', 'mcp'],
99
+ { stdio: 'inherit', env: process.env, shell: isWin }
100
+ );
101
+ child.on('error', (e) => {
102
+ process.stderr.write('[awm] could not start the MCP server: ' + e.message + NL);
103
+ process.stderr.write('[awm] looked in:' + NL);
104
+ for (const t of tried) process.stderr.write(' ' + t + NL);
105
+ process.stderr.write('[awm] fix: npm install -g agent-working-memory' + NL);
106
+ process.exit(1);
107
+ });
108
+ child.on('exit', (code) => process.exit(code == null ? 0 : code));
109
+ }
@@ -49,13 +49,25 @@ export type StoreBackend = 'sqlite' | 'pglite' | 'postgres';
49
49
  function detectBackendFromDisk(): StoreBackend | null {
50
50
  // If AWM_DB_PATH is set, infer from its shape.
51
51
  const explicitPath = process.env.AWM_DB_PATH;
52
- if (explicitPath && existsSync(explicitPath)) {
53
- try {
54
- const stat = statSync(explicitPath);
55
- if (stat.isDirectory()) return 'pglite'; // PGlite uses a directory
56
- if (stat.isFile()) return 'sqlite'; // SQLite is a single file
57
- } catch { /* fall through */ }
58
- }
52
+ if (explicitPath) {
53
+ if (existsSync(explicitPath)) {
54
+ try {
55
+ const stat = statSync(explicitPath);
56
+ if (stat.isDirectory()) return 'pglite'; // PGlite uses a directory
57
+ if (stat.isFile()) return 'sqlite'; // SQLite is a single file
58
+ } catch { /* fall through to the default below */ }
59
+ }
60
+ // The caller named a path that does not exist yet — a brand-new store. Default it to
61
+ // SQLite and STOP: falling through to the cwd conventions below would pick the backend
62
+ // from files that have nothing to do with the path that was asked for.
63
+ //
64
+ // Not hypothetical. Running from a checkout containing a `memory-pglite/` directory
65
+ // made a fresh AWM_DB_PATH=/tmp/new-store.db resolve to PGlite, which is
66
+ // single-process — so two surfaces pointed at 'the same store' would have had the
67
+ // second refuse to start. Found by the cross-surface end-to-end test.
68
+ // PGlite at a new path is still reachable via AWM_STORE_BACKEND=pglite.
69
+ return 'sqlite';
70
+ }
59
71
 
60
72
  // Otherwise look for the conventional defaults in cwd.
61
73
  // PGlite directory wins over SQLite file when both exist — assume the