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
|
@@ -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 0.15.4.
|
|
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@0.15.4', '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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// awm-checkpoint.cjs — installed by `awm setup` (hooks v0.14.6). PreCompact / SessionEnd: checkpoint this session into ITS agent's AWM.
|
|
3
|
+
// Re-running `awm setup` rewrites this file; put local changes elsewhere.
|
|
4
|
+
// Fails open: on any problem it prints nothing and exits 0.
|
|
5
|
+
|
|
6
|
+
// Forwards the whole hook payload (hook_event_name, transcript_path, cwd,
|
|
7
|
+
// session_id) to POST /hooks/checkpoint on the sidecar that belongs to this
|
|
8
|
+
// session's agent. transcript_path matters: the sidecar parses it for the
|
|
9
|
+
// current task and active files, so the checkpoint is a real one, not a stub.
|
|
10
|
+
'use strict';
|
|
11
|
+
const http = require('http');
|
|
12
|
+
|
|
13
|
+
let finder = null;
|
|
14
|
+
try { finder = require('./awm-find-sidecar.cjs'); } catch (e) { process.exit(0); }
|
|
15
|
+
|
|
16
|
+
const TIMEOUT_MS = 5000;
|
|
17
|
+
|
|
18
|
+
let raw = '';
|
|
19
|
+
process.stdin.on('data', function (d) { raw += d; });
|
|
20
|
+
process.stdin.on('end', function () {
|
|
21
|
+
let payload = {};
|
|
22
|
+
try { payload = JSON.parse(raw || '{}'); } catch (e) { payload = {}; }
|
|
23
|
+
if (!payload.hook_event_name) payload.hook_event_name = 'PreCompact';
|
|
24
|
+
main(payload).catch(function () { process.exit(0); });
|
|
25
|
+
});
|
|
26
|
+
// A hook with nothing on stdin must still exit.
|
|
27
|
+
process.stdin.on('error', function () { process.exit(0); });
|
|
28
|
+
|
|
29
|
+
async function main(payload) {
|
|
30
|
+
const found = await finder.findSidecar(payload.cwd);
|
|
31
|
+
const sc = found.sidecar;
|
|
32
|
+
if (!sc) {
|
|
33
|
+
finder.debugLog('checkpoint', { event: payload.hook_event_name, agent: found.config.agentId, sidecar: null, live: found.all.length });
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
const body = JSON.stringify(payload);
|
|
37
|
+
const req = http.request({
|
|
38
|
+
host: '127.0.0.1', port: sc.port, path: '/hooks/checkpoint', method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
'Content-Length': Buffer.byteLength(body),
|
|
42
|
+
'Authorization': 'Bearer ' + found.config.secret,
|
|
43
|
+
},
|
|
44
|
+
timeout: TIMEOUT_MS,
|
|
45
|
+
}, function (res) {
|
|
46
|
+
res.resume();
|
|
47
|
+
res.on('end', function () {
|
|
48
|
+
finder.debugLog('checkpoint', { event: payload.hook_event_name, agent: found.config.agentId, port: sc.port, status: res.statusCode });
|
|
49
|
+
process.exit(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
req.on('error', function () { process.exit(0); });
|
|
53
|
+
req.on('timeout', function () { req.destroy(); process.exit(0); });
|
|
54
|
+
req.end(body);
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// AWM DB-mutation reminder hook (PostToolUse on Bash|PowerShell). Installed by awm setup.
|
|
3
|
+
let raw = '';
|
|
4
|
+
process.stdin.on('data', (d) => (raw += d));
|
|
5
|
+
process.stdin.on('end', () => {
|
|
6
|
+
try {
|
|
7
|
+
const j = JSON.parse(raw || '{}');
|
|
8
|
+
const cmd = (j.tool_input && j.tool_input.command) || '';
|
|
9
|
+
const isDbClient = /\b(sqlcmd|mysql|psql)\b/i.test(cmd) || /(sqlcmd|mysql|psql)\.exe/i.test(cmd);
|
|
10
|
+
const isMutation = /\b(INSERT|UPDATE|DELETE|ALTER|TRUNCATE|DROP|MERGE)\b/i.test(cmd);
|
|
11
|
+
if (isDbClient && isMutation) {
|
|
12
|
+
console.log(JSON.stringify({
|
|
13
|
+
hookSpecificOutput: {
|
|
14
|
+
hookEventName: 'PostToolUse',
|
|
15
|
+
additionalContext:
|
|
16
|
+
'AWM REMINDER: this command contains a production DB mutation (INSERT/UPDATE/DELETE/ALTER/DROP/TRUNCATE/MERGE). ' +
|
|
17
|
+
'The change is NOT complete until it is recorded: call memory_write NOW with the table, what changed, row counts, date, and why ' +
|
|
18
|
+
'(memory_class canonical if other agents must recall it), and confirm a rollback/backup exists. ' +
|
|
19
|
+
'If the keywords were only in a string/comment or on temp tables (#...), no write is needed.',
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
} catch (e) { /* never block the tool result */ }
|
|
24
|
+
process.exit(0);
|
|
25
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// awm-find-sidecar.cjs — installed by `awm setup` (hooks v0.14.6). Shared resolver: which AWM sidecar belongs to THIS session.
|
|
3
|
+
// Re-running `awm setup` rewrites this file; put local changes elsewhere.
|
|
4
|
+
// Fails open: on any problem it prints nothing and exits 0.
|
|
5
|
+
|
|
6
|
+
// Since AWM 0.14.2 each MCP process binds the first free port in
|
|
7
|
+
// [AWM_HOOK_PORT, +AWM_HOOK_PORT_RANGE), so "the sidecar is on 8401" is not a
|
|
8
|
+
// safe assumption: with N sessions open there are N sidecars and the one on
|
|
9
|
+
// 8401 may serve a different agent pool. This module (1) works out which agent
|
|
10
|
+
// a session started in `cwd` belongs to, from the MCP config that governs that
|
|
11
|
+
// directory, (2) probes the whole range in parallel (local, ~10 ms), and
|
|
12
|
+
// (3) returns the responder whose agentId matches — newest version first, then
|
|
13
|
+
// lowest port. No match → null, and callers fail open.
|
|
14
|
+
'use strict';
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const http = require('http');
|
|
19
|
+
|
|
20
|
+
const SERVER_NAME = 'agent-working-memory';
|
|
21
|
+
const PROBE_TIMEOUT_MS = 400;
|
|
22
|
+
|
|
23
|
+
function norm(p) { return String(p || '').replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase(); }
|
|
24
|
+
function readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) { return null; } }
|
|
25
|
+
function readText(p) { try { return fs.readFileSync(p, 'utf8').trim(); } catch (e) { return ''; } }
|
|
26
|
+
|
|
27
|
+
// MIRROR of deriveAgentFromDir() in src/core/agent-id.ts — the server's own
|
|
28
|
+
// fallback when AWM_AGENT_ID is unset. Kept in sync by tests/adapters/hook-scripts.test.ts.
|
|
29
|
+
function deriveAgentFromDir(dir) {
|
|
30
|
+
const d = String(dir || '').replace(/\\/g, '/');
|
|
31
|
+
if (/\/AgentSynapse\//i.test(d)) return 'work';
|
|
32
|
+
return /\/Personal-Projects(\/|$)/i.test(d) ? 'personal' : 'work';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The AWM entry inside one MCP config file, if any. ~/.claude.json keeps
|
|
36
|
+
// project-scoped servers under projects[<path>].mcpServers.
|
|
37
|
+
function awmEntry(json, cwd) {
|
|
38
|
+
if (!json) return null;
|
|
39
|
+
if (json.mcpServers && json.mcpServers[SERVER_NAME]) return json.mcpServers[SERVER_NAME];
|
|
40
|
+
if (json.projects && cwd) {
|
|
41
|
+
for (const k of Object.keys(json.projects)) {
|
|
42
|
+
if (norm(k) !== norm(cwd)) continue;
|
|
43
|
+
const s = json.projects[k] && json.projects[k].mcpServers;
|
|
44
|
+
if (s && s[SERVER_NAME]) return s[SERVER_NAME];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the configuration that governs a session started in `cwd`:
|
|
52
|
+
* { agentId, secret, port, range, dbPath, source }.
|
|
53
|
+
*
|
|
54
|
+
* Order: the nearest .mcp.json / .claude/mcp.json walking up from cwd, then
|
|
55
|
+
* ~/.mcp.json, then ~/.claude.json, then the record `awm setup` left in
|
|
56
|
+
* ~/.claude/hooks/awm-hooks.json. Process env always wins for a given key.
|
|
57
|
+
*/
|
|
58
|
+
function resolveConfig(cwd) {
|
|
59
|
+
cwd = cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
60
|
+
const home = os.homedir();
|
|
61
|
+
const candidates = [];
|
|
62
|
+
let dir = path.resolve(cwd);
|
|
63
|
+
for (let i = 0; i < 16; i++) {
|
|
64
|
+
candidates.push(path.join(dir, '.mcp.json'), path.join(dir, '.claude', 'mcp.json'));
|
|
65
|
+
const parent = path.dirname(dir);
|
|
66
|
+
if (parent === dir) break;
|
|
67
|
+
dir = parent;
|
|
68
|
+
}
|
|
69
|
+
candidates.push(path.join(home, '.mcp.json'), path.join(home, '.claude.json'));
|
|
70
|
+
|
|
71
|
+
let entry = null, source = null;
|
|
72
|
+
for (const c of candidates) {
|
|
73
|
+
const e = awmEntry(readJson(c), cwd);
|
|
74
|
+
if (e) { entry = e; source = c; break; }
|
|
75
|
+
}
|
|
76
|
+
const env = (entry && entry.env) || {};
|
|
77
|
+
const fallback = readJson(path.join(home, '.claude', 'hooks', 'awm-hooks.json')) || {};
|
|
78
|
+
|
|
79
|
+
const agentId = process.env.AWM_AGENT_ID || env.AWM_AGENT_ID
|
|
80
|
+
|| (entry ? deriveAgentFromDir(cwd) : (fallback.agentId || deriveAgentFromDir(cwd)));
|
|
81
|
+
const dbPath = env.AWM_DB_PATH || fallback.dbPath || null;
|
|
82
|
+
let secret = process.env.AWM_HOOK_SECRET || env.AWM_HOOK_SECRET || '';
|
|
83
|
+
if (!secret && dbPath) secret = readText(path.join(path.dirname(dbPath), '.awm-hook-secret'));
|
|
84
|
+
if (!secret && fallback.secretPath) secret = readText(fallback.secretPath);
|
|
85
|
+
const port = parseInt(process.env.AWM_HOOK_PORT || env.AWM_HOOK_PORT || fallback.hookPort || '8401', 10) || 8401;
|
|
86
|
+
const range = Math.max(1, parseInt(process.env.AWM_HOOK_PORT_RANGE || env.AWM_HOOK_PORT_RANGE || fallback.hookPortRange || '10', 10) || 10);
|
|
87
|
+
return { agentId: agentId, secret: secret, port: port, range: range, dbPath: dbPath, source: source };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function probe(port) {
|
|
91
|
+
return new Promise(function (resolve) {
|
|
92
|
+
const req = http.get({ host: '127.0.0.1', port: port, path: '/health', timeout: PROBE_TIMEOUT_MS }, function (res) {
|
|
93
|
+
let data = '';
|
|
94
|
+
res.on('data', function (c) { data += c; });
|
|
95
|
+
res.on('end', function () {
|
|
96
|
+
try {
|
|
97
|
+
const j = JSON.parse(data);
|
|
98
|
+
resolve(j && j.sidecar ? { port: port, agentId: j.agentId || null, version: j.version || '0.0.0', pid: j.pid || null } : null);
|
|
99
|
+
} catch (e) { resolve(null); }
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
req.on('error', function () { resolve(null); });
|
|
103
|
+
req.on('timeout', function () { req.destroy(); resolve(null); });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function versionKey(v) { return String(v || '0').split('.').map(function (n) { return parseInt(n, 10) || 0; }); }
|
|
108
|
+
function newer(a, b) {
|
|
109
|
+
const ka = versionKey(a), kb = versionKey(b);
|
|
110
|
+
for (let i = 0; i < Math.max(ka.length, kb.length); i++) {
|
|
111
|
+
const d = (ka[i] || 0) - (kb[i] || 0);
|
|
112
|
+
if (d) return d > 0;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Every live sidecar in the configured range, regardless of agent. */
|
|
118
|
+
async function listSidecars(cfg) {
|
|
119
|
+
const ports = [];
|
|
120
|
+
for (let i = 0; i < cfg.range; i++) ports.push(cfg.port + i);
|
|
121
|
+
return (await Promise.all(ports.map(probe))).filter(Boolean);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The sidecar for this session, or null. Resolves to
|
|
126
|
+
* { sidecar: {port, agentId, version, pid} | null, config, all: [...] }.
|
|
127
|
+
*/
|
|
128
|
+
async function findSidecar(cwd) {
|
|
129
|
+
const cfg = resolveConfig(cwd);
|
|
130
|
+
const all = await listSidecars(cfg);
|
|
131
|
+
const mine = all.filter(function (s) { return s.agentId === cfg.agentId; });
|
|
132
|
+
mine.sort(function (a, b) { return newer(a.version, b.version) ? -1 : newer(b.version, a.version) ? 1 : a.port - b.port; });
|
|
133
|
+
return { sidecar: mine[0] || null, config: cfg, all: all };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Optional trace for troubleshooting: AWM_HOOK_DEBUG=1 appends to ~/.claude/hooks/awm-hooks.log. */
|
|
137
|
+
function debugLog(hook, fields) {
|
|
138
|
+
if (!process.env.AWM_HOOK_DEBUG) return;
|
|
139
|
+
try {
|
|
140
|
+
const line = Object.assign({ ts: new Date().toISOString(), hook: hook }, fields);
|
|
141
|
+
fs.appendFileSync(path.join(os.homedir(), '.claude', 'hooks', 'awm-hooks.log'), JSON.stringify(line) + '\n');
|
|
142
|
+
} catch (e) { /* never matters */ }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { resolveConfig, findSidecar, listSidecars, deriveAgentFromDir, debugLog, PROBE_TIMEOUT_MS };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// awm-prime.cjs — installed by `awm setup` (hooks v0.14.6). UserPromptSubmit: prime the turn with relevant memories, or stay silent.
|
|
3
|
+
// Re-running `awm setup` rewrites this file; put local changes elsewhere.
|
|
4
|
+
// Fails open: on any problem it prints nothing and exits 0.
|
|
5
|
+
|
|
6
|
+
// The PRIME phase of the AWM-native harness (docs/patterns/awm-native-harness.md).
|
|
7
|
+
// Posts the prompt to POST /hooks/prime on this session's sidecar and injects the
|
|
8
|
+
// returned text as additionalContext. The sidecar decides what to inject, formats
|
|
9
|
+
// it, abstains when confidence is low and caps the token budget — see
|
|
10
|
+
// src/hooks/prime.ts — so this file stays thin.
|
|
11
|
+
//
|
|
12
|
+
// Turn it off without uninstalling: create ~/.claude/hooks/awm-prime.disabled
|
|
13
|
+
// (an empty file). Remove the file to turn it back on. `awm doctor` reports which.
|
|
14
|
+
'use strict';
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const http = require('http');
|
|
19
|
+
|
|
20
|
+
let finder = null;
|
|
21
|
+
try { finder = require('./awm-find-sidecar.cjs'); } catch (e) { process.exit(0); }
|
|
22
|
+
|
|
23
|
+
// A slow AWM must never delay a prompt. Warm recall with rerank measures 1.4–1.8 s
|
|
24
|
+
// on a 30k-memory store; 4 s leaves margin. Connection refused returns in ~100 ms.
|
|
25
|
+
const TIMEOUT_MS = 4000;
|
|
26
|
+
const MIN_PROMPT_LEN = 15; // skip "yes" / "ok" / "go on" turns
|
|
27
|
+
// Set-level confidence below which the sidecar injects nothing. 0.10 rather than
|
|
28
|
+
// the 0.25 once documented as "balanced": measured 2026-09-11, 0.25 silenced
|
|
29
|
+
// specific prompts with one clear winner (top score 0.26–0.38) while passing a
|
|
30
|
+
// vague one at 0.92. The gate is on the shape of the score distribution, not on
|
|
31
|
+
// relevance, so specific questions are exactly the ones a high threshold loses.
|
|
32
|
+
const MIN_CONFIDENCE = 0.10;
|
|
33
|
+
const DISABLE_FILE = path.join(os.homedir(), '.claude', 'hooks', 'awm-prime.disabled');
|
|
34
|
+
|
|
35
|
+
let raw = '';
|
|
36
|
+
process.stdin.on('data', function (d) { raw += d; });
|
|
37
|
+
process.stdin.on('end', function () {
|
|
38
|
+
let payload = {};
|
|
39
|
+
try { payload = JSON.parse(raw || '{}'); } catch (e) { process.exit(0); }
|
|
40
|
+
main(payload).catch(function () { process.exit(0); });
|
|
41
|
+
});
|
|
42
|
+
process.stdin.on('error', function () { process.exit(0); });
|
|
43
|
+
|
|
44
|
+
function extractPrompt(j) {
|
|
45
|
+
for (const key of ['prompt', 'message', 'user_prompt', 'text', 'input']) {
|
|
46
|
+
if (typeof j[key] === 'string' && j[key].trim()) return j[key];
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function main(payload) {
|
|
52
|
+
if (fs.existsSync(DISABLE_FILE)) process.exit(0);
|
|
53
|
+
const prompt = extractPrompt(payload);
|
|
54
|
+
if (!prompt || prompt.trim().length < MIN_PROMPT_LEN) process.exit(0);
|
|
55
|
+
|
|
56
|
+
const found = await finder.findSidecar(payload.cwd);
|
|
57
|
+
const sc = found.sidecar;
|
|
58
|
+
if (!sc) {
|
|
59
|
+
finder.debugLog('prime', { agent: found.config.agentId, sidecar: null, live: found.all.length });
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const body = JSON.stringify({
|
|
64
|
+
hook_event_name: 'UserPromptSubmit',
|
|
65
|
+
session_id: payload.session_id,
|
|
66
|
+
cwd: payload.cwd,
|
|
67
|
+
prompt: prompt,
|
|
68
|
+
minConfidence: MIN_CONFIDENCE,
|
|
69
|
+
});
|
|
70
|
+
const req = http.request({
|
|
71
|
+
host: '127.0.0.1', port: sc.port, path: '/hooks/prime', method: 'POST',
|
|
72
|
+
headers: {
|
|
73
|
+
'Content-Type': 'application/json',
|
|
74
|
+
'Content-Length': Buffer.byteLength(body),
|
|
75
|
+
'Authorization': 'Bearer ' + found.config.secret,
|
|
76
|
+
},
|
|
77
|
+
timeout: TIMEOUT_MS,
|
|
78
|
+
}, function (res) {
|
|
79
|
+
let data = '';
|
|
80
|
+
res.on('data', function (c) { data += c; });
|
|
81
|
+
res.on('end', function () {
|
|
82
|
+
let out = null;
|
|
83
|
+
try { out = JSON.parse(data); } catch (e) { out = null; }
|
|
84
|
+
const inject = out && typeof out.inject === 'string' ? out.inject : '';
|
|
85
|
+
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 });
|
|
86
|
+
if (res.statusCode === 200 && inject) {
|
|
87
|
+
process.stdout.write(JSON.stringify({
|
|
88
|
+
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: inject },
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
process.exit(0);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
req.on('error', function () { process.exit(0); });
|
|
95
|
+
req.on('timeout', function () { req.destroy(); process.exit(0); });
|
|
96
|
+
req.end(body);
|
|
97
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"Stop": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "echo \"MEMORY: (1) Did you learn anything new? Call memory_write. (2) Are you about to work on a topic you might have prior knowledge about? Call memory_recall. (3) Switching tasks? Call memory_task_begin.\"",
|
|
10
|
+
"timeout": 5,
|
|
11
|
+
"async": true
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"PreCompact": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/awm-checkpoint.cjs\"",
|
|
23
|
+
"timeout": 10
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"SessionEnd": [
|
|
29
|
+
{
|
|
30
|
+
"matcher": "",
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/awm-checkpoint.cjs\"",
|
|
35
|
+
"timeout": 8
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"UserPromptSubmit": [
|
|
41
|
+
{
|
|
42
|
+
"matcher": "",
|
|
43
|
+
"hooks": [
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/awm-prime.cjs\"",
|
|
47
|
+
"timeout": 6
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"PostToolUse": [
|
|
53
|
+
{
|
|
54
|
+
"matcher": "Bash",
|
|
55
|
+
"hooks": [
|
|
56
|
+
{
|
|
57
|
+
"type": "command",
|
|
58
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/awm-db-mutation-reminder.cjs\"",
|
|
59
|
+
"timeout": 5,
|
|
60
|
+
"async": true
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|