agent-working-memory 0.14.1 → 0.14.6
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/README.md +240 -653
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +234 -131
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/common.d.ts +44 -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 +21 -5
- 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/whoami.d.ts +9 -1
- package/dist/core/whoami.d.ts.map +1 -1
- package/dist/core/whoami.js +12 -2
- package/dist/core/whoami.js.map +1 -1
- package/dist/engine/activation.d.ts +2 -0
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +18 -3
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/eval.d.ts +15 -0
- package/dist/engine/eval.d.ts.map +1 -1
- package/dist/engine/eval.js +23 -0
- package/dist/engine/eval.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/hooks/sidecar.d.ts +18 -2
- package/dist/hooks/sidecar.d.ts.map +1 -1
- package/dist/hooks/sidecar.js +30 -4
- package/dist/hooks/sidecar.js.map +1 -1
- package/dist/mcp.js +48 -19
- package/dist/mcp.js.map +1 -1
- package/dist/storage/pglite.d.ts +7 -0
- package/dist/storage/pglite.d.ts.map +1 -1
- package/dist/storage/pglite.js +17 -3
- package/dist/storage/pglite.js.map +1 -1
- package/dist/storage/postgres.d.ts +7 -0
- package/dist/storage/postgres.d.ts.map +1 -1
- package/dist/storage/postgres.js +17 -3
- package/dist/storage/postgres.js.map +1 -1
- package/dist/storage/sqlite.d.ts +12 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +24 -3
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types/engram.d.ts +16 -0
- package/dist/types/engram.d.ts.map +1 -1
- package/dist/types/eval.d.ts +2 -0
- package/dist/types/eval.d.ts.map +1 -1
- package/package.json +94 -72
- package/src/adapters/claude-code.ts +223 -125
- package/src/adapters/common.ts +117 -42
- package/src/adapters/hook-scripts.ts +370 -0
- package/src/adapters/types.ts +14 -3
- package/src/cli.ts +19 -5
- package/src/core/agent-id.ts +28 -0
- package/src/core/whoami.ts +11 -1
- package/src/engine/activation.ts +18 -3
- package/src/engine/eval.ts +34 -0
- package/src/hooks/prime.ts +22 -7
- package/src/hooks/sidecar.ts +50 -6
- package/src/mcp.ts +50 -18
- package/src/storage/pglite.ts +22 -4
- package/src/storage/postgres.ts +22 -4
- package/src/storage/sqlite.ts +26 -4
- package/src/types/engram.ts +16 -0
- package/src/types/eval.ts +3 -1
|
@@ -12,6 +12,8 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
|
12
12
|
import { join, dirname, basename } from 'node:path';
|
|
13
13
|
import type { CLIAdapter, SetupContext, DiagnosticResult } from './types.js';
|
|
14
14
|
import { resolveMcpCommand, homedir, AWM_INSTRUCTION_CONTENT, upsertAwmSection } from './common.js';
|
|
15
|
+
import { HOOK_SCRIPTS, AWM_HOOKS_VERSION, PRIME_DISABLE_FILE, type HooksRecord } from './hook-scripts.js';
|
|
16
|
+
import { request as httpRequest } from 'node:http';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* DB-mutation reminder hook (PostToolUse on Bash|PowerShell).
|
|
@@ -48,40 +50,101 @@ process.stdin.on('end', () => {
|
|
|
48
50
|
});
|
|
49
51
|
`;
|
|
50
52
|
|
|
53
|
+
const SERVER_NAME = 'agent-working-memory';
|
|
54
|
+
|
|
55
|
+
/** Where this adapter keeps the AWM server entry for a scope. */
|
|
56
|
+
function mcpJsonPathFor(isGlobal: boolean, cwd: string): string {
|
|
57
|
+
// Global: ~/.mcp.json (standard MCP location, all projects)
|
|
58
|
+
// Project: .claude/mcp.json (Claude Code's native project scope)
|
|
59
|
+
return isGlobal ? join(homedir(), '.mcp.json') : join(cwd, '.claude', 'mcp.json');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readJson(path: string): any | null {
|
|
63
|
+
if (!existsSync(path)) return null;
|
|
64
|
+
try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { return null; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Exactly the hook scripts `awm setup` installs. Used to recognise our own groups in
|
|
69
|
+
* settings.json so they can be replaced on a re-run.
|
|
70
|
+
*
|
|
71
|
+
* This list must stay TIGHT. It was briefly a loose `awm-*.cjs` pattern, and on a real
|
|
72
|
+
* install that silently deleted the owner's own `awm-dev-log.cjs` and
|
|
73
|
+
* `awm-recall-stamp.cjs` hooks, which merely share the prefix. Anything not installed by
|
|
74
|
+
* this adapter belongs to the user.
|
|
75
|
+
*/
|
|
76
|
+
const OWNED_HOOK_FILES = [
|
|
77
|
+
...HOOK_SCRIPTS.map(h => h.file),
|
|
78
|
+
'awm-db-mutation-reminder.cjs',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/** True for a settings.json hook group that `awm setup` wrote, in any version. */
|
|
82
|
+
function isAwmHookGroup(group: any): boolean {
|
|
83
|
+
const text = JSON.stringify(group ?? {});
|
|
84
|
+
return OWNED_HOOK_FILES.some(f => text.includes(f)) // 0.14.6 scripts
|
|
85
|
+
|| /curl[^"]*\/hooks\/checkpoint/.test(text) // pre-0.14.6 inline curl
|
|
86
|
+
|| text.includes('MEMORY: (1) Did you learn'); // the Stop reminder
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Merge one hook group for `event`: drop every AWM-owned group, keep the user's, append ours. */
|
|
90
|
+
function setHookGroup(settings: any, event: string, group: any | null): void {
|
|
91
|
+
const others = (settings.hooks[event] || []).filter((g: any) => !isAwmHookGroup(g));
|
|
92
|
+
settings.hooks[event] = group ? [...others, group] : others;
|
|
93
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface LiveSidecar { port: number; agentId: string | null; version: string; pid: number | null }
|
|
97
|
+
|
|
98
|
+
/** GET /health on one sidecar port; null when nothing answers or it is not a sidecar. */
|
|
99
|
+
function probeSidecar(port: number, timeoutMs = 400): Promise<LiveSidecar | null> {
|
|
100
|
+
return new Promise(resolve => {
|
|
101
|
+
const req = httpRequest({ host: '127.0.0.1', port, path: '/health', method: 'GET', timeout: timeoutMs }, res => {
|
|
102
|
+
let data = '';
|
|
103
|
+
res.on('data', c => { data += c; });
|
|
104
|
+
res.on('end', () => {
|
|
105
|
+
try {
|
|
106
|
+
const j = JSON.parse(data);
|
|
107
|
+
resolve(j && j.sidecar ? { port, agentId: j.agentId ?? null, version: j.version ?? '0.0.0', pid: j.pid ?? null } : null);
|
|
108
|
+
} catch { resolve(null); }
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
req.on('error', () => resolve(null));
|
|
112
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
113
|
+
req.end();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
51
117
|
const adapter: CLIAdapter = {
|
|
52
118
|
id: 'claude-code',
|
|
53
119
|
name: 'Claude Code',
|
|
54
120
|
supportsProjectScope: true,
|
|
55
121
|
supportsGlobalScope: true,
|
|
56
122
|
|
|
123
|
+
readExistingEnv(isGlobal: boolean, cwd: string): Record<string, string> | null {
|
|
124
|
+
const entry = readJson(mcpJsonPathFor(isGlobal, cwd))?.mcpServers?.[SERVER_NAME];
|
|
125
|
+
return entry?.env && typeof entry.env === 'object' ? { ...entry.env } : null;
|
|
126
|
+
},
|
|
127
|
+
|
|
57
128
|
writeMcpConfig(ctx: SetupContext): string {
|
|
58
129
|
const mcpCmd = resolveMcpCommand(ctx);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
const mcpJsonPath = ctx.isGlobal
|
|
64
|
-
? join(homedir(), '.mcp.json')
|
|
65
|
-
: join(ctx.cwd, '.claude', 'mcp.json');
|
|
66
|
-
|
|
130
|
+
// ctx.envVars already layers recommended defaults < the user's existing env < owned keys
|
|
131
|
+
// (buildEnvVars). Only the env and the command are ours; anything else the user put on
|
|
132
|
+
// the entry (e.g. a `type`, a `cwd`) is carried across.
|
|
133
|
+
const mcpJsonPath = mcpJsonPathFor(ctx.isGlobal, ctx.cwd);
|
|
67
134
|
const mcpDir = dirname(mcpJsonPath);
|
|
68
|
-
if (!existsSync(mcpDir)) {
|
|
69
|
-
mkdirSync(mcpDir, { recursive: true });
|
|
70
|
-
}
|
|
135
|
+
if (!existsSync(mcpDir)) mkdirSync(mcpDir, { recursive: true });
|
|
71
136
|
|
|
72
|
-
|
|
73
|
-
if (existsSync(mcpJsonPath)) {
|
|
74
|
-
try {
|
|
75
|
-
existing = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
76
|
-
} catch {
|
|
77
|
-
existing = {};
|
|
78
|
-
}
|
|
79
|
-
}
|
|
137
|
+
const existing = readJson(mcpJsonPath) ?? {};
|
|
80
138
|
if (!existing.mcpServers) existing.mcpServers = {};
|
|
81
|
-
|
|
82
|
-
|
|
139
|
+
const prior = existing.mcpServers[SERVER_NAME] ?? {};
|
|
140
|
+
const priorEnv: Record<string, string> = prior.env ?? {};
|
|
141
|
+
const preserved = Object.keys(priorEnv).filter(k => ctx.envVars[k] === priorEnv[k]);
|
|
142
|
+
existing.mcpServers[SERVER_NAME] = { ...prior, ...mcpCmd, env: ctx.envVars };
|
|
83
143
|
writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
|
|
84
|
-
|
|
144
|
+
const kept = Object.keys(priorEnv).length
|
|
145
|
+
? ` (${preserved.length} of ${Object.keys(priorEnv).length} existing env values carried over)`
|
|
146
|
+
: '';
|
|
147
|
+
return `MCP config: ${mcpJsonPath}${kept}`;
|
|
85
148
|
},
|
|
86
149
|
|
|
87
150
|
writeInstructions(ctx: SetupContext, skip: boolean): string {
|
|
@@ -99,20 +162,30 @@ const adapter: CLIAdapter = {
|
|
|
99
162
|
if (skip) return 'Hooks: skipped (--no-hooks)';
|
|
100
163
|
|
|
101
164
|
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
102
|
-
|
|
103
|
-
if (existsSync(settingsPath)) {
|
|
104
|
-
try {
|
|
105
|
-
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
106
|
-
} catch {
|
|
107
|
-
settings = {};
|
|
108
|
-
}
|
|
109
|
-
}
|
|
165
|
+
const settings: any = readJson(settingsPath) ?? {};
|
|
110
166
|
if (!settings.hooks) settings.hooks = {};
|
|
111
167
|
|
|
112
|
-
|
|
168
|
+
// 0.14.6: hooks are scripts that resolve the sidecar, the agent and the secret at run
|
|
169
|
+
// time (see hook-scripts.ts). Nothing about ports or secrets is baked into settings.json.
|
|
170
|
+
const hooksDir = join(homedir(), '.claude', 'hooks');
|
|
171
|
+
if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
|
|
172
|
+
for (const { file, source } of HOOK_SCRIPTS) {
|
|
173
|
+
writeFileSync(join(hooksDir, file), source);
|
|
174
|
+
}
|
|
175
|
+
const record: HooksRecord = {
|
|
176
|
+
version: AWM_HOOKS_VERSION,
|
|
177
|
+
agentId: ctx.agentId,
|
|
178
|
+
dbPath: ctx.envVars.AWM_DB_PATH,
|
|
179
|
+
secretPath: join(dirname(ctx.dbPath), '.awm-hook-secret').replace(/\\/g, '/'),
|
|
180
|
+
hookPort: ctx.hookPort,
|
|
181
|
+
hookPortRange: ctx.hookPortRange,
|
|
182
|
+
installedAt: new Date().toISOString(),
|
|
183
|
+
};
|
|
184
|
+
writeFileSync(join(hooksDir, 'awm-hooks.json'), JSON.stringify(record, null, 2) + '\n');
|
|
185
|
+
const script = (file: string) => `node "${join(hooksDir, file).replace(/\\/g, '/')}"`;
|
|
113
186
|
|
|
114
187
|
// Stop — remind Claude to write/recall/switch tasks
|
|
115
|
-
settings
|
|
188
|
+
setHookGroup(settings, 'Stop', {
|
|
116
189
|
matcher: '',
|
|
117
190
|
hooks: [{
|
|
118
191
|
type: 'command',
|
|
@@ -120,73 +193,50 @@ const adapter: CLIAdapter = {
|
|
|
120
193
|
timeout: 5,
|
|
121
194
|
async: true,
|
|
122
195
|
}],
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Multi-port fallback for separate memory pools
|
|
126
|
-
const altPort = ctx.hookPort === '8401' ? '8402' : '8401';
|
|
127
|
-
const hookUrlAlt = `http://127.0.0.1:${altPort}/hooks/checkpoint`;
|
|
128
|
-
const buildHookCmd = (event: string, maxTime: number) => {
|
|
129
|
-
const primary = `curl -sf -X POST ${hookUrl} -H "Content-Type: application/json" -H "Authorization: Bearer ${ctx.hookSecret}" -d "{\\"hook_event_name\\":\\"${event}\\"}" --max-time ${maxTime}`;
|
|
130
|
-
const fallback = `curl -sf -X POST ${hookUrlAlt} -H "Content-Type: application/json" -H "Authorization: Bearer ${ctx.hookSecret}" -d "{\\"hook_event_name\\":\\"${event}\\"}" --max-time ${maxTime}`;
|
|
131
|
-
return `${primary} || ${fallback}`;
|
|
132
|
-
};
|
|
196
|
+
});
|
|
133
197
|
|
|
134
198
|
// PreCompact — auto-checkpoint before context compaction
|
|
135
|
-
settings
|
|
199
|
+
setHookGroup(settings, 'PreCompact', {
|
|
136
200
|
matcher: '',
|
|
137
|
-
hooks: [{
|
|
138
|
-
|
|
139
|
-
command: buildHookCmd('PreCompact', 5),
|
|
140
|
-
timeout: 10,
|
|
141
|
-
}],
|
|
142
|
-
}];
|
|
201
|
+
hooks: [{ type: 'command', command: script('awm-checkpoint.cjs'), timeout: 10 }],
|
|
202
|
+
});
|
|
143
203
|
|
|
144
|
-
// SessionEnd — auto-checkpoint on session close
|
|
145
|
-
settings
|
|
204
|
+
// SessionEnd — auto-checkpoint on session close (+ consolidation in the sidecar)
|
|
205
|
+
setHookGroup(settings, 'SessionEnd', {
|
|
146
206
|
matcher: '',
|
|
147
|
-
hooks: [{
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
207
|
+
hooks: [{ type: 'command', command: script('awm-checkpoint.cjs'), timeout: 8 }],
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// UserPromptSubmit — PRIME: inject relevant memories, or nothing (0.14.6)
|
|
211
|
+
setHookGroup(settings, 'UserPromptSubmit', ctx.installPrime ? {
|
|
212
|
+
matcher: '',
|
|
213
|
+
hooks: [{ type: 'command', command: script('awm-prime.cjs'), timeout: 6 }],
|
|
214
|
+
} : null);
|
|
153
215
|
|
|
154
216
|
// PostToolUse — DB-mutation reminder: a production data change is not
|
|
155
|
-
// complete until it is memory_written.
|
|
156
|
-
// hook entry (preserve any unrelated PostToolUse groups the user has).
|
|
157
|
-
const hooksDir = join(homedir(), '.claude', 'hooks');
|
|
158
|
-
if (!existsSync(hooksDir)) {
|
|
159
|
-
mkdirSync(hooksDir, { recursive: true });
|
|
160
|
-
}
|
|
217
|
+
// complete until it is memory_written.
|
|
161
218
|
const mutationHookPath = join(hooksDir, 'awm-db-mutation-reminder.cjs');
|
|
162
219
|
writeFileSync(mutationHookPath, DB_MUTATION_HOOK_SCRIPT);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
);
|
|
167
|
-
settings.hooks.PostToolUse = [
|
|
168
|
-
...otherPostToolUse,
|
|
169
|
-
{
|
|
170
|
-
matcher: 'Bash|PowerShell',
|
|
171
|
-
hooks: [{
|
|
172
|
-
type: 'command',
|
|
173
|
-
command: mutationHookCmd,
|
|
174
|
-
timeout: 10,
|
|
175
|
-
}],
|
|
176
|
-
},
|
|
177
|
-
];
|
|
220
|
+
setHookGroup(settings, 'PostToolUse', {
|
|
221
|
+
matcher: 'Bash|PowerShell',
|
|
222
|
+
hooks: [{ type: 'command', command: script('awm-db-mutation-reminder.cjs'), timeout: 10 }],
|
|
223
|
+
});
|
|
178
224
|
|
|
179
225
|
const settingsDir = dirname(settingsPath);
|
|
180
|
-
if (!existsSync(settingsDir)) {
|
|
181
|
-
mkdirSync(settingsDir, { recursive: true });
|
|
182
|
-
}
|
|
226
|
+
if (!existsSync(settingsDir)) mkdirSync(settingsDir, { recursive: true });
|
|
183
227
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
184
228
|
|
|
185
|
-
|
|
229
|
+
const lastPort = Number(ctx.hookPort) + Number(ctx.hookPortRange) - 1;
|
|
230
|
+
const primeNote = ctx.installPrime
|
|
231
|
+
? `UserPromptSubmit prime (off switch: create ${join(hooksDir, PRIME_DISABLE_FILE).replace(/\\/g, '/')})`
|
|
232
|
+
: 'prime skipped (--no-prime)';
|
|
233
|
+
return `Hooks: Stop + PreCompact/SessionEnd checkpoint + ${primeNote} + PostToolUse DB-mutation reminder\n` +
|
|
234
|
+
` scripts in ${hooksDir.replace(/\\/g, '/')} find this session's sidecar on ports ${ctx.hookPort}–${lastPort} at run time`;
|
|
186
235
|
},
|
|
187
236
|
|
|
188
|
-
diagnose(ctx: SetupContext): DiagnosticResult[] {
|
|
237
|
+
async diagnose(ctx: SetupContext): Promise<DiagnosticResult[]> {
|
|
189
238
|
const results: DiagnosticResult[] = [];
|
|
239
|
+
const rerun = `Re-run: awm setup claude-code${ctx.isGlobal ? ' --global' : ''}`;
|
|
190
240
|
|
|
191
241
|
// Check MCP config — Claude Code reads from both locations, check all
|
|
192
242
|
const globalPath = join(homedir(), '.mcp.json');
|
|
@@ -194,36 +244,36 @@ const adapter: CLIAdapter = {
|
|
|
194
244
|
const candidates = [projectPath, globalPath];
|
|
195
245
|
|
|
196
246
|
let foundConfig = false;
|
|
247
|
+
let configuredEnv: Record<string, string> = {};
|
|
197
248
|
for (const mcpJsonPath of candidates) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
249
|
+
const entry = readJson(mcpJsonPath)?.mcpServers?.[SERVER_NAME];
|
|
250
|
+
if (!entry) continue;
|
|
251
|
+
configuredEnv = entry.env ?? {};
|
|
252
|
+
const agentNote = configuredEnv.AWM_AGENT_ID
|
|
253
|
+
? ` (agent ${configuredEnv.AWM_AGENT_ID})`
|
|
254
|
+
: ' (no AWM_AGENT_ID — the server derives it from the directory)';
|
|
255
|
+
results.push({ check: 'MCP config', status: 'ok', message: `AWM registered in ${mcpJsonPath}${agentNote}` });
|
|
256
|
+
const missing = ['AWM_RERANK2', 'AWM_RERANK_WINDOW', 'AWM_RERANK_TAGS'].filter(k => !(k in configuredEnv));
|
|
257
|
+
if (missing.length) {
|
|
258
|
+
results.push({
|
|
259
|
+
check: 'Recommended env',
|
|
260
|
+
status: 'warn',
|
|
261
|
+
message: `not set: ${missing.join(', ')} (the measured-good retrieval configuration)`,
|
|
262
|
+
fix: `${rerun} — adds them; every other env value you have is kept`,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
foundConfig = true;
|
|
266
|
+
break;
|
|
207
267
|
}
|
|
208
268
|
if (!foundConfig) {
|
|
209
|
-
results.push({
|
|
210
|
-
check: 'MCP config',
|
|
211
|
-
status: 'fail',
|
|
212
|
-
message: 'No AWM MCP config found',
|
|
213
|
-
fix: `Run: awm setup claude-code${ctx.isGlobal ? ' --global' : ''}`,
|
|
214
|
-
});
|
|
269
|
+
results.push({ check: 'MCP config', status: 'fail', message: 'No AWM MCP config found', fix: `Run: awm setup claude-code${ctx.isGlobal ? ' --global' : ''}` });
|
|
215
270
|
}
|
|
216
271
|
|
|
217
272
|
// Check MCP entrypoint
|
|
218
273
|
if (ctx.hasDist) {
|
|
219
274
|
results.push({ check: 'MCP entrypoint', status: 'ok', message: `dist/mcp.js exists` });
|
|
220
275
|
} else {
|
|
221
|
-
results.push({
|
|
222
|
-
check: 'MCP entrypoint',
|
|
223
|
-
status: 'warn',
|
|
224
|
-
message: 'dist/mcp.js not found — using dev mode (npx tsx)',
|
|
225
|
-
fix: 'Run: npm run build',
|
|
226
|
-
});
|
|
276
|
+
results.push({ check: 'MCP entrypoint', status: 'warn', message: 'dist/mcp.js not found — using dev mode (npx tsx)', fix: 'Run: npm run build' });
|
|
227
277
|
}
|
|
228
278
|
|
|
229
279
|
// Check database
|
|
@@ -240,38 +290,86 @@ const adapter: CLIAdapter = {
|
|
|
240
290
|
if (existsSync(claudeMdPath)) {
|
|
241
291
|
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
242
292
|
if (content.includes('## Memory (AWM)')) {
|
|
243
|
-
|
|
244
|
-
} else {
|
|
293
|
+
const marked = content.includes('AWM:GENERATED:BEGIN');
|
|
245
294
|
results.push({
|
|
246
295
|
check: 'Instructions',
|
|
247
|
-
status: '
|
|
248
|
-
message: `CLAUDE.md
|
|
249
|
-
fix: `Run: awm setup claude-code${ctx.isGlobal ? ' --global' : ''}`,
|
|
296
|
+
status: 'ok',
|
|
297
|
+
message: `CLAUDE.md has AWM section${marked ? '' : ' (hand-maintained, predates generated markers — awm setup leaves it alone)'}`,
|
|
250
298
|
});
|
|
299
|
+
} else {
|
|
300
|
+
results.push({ check: 'Instructions', status: 'warn', message: `CLAUDE.md exists but missing AWM section`, fix: rerun });
|
|
251
301
|
}
|
|
252
302
|
} else {
|
|
253
303
|
results.push({ check: 'Instructions', status: 'warn', message: `CLAUDE.md not found` });
|
|
254
304
|
}
|
|
255
305
|
|
|
256
|
-
// Check hooks
|
|
306
|
+
// Check hooks — scripts present and current, settings pointing at them, no legacy curl
|
|
307
|
+
const hooksDir = join(homedir(), '.claude', 'hooks');
|
|
257
308
|
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
309
|
+
const settings = readJson(settingsPath);
|
|
310
|
+
if (!settings) {
|
|
311
|
+
results.push({
|
|
312
|
+
check: 'Hooks',
|
|
313
|
+
status: 'warn',
|
|
314
|
+
message: existsSync(settingsPath) ? 'settings.json is not valid JSON' : 'No hooks configured (auto-checkpoint and prime disabled)',
|
|
315
|
+
fix: rerun,
|
|
316
|
+
});
|
|
317
|
+
} else {
|
|
318
|
+
const hooksText = JSON.stringify(settings.hooks ?? {});
|
|
319
|
+
const has = (ev: string, needle: string) => JSON.stringify(settings.hooks?.[ev] ?? []).includes(needle);
|
|
320
|
+
const legacyCurl = /curl[^"]*\/hooks\/checkpoint/.test(hooksText);
|
|
321
|
+
const record = readJson(join(hooksDir, 'awm-hooks.json')) as HooksRecord | null;
|
|
322
|
+
const scriptsPresent = HOOK_SCRIPTS.every(h => existsSync(join(hooksDir, h.file)));
|
|
323
|
+
const checkpointWired = has('PreCompact', 'awm-checkpoint.cjs') && has('SessionEnd', 'awm-checkpoint.cjs');
|
|
324
|
+
const primeWired = has('UserPromptSubmit', 'awm-prime.cjs');
|
|
325
|
+
const primeDisabled = existsSync(join(hooksDir, PRIME_DISABLE_FILE));
|
|
326
|
+
const mutationWired = has('PostToolUse', 'awm-db-mutation-reminder');
|
|
327
|
+
|
|
328
|
+
if (legacyCurl || !scriptsPresent || !checkpointWired) {
|
|
329
|
+
results.push({
|
|
330
|
+
check: 'Hooks',
|
|
331
|
+
status: 'warn',
|
|
332
|
+
message: legacyCurl
|
|
333
|
+
? 'checkpoint hooks are the pre-0.14.6 inline curl (fixed port 8401/8402, secret baked into settings.json)'
|
|
334
|
+
: !scriptsPresent ? 'hook scripts missing from ~/.claude/hooks' : 'checkpoint hooks not wired to awm-checkpoint.cjs',
|
|
335
|
+
fix: rerun,
|
|
336
|
+
});
|
|
337
|
+
} else if (record && record.version !== AWM_HOOKS_VERSION) {
|
|
338
|
+
results.push({ check: 'Hooks', status: 'warn', message: `hook scripts v${record.version}; current is v${AWM_HOOKS_VERSION}`, fix: rerun });
|
|
339
|
+
} else {
|
|
340
|
+
results.push({ check: 'Hooks', status: 'ok', message: `PreCompact + SessionEnd checkpoint via awm-checkpoint.cjs (hooks v${record?.version ?? AWM_HOOKS_VERSION})` });
|
|
272
341
|
}
|
|
342
|
+
if (!primeWired) {
|
|
343
|
+
results.push({ check: 'Prime hook', status: 'warn', message: 'not installed — memory reaches the agent only when it chooses to call memory_recall', fix: `${rerun} (without --no-prime)` });
|
|
344
|
+
} else if (primeDisabled) {
|
|
345
|
+
results.push({ check: 'Prime hook', status: 'warn', message: `installed but OFF (${PRIME_DISABLE_FILE} present)`, fix: `Delete ${join(hooksDir, PRIME_DISABLE_FILE)} to turn it on` });
|
|
346
|
+
} else {
|
|
347
|
+
results.push({ check: 'Prime hook', status: 'ok', message: 'UserPromptSubmit → awm-prime.cjs (memories injected per prompt when confident)' });
|
|
348
|
+
}
|
|
349
|
+
if (!mutationWired) {
|
|
350
|
+
results.push({ check: 'DB-mutation reminder', status: 'warn', message: 'PostToolUse reminder missing', fix: rerun });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Live sidecars — the thing the hooks actually talk to
|
|
355
|
+
const basePort = parseInt(configuredEnv.AWM_HOOK_PORT ?? ctx.hookPort, 10) || 8401;
|
|
356
|
+
const range = Math.max(1, parseInt(configuredEnv.AWM_HOOK_PORT_RANGE ?? ctx.hookPortRange, 10) || 10);
|
|
357
|
+
const probes = await Promise.all(Array.from({ length: range }, (_, i) => probeSidecar(basePort + i)));
|
|
358
|
+
const live = probes.filter((s): s is LiveSidecar => s !== null);
|
|
359
|
+
if (live.length === 0) {
|
|
360
|
+
results.push({ check: 'Sidecars', status: 'warn', message: `none listening on ${basePort}–${basePort + range - 1} (normal when no Claude Code session is open)` });
|
|
273
361
|
} else {
|
|
274
|
-
|
|
362
|
+
const summary = live.map(s => `${s.port}=${s.agentId ?? '?'}@${s.version}${s.pid ? ` pid ${s.pid}` : ''}`).join(', ');
|
|
363
|
+
results.push({ check: 'Sidecars', status: 'ok', message: `${live.length} live: ${summary}` });
|
|
364
|
+
const agents = [...new Set(live.map(s => s.agentId))];
|
|
365
|
+
const configuredAgent = configuredEnv.AWM_AGENT_ID ?? ctx.agentId;
|
|
366
|
+
if (!agents.includes(configuredAgent)) {
|
|
367
|
+
results.push({
|
|
368
|
+
check: 'Sidecar match',
|
|
369
|
+
status: 'warn',
|
|
370
|
+
message: `no live sidecar serves agent "${configuredAgent}" — hooks from this directory fail open (live agents: ${agents.join(', ')})`,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
275
373
|
}
|
|
276
374
|
|
|
277
375
|
return results;
|