agent-working-memory 0.6.0 → 0.6.1
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 +15 -9
- package/dist/adapters/claude-code.d.ts +4 -0
- package/dist/adapters/claude-code.d.ts.map +1 -0
- package/dist/adapters/claude-code.js +218 -0
- package/dist/adapters/claude-code.js.map +1 -0
- package/dist/adapters/codex.d.ts +4 -0
- package/dist/adapters/codex.d.ts.map +1 -0
- package/dist/adapters/codex.js +226 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/common.d.ts +34 -0
- package/dist/adapters/common.d.ts.map +1 -0
- package/dist/adapters/common.js +145 -0
- package/dist/adapters/common.js.map +1 -0
- package/dist/adapters/cursor.d.ts +4 -0
- package/dist/adapters/cursor.d.ts.map +1 -0
- package/dist/adapters/cursor.js +138 -0
- package/dist/adapters/cursor.js.map +1 -0
- package/dist/adapters/http.d.ts +4 -0
- package/dist/adapters/http.d.ts.map +1 -0
- package/dist/adapters/http.js +88 -0
- package/dist/adapters/http.js.map +1 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +65 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +4 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cli.js +104 -230
- package/dist/cli.js.map +1 -1
- package/dist/coordination/events.d.ts +59 -0
- package/dist/coordination/events.d.ts.map +1 -0
- package/dist/coordination/events.js +28 -0
- package/dist/coordination/events.js.map +1 -0
- package/dist/coordination/index.d.ts +10 -1
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +87 -3
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/peer-decisions.d.ts +40 -0
- package/dist/coordination/peer-decisions.d.ts.map +1 -0
- package/dist/coordination/peer-decisions.js +82 -0
- package/dist/coordination/peer-decisions.js.map +1 -0
- package/dist/coordination/plugin-loader.d.ts +18 -0
- package/dist/coordination/plugin-loader.d.ts.map +1 -0
- package/dist/coordination/plugin-loader.js +55 -0
- package/dist/coordination/plugin-loader.js.map +1 -0
- package/dist/coordination/plugin.d.ts +40 -0
- package/dist/coordination/plugin.d.ts.map +1 -0
- package/dist/coordination/plugin.js +22 -0
- package/dist/coordination/plugin.js.map +1 -0
- package/dist/coordination/routes.d.ts +2 -1
- package/dist/coordination/routes.d.ts.map +1 -1
- package/dist/coordination/routes.js +899 -76
- package/dist/coordination/routes.js.map +1 -1
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +72 -14
- package/dist/coordination/schema.js.map +1 -1
- package/dist/coordination/schemas.d.ts +84 -3
- package/dist/coordination/schemas.d.ts.map +1 -1
- package/dist/coordination/schemas.js +71 -1
- package/dist/coordination/schemas.js.map +1 -1
- package/dist/coordination/stale.d.ts.map +1 -1
- package/dist/coordination/stale.js +2 -1
- package/dist/coordination/stale.js.map +1 -1
- package/dist/coordination/types.d.ts +252 -0
- package/dist/coordination/types.d.ts.map +1 -0
- package/dist/coordination/types.js +8 -0
- package/dist/coordination/types.js.map +1 -0
- package/dist/coordination/write-mutex.d.ts +26 -0
- package/dist/coordination/write-mutex.d.ts.map +1 -0
- package/dist/coordination/write-mutex.js +63 -0
- package/dist/coordination/write-mutex.js.map +1 -0
- package/dist/core/embeddings.d.ts +2 -0
- package/dist/core/embeddings.d.ts.map +1 -1
- package/dist/core/embeddings.js +4 -0
- package/dist/core/embeddings.js.map +1 -1
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +16 -3
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/consolidation.d.ts.map +1 -1
- package/dist/engine/consolidation.js +15 -6
- package/dist/engine/consolidation.js.map +1 -1
- package/dist/engine/retraction.d.ts +3 -1
- package/dist/engine/retraction.d.ts.map +1 -1
- package/dist/engine/retraction.js +19 -6
- package/dist/engine/retraction.js.map +1 -1
- package/dist/index.js +6 -18
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +52 -3
- package/dist/mcp.js.map +1 -1
- package/dist/storage/sqlite.d.ts +6 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +39 -3
- package/dist/storage/sqlite.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/claude-code.ts +234 -0
- package/src/adapters/codex.ts +262 -0
- package/src/adapters/common.ts +172 -0
- package/src/adapters/cursor.ts +150 -0
- package/src/adapters/http.ts +100 -0
- package/src/adapters/index.ts +31 -0
- package/src/adapters/types.ts +75 -0
- package/src/cli.ts +107 -238
- package/src/coordination/events.ts +90 -0
- package/src/coordination/index.ts +102 -3
- package/src/coordination/peer-decisions.ts +105 -0
- package/src/coordination/plugin-loader.ts +60 -0
- package/src/coordination/plugin.ts +44 -0
- package/src/coordination/routes.ts +1176 -105
- package/src/coordination/schema.ts +67 -14
- package/src/coordination/schemas.ts +85 -1
- package/src/coordination/stale.ts +3 -2
- package/src/coordination/types.ts +311 -0
- package/src/coordination/write-mutex.ts +69 -0
- package/src/core/embeddings.ts +5 -0
- package/src/engine/activation.ts +13 -3
- package/src/engine/consolidation.ts +15 -6
- package/src/engine/retraction.ts +22 -6
- package/src/index.ts +6 -15
- package/src/mcp.ts +73 -9
- package/src/storage/sqlite.ts +39 -3
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code adapter — writes .mcp.json, CLAUDE.md, and hooks.
|
|
6
|
+
*
|
|
7
|
+
* This is a direct extraction of the original setup() behavior.
|
|
8
|
+
* Zero behavioral change from the monolithic version.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
12
|
+
import { join, dirname, basename } from 'node:path';
|
|
13
|
+
import type { CLIAdapter, SetupContext, DiagnosticResult } from './types.js';
|
|
14
|
+
import { resolveMcpCommand, homedir, AWM_INSTRUCTION_CONTENT } from './common.js';
|
|
15
|
+
|
|
16
|
+
const adapter: CLIAdapter = {
|
|
17
|
+
id: 'claude-code',
|
|
18
|
+
name: 'Claude Code',
|
|
19
|
+
supportsProjectScope: true,
|
|
20
|
+
supportsGlobalScope: true,
|
|
21
|
+
|
|
22
|
+
writeMcpConfig(ctx: SetupContext): string {
|
|
23
|
+
const mcpCmd = resolveMcpCommand(ctx);
|
|
24
|
+
const mcpConfig = { ...mcpCmd, env: ctx.envVars };
|
|
25
|
+
|
|
26
|
+
// Global: ~/.mcp.json (standard MCP location, all projects)
|
|
27
|
+
// Project: .claude/mcp.json (Claude Code's native project scope)
|
|
28
|
+
const mcpJsonPath = ctx.isGlobal
|
|
29
|
+
? join(homedir(), '.mcp.json')
|
|
30
|
+
: join(ctx.cwd, '.claude', 'mcp.json');
|
|
31
|
+
|
|
32
|
+
const mcpDir = dirname(mcpJsonPath);
|
|
33
|
+
if (!existsSync(mcpDir)) {
|
|
34
|
+
mkdirSync(mcpDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let existing: any = {};
|
|
38
|
+
if (existsSync(mcpJsonPath)) {
|
|
39
|
+
try {
|
|
40
|
+
existing = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
41
|
+
} catch {
|
|
42
|
+
existing = {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!existing.mcpServers) existing.mcpServers = {};
|
|
46
|
+
|
|
47
|
+
existing.mcpServers['agent-working-memory'] = mcpConfig;
|
|
48
|
+
writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
|
|
49
|
+
return `MCP config: ${mcpJsonPath}`;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
writeInstructions(ctx: SetupContext, skip: boolean): string {
|
|
53
|
+
const claudeMdPath = ctx.isGlobal
|
|
54
|
+
? join(homedir(), '.claude', 'CLAUDE.md')
|
|
55
|
+
: join(ctx.cwd, 'CLAUDE.md');
|
|
56
|
+
|
|
57
|
+
if (skip) return 'CLAUDE.md: skipped (--no-instructions)';
|
|
58
|
+
|
|
59
|
+
// Ensure parent directory exists
|
|
60
|
+
const dir = dirname(claudeMdPath);
|
|
61
|
+
if (!existsSync(dir)) {
|
|
62
|
+
mkdirSync(dir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (existsSync(claudeMdPath)) {
|
|
66
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
67
|
+
if (content.includes('## Memory (AWM)')) {
|
|
68
|
+
return 'CLAUDE.md: already has AWM section (skipped)';
|
|
69
|
+
}
|
|
70
|
+
writeFileSync(claudeMdPath, content.trimEnd() + '\n\n' + AWM_INSTRUCTION_CONTENT);
|
|
71
|
+
return 'CLAUDE.md: appended AWM workflow section';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const title = ctx.isGlobal ? '# Global Instructions' : `# ${basename(ctx.cwd)}`;
|
|
75
|
+
writeFileSync(claudeMdPath, `${title}\n\n${AWM_INSTRUCTION_CONTENT}`);
|
|
76
|
+
return 'CLAUDE.md: created with AWM workflow section';
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
writeHooks(ctx: SetupContext, skip: boolean): string {
|
|
80
|
+
if (skip) return 'Hooks: skipped (--no-hooks)';
|
|
81
|
+
|
|
82
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
83
|
+
let settings: any = {};
|
|
84
|
+
if (existsSync(settingsPath)) {
|
|
85
|
+
try {
|
|
86
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
87
|
+
} catch {
|
|
88
|
+
settings = {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!settings.hooks) settings.hooks = {};
|
|
92
|
+
|
|
93
|
+
const hookUrl = `http://127.0.0.1:${ctx.hookPort}/hooks/checkpoint`;
|
|
94
|
+
|
|
95
|
+
// Stop — remind Claude to write/recall/switch tasks
|
|
96
|
+
settings.hooks.Stop = [{
|
|
97
|
+
matcher: '',
|
|
98
|
+
hooks: [{
|
|
99
|
+
type: 'command',
|
|
100
|
+
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."',
|
|
101
|
+
timeout: 5,
|
|
102
|
+
async: true,
|
|
103
|
+
}],
|
|
104
|
+
}];
|
|
105
|
+
|
|
106
|
+
// Multi-port fallback for separate memory pools
|
|
107
|
+
const altPort = ctx.hookPort === '8401' ? '8402' : '8401';
|
|
108
|
+
const hookUrlAlt = `http://127.0.0.1:${altPort}/hooks/checkpoint`;
|
|
109
|
+
const buildHookCmd = (event: string, maxTime: number) => {
|
|
110
|
+
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}`;
|
|
111
|
+
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}`;
|
|
112
|
+
return `${primary} || ${fallback}`;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// PreCompact — auto-checkpoint before context compaction
|
|
116
|
+
settings.hooks.PreCompact = [{
|
|
117
|
+
matcher: '',
|
|
118
|
+
hooks: [{
|
|
119
|
+
type: 'command',
|
|
120
|
+
command: buildHookCmd('PreCompact', 5),
|
|
121
|
+
timeout: 10,
|
|
122
|
+
}],
|
|
123
|
+
}];
|
|
124
|
+
|
|
125
|
+
// SessionEnd — auto-checkpoint on session close
|
|
126
|
+
settings.hooks.SessionEnd = [{
|
|
127
|
+
matcher: '',
|
|
128
|
+
hooks: [{
|
|
129
|
+
type: 'command',
|
|
130
|
+
command: buildHookCmd('SessionEnd', 2),
|
|
131
|
+
timeout: 5,
|
|
132
|
+
}],
|
|
133
|
+
}];
|
|
134
|
+
|
|
135
|
+
const settingsDir = dirname(settingsPath);
|
|
136
|
+
if (!existsSync(settingsDir)) {
|
|
137
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
140
|
+
|
|
141
|
+
return `Hooks: Stop + PreCompact + SessionEnd (port ${ctx.hookPort})`;
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
diagnose(ctx: SetupContext): DiagnosticResult[] {
|
|
145
|
+
const results: DiagnosticResult[] = [];
|
|
146
|
+
|
|
147
|
+
// Check MCP config — Claude Code reads from both locations, check all
|
|
148
|
+
const globalPath = join(homedir(), '.mcp.json');
|
|
149
|
+
const projectPath = join(ctx.cwd, '.claude', 'mcp.json');
|
|
150
|
+
const candidates = [projectPath, globalPath];
|
|
151
|
+
|
|
152
|
+
let foundConfig = false;
|
|
153
|
+
for (const mcpJsonPath of candidates) {
|
|
154
|
+
if (!existsSync(mcpJsonPath)) continue;
|
|
155
|
+
try {
|
|
156
|
+
const config = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
157
|
+
if (config.mcpServers?.['agent-working-memory']) {
|
|
158
|
+
results.push({ check: 'MCP config', status: 'ok', message: `AWM registered in ${mcpJsonPath}` });
|
|
159
|
+
foundConfig = true;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
} catch { /* not valid JSON, skip */ }
|
|
163
|
+
}
|
|
164
|
+
if (!foundConfig) {
|
|
165
|
+
results.push({
|
|
166
|
+
check: 'MCP config',
|
|
167
|
+
status: 'fail',
|
|
168
|
+
message: 'No AWM MCP config found',
|
|
169
|
+
fix: `Run: awm setup claude-code${ctx.isGlobal ? ' --global' : ''}`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check MCP entrypoint
|
|
174
|
+
if (ctx.hasDist) {
|
|
175
|
+
results.push({ check: 'MCP entrypoint', status: 'ok', message: `dist/mcp.js exists` });
|
|
176
|
+
} else {
|
|
177
|
+
results.push({
|
|
178
|
+
check: 'MCP entrypoint',
|
|
179
|
+
status: 'warn',
|
|
180
|
+
message: 'dist/mcp.js not found — using dev mode (npx tsx)',
|
|
181
|
+
fix: 'Run: npm run build',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check database
|
|
186
|
+
if (existsSync(ctx.dbPath)) {
|
|
187
|
+
results.push({ check: 'Database', status: 'ok', message: ctx.dbPath });
|
|
188
|
+
} else {
|
|
189
|
+
results.push({ check: 'Database', status: 'warn', message: `${ctx.dbPath} not found (will be created on first use)` });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check CLAUDE.md
|
|
193
|
+
const claudeMdPath = ctx.isGlobal
|
|
194
|
+
? join(homedir(), '.claude', 'CLAUDE.md')
|
|
195
|
+
: join(ctx.cwd, 'CLAUDE.md');
|
|
196
|
+
if (existsSync(claudeMdPath)) {
|
|
197
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
198
|
+
if (content.includes('## Memory (AWM)')) {
|
|
199
|
+
results.push({ check: 'Instructions', status: 'ok', message: `CLAUDE.md has AWM section` });
|
|
200
|
+
} else {
|
|
201
|
+
results.push({
|
|
202
|
+
check: 'Instructions',
|
|
203
|
+
status: 'warn',
|
|
204
|
+
message: `CLAUDE.md exists but missing AWM section`,
|
|
205
|
+
fix: `Run: awm setup claude-code${ctx.isGlobal ? ' --global' : ''}`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
results.push({ check: 'Instructions', status: 'warn', message: `CLAUDE.md not found` });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check hooks
|
|
213
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
214
|
+
if (existsSync(settingsPath)) {
|
|
215
|
+
try {
|
|
216
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
217
|
+
const hasHooks = settings.hooks?.PreCompact && settings.hooks?.SessionEnd;
|
|
218
|
+
if (hasHooks) {
|
|
219
|
+
results.push({ check: 'Hooks', status: 'ok', message: 'PreCompact + SessionEnd configured' });
|
|
220
|
+
} else {
|
|
221
|
+
results.push({ check: 'Hooks', status: 'warn', message: 'Hooks partially configured' });
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
results.push({ check: 'Hooks', status: 'warn', message: 'settings.json is not valid JSON' });
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
results.push({ check: 'Hooks', status: 'warn', message: 'No hooks configured (auto-checkpoint disabled)' });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return results;
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export default adapter;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Codex CLI adapter — writes TOML config, AGENTS.md instructions.
|
|
6
|
+
*
|
|
7
|
+
* Codex stores MCP config in ~/.codex/config.toml (global only).
|
|
8
|
+
* Uses snake_case [mcp_servers.name] sections.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
12
|
+
import { join, dirname, basename } from 'node:path';
|
|
13
|
+
import type { CLIAdapter, SetupContext, DiagnosticResult } from './types.js';
|
|
14
|
+
import { resolveMcpCommand, homedir, AWM_INSTRUCTION_CONTENT } from './common.js';
|
|
15
|
+
|
|
16
|
+
// ─── Minimal TOML read/write ──────────────────────────
|
|
17
|
+
// Only handles the flat structure Codex uses: [section.name] with key = "value" or key = ["array"]
|
|
18
|
+
|
|
19
|
+
interface TomlSection {
|
|
20
|
+
[key: string]: string | string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface TomlDoc {
|
|
24
|
+
/** Sections: key is the full dotted section name */
|
|
25
|
+
sections: Map<string, TomlSection>;
|
|
26
|
+
/** Lines before the first section (top-level keys, comments) */
|
|
27
|
+
preamble: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseTOML(content: string): TomlDoc {
|
|
31
|
+
const doc: TomlDoc = { sections: new Map(), preamble: [] };
|
|
32
|
+
let currentSection = '';
|
|
33
|
+
const lines = content.split('\n');
|
|
34
|
+
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
|
|
38
|
+
// Section header
|
|
39
|
+
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
40
|
+
if (sectionMatch) {
|
|
41
|
+
currentSection = sectionMatch[1];
|
|
42
|
+
if (!doc.sections.has(currentSection)) {
|
|
43
|
+
doc.sections.set(currentSection, {});
|
|
44
|
+
}
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Skip comments and empty lines
|
|
49
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
50
|
+
if (!currentSection) doc.preamble.push(line);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Key = value
|
|
55
|
+
const eqIdx = trimmed.indexOf('=');
|
|
56
|
+
if (eqIdx === -1) {
|
|
57
|
+
if (!currentSection) doc.preamble.push(line);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
62
|
+
const rawVal = trimmed.slice(eqIdx + 1).trim();
|
|
63
|
+
|
|
64
|
+
if (!currentSection) {
|
|
65
|
+
if (!doc.sections.has('')) doc.sections.set('', {});
|
|
66
|
+
doc.sections.get('')![key] = parseTomlValue(rawVal);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!doc.sections.has(currentSection)) {
|
|
71
|
+
doc.sections.set(currentSection, {});
|
|
72
|
+
}
|
|
73
|
+
doc.sections.get(currentSection)![key] = parseTomlValue(rawVal);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return doc;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseTomlValue(raw: string): string | string[] {
|
|
80
|
+
// Array: ["a", "b"]
|
|
81
|
+
if (raw.startsWith('[')) {
|
|
82
|
+
const inner = raw.slice(1, raw.lastIndexOf(']'));
|
|
83
|
+
return inner.split(',')
|
|
84
|
+
.map(s => s.trim().replace(/^["']|["']$/g, ''))
|
|
85
|
+
.filter(s => s.length > 0);
|
|
86
|
+
}
|
|
87
|
+
// Quoted string
|
|
88
|
+
return raw.replace(/^["']|["']$/g, '');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function serializeTOML(doc: TomlDoc): string {
|
|
92
|
+
const lines: string[] = [];
|
|
93
|
+
|
|
94
|
+
// Preamble (top-level content before any section)
|
|
95
|
+
if (doc.preamble.length > 0) {
|
|
96
|
+
lines.push(...doc.preamble);
|
|
97
|
+
if (lines.length > 0 && lines[lines.length - 1].trim() !== '') {
|
|
98
|
+
lines.push('');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const [section, values] of doc.sections) {
|
|
103
|
+
if (section === '') continue; // already in preamble
|
|
104
|
+
lines.push(`[${section}]`);
|
|
105
|
+
for (const [key, val] of Object.entries(values)) {
|
|
106
|
+
if (Array.isArray(val)) {
|
|
107
|
+
const items = val.map(v => `"${v}"`).join(', ');
|
|
108
|
+
lines.push(`${key} = [${items}]`);
|
|
109
|
+
} else {
|
|
110
|
+
lines.push(`${key} = "${val}"`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
lines.push('');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return lines.join('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Adapter ──────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
const adapter: CLIAdapter = {
|
|
122
|
+
id: 'codex',
|
|
123
|
+
name: 'Codex',
|
|
124
|
+
supportsProjectScope: false,
|
|
125
|
+
supportsGlobalScope: true,
|
|
126
|
+
|
|
127
|
+
writeMcpConfig(ctx: SetupContext): string {
|
|
128
|
+
if (!ctx.isGlobal) {
|
|
129
|
+
console.log(' Note: Codex only supports global MCP config. Using --global automatically.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const configPath = join(homedir(), '.codex', 'config.toml');
|
|
133
|
+
const mcpCmd = resolveMcpCommand(ctx);
|
|
134
|
+
|
|
135
|
+
// Read existing config or start fresh
|
|
136
|
+
let doc: TomlDoc;
|
|
137
|
+
if (existsSync(configPath)) {
|
|
138
|
+
doc = parseTOML(readFileSync(configPath, 'utf-8'));
|
|
139
|
+
} else {
|
|
140
|
+
doc = { sections: new Map(), preamble: [] };
|
|
141
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Write the MCP server section
|
|
145
|
+
const sectionName = 'mcp_servers.agent-working-memory';
|
|
146
|
+
doc.sections.set(sectionName, {
|
|
147
|
+
command: mcpCmd.command,
|
|
148
|
+
args: mcpCmd.args,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Write env vars in a sub-section
|
|
152
|
+
const envSection = `${sectionName}.env`;
|
|
153
|
+
const envEntries: TomlSection = {};
|
|
154
|
+
for (const [key, val] of Object.entries(ctx.envVars)) {
|
|
155
|
+
envEntries[key] = val;
|
|
156
|
+
}
|
|
157
|
+
doc.sections.set(envSection, envEntries);
|
|
158
|
+
|
|
159
|
+
writeFileSync(configPath, serializeTOML(doc));
|
|
160
|
+
return `MCP config: ${configPath}`;
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
writeInstructions(ctx: SetupContext, skip: boolean): string {
|
|
164
|
+
const agentsMdPath = join(ctx.cwd, 'AGENTS.md');
|
|
165
|
+
|
|
166
|
+
if (skip) return 'AGENTS.md: skipped (--no-instructions)';
|
|
167
|
+
|
|
168
|
+
if (existsSync(agentsMdPath)) {
|
|
169
|
+
const content = readFileSync(agentsMdPath, 'utf-8');
|
|
170
|
+
if (content.includes('## Memory (AWM)')) {
|
|
171
|
+
return 'AGENTS.md: already has AWM section (skipped)';
|
|
172
|
+
}
|
|
173
|
+
writeFileSync(agentsMdPath, content.trimEnd() + '\n\n' + AWM_INSTRUCTION_CONTENT);
|
|
174
|
+
return 'AGENTS.md: appended AWM workflow section';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const title = `# ${basename(ctx.cwd)} — Agent Instructions`;
|
|
178
|
+
writeFileSync(agentsMdPath, `${title}\n\n${AWM_INSTRUCTION_CONTENT}`);
|
|
179
|
+
return 'AGENTS.md: created with AWM workflow section';
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
writeHooks(_ctx: SetupContext, _skip: boolean): string {
|
|
183
|
+
return 'Hooks: not supported by Codex (auto-checkpoint unavailable)';
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
diagnose(ctx: SetupContext): DiagnosticResult[] {
|
|
187
|
+
const results: DiagnosticResult[] = [];
|
|
188
|
+
|
|
189
|
+
// Check TOML config
|
|
190
|
+
const configPath = join(homedir(), '.codex', 'config.toml');
|
|
191
|
+
if (!existsSync(configPath)) {
|
|
192
|
+
results.push({
|
|
193
|
+
check: 'MCP config',
|
|
194
|
+
status: 'fail',
|
|
195
|
+
message: `${configPath} not found`,
|
|
196
|
+
fix: 'Run: awm setup codex',
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
try {
|
|
200
|
+
const doc = parseTOML(readFileSync(configPath, 'utf-8'));
|
|
201
|
+
if (doc.sections.has('mcp_servers.agent-working-memory')) {
|
|
202
|
+
results.push({ check: 'MCP config', status: 'ok', message: `AWM registered in ${configPath}` });
|
|
203
|
+
|
|
204
|
+
// Verify the command points to a real file
|
|
205
|
+
const section = doc.sections.get('mcp_servers.agent-working-memory')!;
|
|
206
|
+
const args = section.args;
|
|
207
|
+
if (Array.isArray(args) && args.length > 0) {
|
|
208
|
+
const entrypoint = args[args.length - 1];
|
|
209
|
+
if (existsSync(entrypoint)) {
|
|
210
|
+
results.push({ check: 'MCP entrypoint', status: 'ok', message: entrypoint });
|
|
211
|
+
} else {
|
|
212
|
+
results.push({
|
|
213
|
+
check: 'MCP entrypoint',
|
|
214
|
+
status: 'fail',
|
|
215
|
+
message: `Configured entrypoint not found: ${entrypoint}`,
|
|
216
|
+
fix: 'Run: npm run build (in AWM package) then awm setup codex',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
results.push({
|
|
222
|
+
check: 'MCP config',
|
|
223
|
+
status: 'fail',
|
|
224
|
+
message: 'config.toml exists but missing agent-working-memory section',
|
|
225
|
+
fix: 'Run: awm setup codex',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
results.push({ check: 'MCP config', status: 'fail', message: `${configPath} could not be parsed` });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check database
|
|
234
|
+
if (existsSync(ctx.dbPath)) {
|
|
235
|
+
results.push({ check: 'Database', status: 'ok', message: ctx.dbPath });
|
|
236
|
+
} else {
|
|
237
|
+
results.push({ check: 'Database', status: 'warn', message: `${ctx.dbPath} not found (will be created on first use)` });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Check AGENTS.md
|
|
241
|
+
const agentsMdPath = join(ctx.cwd, 'AGENTS.md');
|
|
242
|
+
if (existsSync(agentsMdPath)) {
|
|
243
|
+
const content = readFileSync(agentsMdPath, 'utf-8');
|
|
244
|
+
if (content.includes('## Memory (AWM)')) {
|
|
245
|
+
results.push({ check: 'Instructions', status: 'ok', message: 'AGENTS.md has AWM section' });
|
|
246
|
+
} else {
|
|
247
|
+
results.push({
|
|
248
|
+
check: 'Instructions',
|
|
249
|
+
status: 'warn',
|
|
250
|
+
message: 'AGENTS.md exists but missing AWM section',
|
|
251
|
+
fix: 'Run: awm setup codex',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
results.push({ check: 'Instructions', status: 'warn', message: 'AGENTS.md not found' });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return results;
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export default adapter;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared utilities for CLI adapters.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from the original setup() in cli.ts — path resolution, secrets,
|
|
8
|
+
* environment variables, MCP command building, and the AWM instruction snippet.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
12
|
+
import { resolve, join, dirname, basename } from 'node:path';
|
|
13
|
+
import { randomBytes } from 'node:crypto';
|
|
14
|
+
import { homedir as osHomedir } from 'node:os';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import type { SetupContext } from './types.js';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
|
|
21
|
+
/** Resolve the AWM package root (where src/ and dist/ live). */
|
|
22
|
+
export function resolvePackageRoot(): string {
|
|
23
|
+
// __dirname is src/adapters/ at dev time, dist/adapters/ at build time
|
|
24
|
+
return resolve(__dirname, '..', '..');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Resolve the database path — default to <packageRoot>/data/memory.db. */
|
|
28
|
+
export function resolveDbPath(packageRoot: string, explicit?: string | null): string {
|
|
29
|
+
const dbPath = explicit ?? join(packageRoot, 'data', 'memory.db');
|
|
30
|
+
const dbDir = dirname(dbPath);
|
|
31
|
+
if (!existsSync(dbDir)) {
|
|
32
|
+
mkdirSync(dbDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
return dbPath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Read or generate the hook secret token. */
|
|
38
|
+
export function resolveHookSecret(dbPath: string): string {
|
|
39
|
+
const secretPath = join(dirname(dbPath), '.awm-hook-secret');
|
|
40
|
+
if (existsSync(secretPath)) {
|
|
41
|
+
const existing = readFileSync(secretPath, 'utf-8').trim();
|
|
42
|
+
if (existing) return existing;
|
|
43
|
+
}
|
|
44
|
+
const secret = randomBytes(32).toString('hex');
|
|
45
|
+
mkdirSync(dirname(secretPath), { recursive: true });
|
|
46
|
+
writeFileSync(secretPath, secret + '\n');
|
|
47
|
+
return secret;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Build environment variables for the MCP server process. */
|
|
51
|
+
export function buildEnvVars(
|
|
52
|
+
dbPath: string,
|
|
53
|
+
agentId: string,
|
|
54
|
+
hookPort: string,
|
|
55
|
+
hookSecret: string,
|
|
56
|
+
isWindows: boolean,
|
|
57
|
+
): Record<string, string> {
|
|
58
|
+
return {
|
|
59
|
+
AWM_DB_PATH: isWindows ? dbPath.replace(/\\/g, '/') : dbPath,
|
|
60
|
+
AWM_AGENT_ID: agentId,
|
|
61
|
+
AWM_HOOK_PORT: hookPort,
|
|
62
|
+
AWM_HOOK_SECRET: hookSecret,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the MCP server command + args.
|
|
68
|
+
*
|
|
69
|
+
* Prefers absolute path to dist/mcp.js (works from any cwd).
|
|
70
|
+
* Falls back to npx tsx src/mcp.ts for dev mode.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveMcpCommand(ctx: SetupContext): {
|
|
73
|
+
command: string;
|
|
74
|
+
args: string[];
|
|
75
|
+
} {
|
|
76
|
+
if (ctx.hasDist) {
|
|
77
|
+
return {
|
|
78
|
+
command: 'node',
|
|
79
|
+
args: [ctx.mcpDist.replace(/\\/g, '/')],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Dev fallback
|
|
83
|
+
if (ctx.isWindows) {
|
|
84
|
+
return {
|
|
85
|
+
command: 'cmd',
|
|
86
|
+
args: ['/c', 'npx', 'tsx', ctx.mcpScript.replace(/\\/g, '/')],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
command: 'npx',
|
|
91
|
+
args: ['tsx', ctx.mcpScript],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Build a full SetupContext from parsed CLI flags. */
|
|
96
|
+
export function buildSetupContext(opts: {
|
|
97
|
+
agentId?: string;
|
|
98
|
+
dbPath?: string | null;
|
|
99
|
+
isGlobal: boolean;
|
|
100
|
+
hookPort: string;
|
|
101
|
+
}): SetupContext {
|
|
102
|
+
const cwd = process.cwd();
|
|
103
|
+
const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
104
|
+
const packageRoot = resolvePackageRoot();
|
|
105
|
+
const mcpScript = join(packageRoot, 'src', 'mcp.ts');
|
|
106
|
+
const mcpDist = join(packageRoot, 'dist', 'mcp.js');
|
|
107
|
+
const hasDist = existsSync(mcpDist);
|
|
108
|
+
const isWindows = process.platform === 'win32';
|
|
109
|
+
|
|
110
|
+
const agentId = opts.agentId ?? (opts.isGlobal ? 'claude' : projectName);
|
|
111
|
+
const dbPath = resolveDbPath(packageRoot, opts.dbPath);
|
|
112
|
+
const hookSecret = resolveHookSecret(dbPath);
|
|
113
|
+
const envVars = buildEnvVars(dbPath, agentId, opts.hookPort, hookSecret, isWindows);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
cwd,
|
|
117
|
+
projectName,
|
|
118
|
+
agentId,
|
|
119
|
+
dbPath,
|
|
120
|
+
packageRoot,
|
|
121
|
+
mcpDist,
|
|
122
|
+
mcpScript,
|
|
123
|
+
hasDist,
|
|
124
|
+
hookSecret,
|
|
125
|
+
hookPort: opts.hookPort,
|
|
126
|
+
isGlobal: opts.isGlobal,
|
|
127
|
+
isWindows,
|
|
128
|
+
envVars,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Home directory. */
|
|
133
|
+
export function homedir(): string {
|
|
134
|
+
return osHomedir();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Instruction content ────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Core AWM instruction snippet — shared across all adapters.
|
|
141
|
+
* Each adapter wraps this in the appropriate file format.
|
|
142
|
+
*/
|
|
143
|
+
export const AWM_INSTRUCTION_CONTENT = `
|
|
144
|
+
## Memory (AWM)
|
|
145
|
+
You have persistent memory via the agent-working-memory MCP server.
|
|
146
|
+
|
|
147
|
+
### Lifecycle (always do these)
|
|
148
|
+
- Session start: call memory_restore to recover previous context
|
|
149
|
+
- Starting a task: call memory_task_begin (checkpoints + recalls relevant memories)
|
|
150
|
+
- Finishing a task: call memory_task_end with a summary
|
|
151
|
+
- Auto-checkpoint: hooks handle compaction, session end, and 15-min timer (no action needed)
|
|
152
|
+
|
|
153
|
+
### Write memory when:
|
|
154
|
+
- A project decision is made or changed
|
|
155
|
+
- A root cause is discovered after debugging
|
|
156
|
+
- A reusable implementation pattern is established
|
|
157
|
+
- A user preference, constraint, or requirement is clarified
|
|
158
|
+
- A prior assumption is found to be wrong
|
|
159
|
+
- A significant piece of work is completed
|
|
160
|
+
|
|
161
|
+
### Recall memory when:
|
|
162
|
+
- Starting work on a new task or subsystem
|
|
163
|
+
- Re-entering code you haven't touched recently
|
|
164
|
+
- After a failed attempt — check if there's prior knowledge
|
|
165
|
+
- Before refactoring or making architectural changes
|
|
166
|
+
- When a topic comes up that you might have prior context on
|
|
167
|
+
|
|
168
|
+
### Also:
|
|
169
|
+
- After using a recalled memory: call memory_feedback (useful/not-useful)
|
|
170
|
+
- To correct wrong info: call memory_retract or memory_supersede
|
|
171
|
+
- To track work items: memory_task_add, memory_task_update, memory_task_list, memory_task_next
|
|
172
|
+
`.trimStart();
|