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.
- package/.claude-plugin/marketplace.json +17 -0
- package/README.md +131 -122
- package/dist/adapters/claude-code.d.ts +9 -0
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +235 -132
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/common.d.ts +47 -5
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +111 -41
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/hook-scripts.d.ts +54 -0
- package/dist/adapters/hook-scripts.d.ts.map +1 -0
- package/dist/adapters/hook-scripts.js +340 -0
- package/dist/adapters/hook-scripts.js.map +1 -0
- package/dist/adapters/types.d.ts +13 -3
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cli.js +74 -6
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-id.d.ts +19 -0
- package/dist/core/agent-id.d.ts.map +1 -0
- package/dist/core/agent-id.js +28 -0
- package/dist/core/agent-id.js.map +1 -0
- package/dist/core/write-pipeline.js +17 -0
- package/dist/core/write-pipeline.js.map +1 -1
- package/dist/hooks/prime.d.ts +10 -4
- package/dist/hooks/prime.d.ts.map +1 -1
- package/dist/hooks/prime.js +16 -6
- package/dist/hooks/prime.js.map +1 -1
- package/dist/mcp.js +5 -12
- package/dist/mcp.js.map +1 -1
- package/dist/storage/factory.d.ts.map +1 -1
- package/dist/storage/factory.js +20 -8
- package/dist/storage/factory.js.map +1 -1
- package/package.json +100 -90
- package/plugin/.claude-plugin/plugin.json +34 -0
- package/plugin/README.md +36 -0
- package/plugin/bin/awm-mcp-launcher.cjs +109 -0
- package/plugin/hooks/awm-checkpoint.cjs +55 -0
- package/plugin/hooks/awm-db-mutation-reminder.cjs +25 -0
- package/plugin/hooks/awm-find-sidecar.cjs +145 -0
- package/plugin/hooks/awm-prime.cjs +97 -0
- package/plugin/hooks/hooks.json +66 -0
- package/plugin/skills/awm-memory/SKILL.md +445 -0
- package/src/adapters/claude-code.ts +224 -126
- package/src/adapters/common.ts +119 -42
- package/src/adapters/hook-scripts.ts +370 -0
- package/src/adapters/types.ts +14 -3
- package/src/cli.ts +76 -5
- package/src/core/agent-id.ts +28 -0
- package/src/core/write-pipeline.ts +18 -0
- package/src/hooks/prime.ts +22 -7
- package/src/mcp.ts +5 -11
- package/src/plugin/awm-mcp-launcher.cjs +109 -0
- package/src/storage/factory.ts +19 -7
package/src/adapters/types.ts
CHANGED
|
@@ -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
|
-
--
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
package/src/hooks/prime.ts
CHANGED
|
@@ -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: {
|
|
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
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
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
|
+
}
|
package/src/storage/factory.ts
CHANGED
|
@@ -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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|