forget-connect 0.5.2 → 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 +77 -21
- package/assets/hooks/forget_bstate.py +229 -0
- package/assets/hooks/forget_capture.py +42 -6
- package/assets/hooks/forget_sessionstart.py +41 -4
- package/assets/hooks/forget_toolrecall.py +118 -0
- package/assets/hooks/forget_turnrecall.py +664 -45
- package/assets/plugins/memory-agent/.claude-plugin/plugin.json +15 -0
- package/assets/plugins/memory-agent/.codex-plugin/plugin.json +29 -0
- package/assets/plugins/memory-agent/README.md +9 -0
- package/assets/plugins/memory-agent/skills/memory-agent/SKILL.md +22 -0
- package/assets/plugins/memory-agent/skills/memory-agent/agents/openai.yaml +6 -0
- package/assets/proxy/forget_proxy_watchdog.py +215 -0
- package/assets/skills/memory-agent/SKILL.md +22 -0
- package/assets/skills/memory-agent/agents/openai.yaml +6 -0
- package/package.json +7 -3
- package/src/cli.js +447 -58
- package/src/core.js +28 -9
- package/src/doctor.js +23 -0
- package/src/hooks.js +50 -20
- package/src/proxy.js +526 -0
- package/src/skills.js +95 -0
package/src/core.js
CHANGED
|
@@ -43,6 +43,19 @@ export const MEMORY_RULES = [
|
|
|
43
43
|
RULES_END,
|
|
44
44
|
].join("\n");
|
|
45
45
|
|
|
46
|
+
export const CODEX_MEMORY_RULES = [
|
|
47
|
+
RULES_START,
|
|
48
|
+
"# Memory (Forget)",
|
|
49
|
+
"You have the user's long-term memory via the `forget` MCP server.",
|
|
50
|
+
"At session start and on resume/continue requests, call `prepare_codex_context` once with a short query for the current work and the exact current working directory as `client_workdir`. Use only its project-bound results. If it returns `project_unresolved`, `empty`, or `unavailable`, continue without memory; do not fall back to generic autopilot or cross-project task state.",
|
|
51
|
+
"Whenever the user refers to their own past decisions, preferences, plans, people, or prior discussions, call `search_memories` FIRST. For project-specific history, prefer `prepare_codex_context(query, client_workdir)` so another project's state cannot enter the capsule.",
|
|
52
|
+
"Results may carry a `trust` label: green (user-stated or tool-observed) is safe to act on; yellow (agent-inferred or self-summarized) requires confirmation before real-world action; red is superseded; unlabeled is yellow.",
|
|
53
|
+
"When the user states a durable decision, preference, or lasting fact, save it with `add_memory`; use `source_role=\"user\"` only for the user's own words and check `get_event_status` before claiming the write completed. Never record a planned action as completed without evidence.",
|
|
54
|
+
"Use `supersede_memory` to retire a wrong fact and `confirm_memory` when evidence verifies a provisional claim. When a context trace materially influenced work, call `record_context_outcome` with the memory ids actually used or rejected.",
|
|
55
|
+
"Use `team_read` and `team_note` only for explicit agent collaboration, never as a substitute for personal memory.",
|
|
56
|
+
RULES_END,
|
|
57
|
+
].join("\n");
|
|
58
|
+
|
|
46
59
|
export class ConfigError extends Error {
|
|
47
60
|
constructor(message) {
|
|
48
61
|
super(message);
|
|
@@ -315,16 +328,16 @@ function removeRulesBlock(text, startMarker, endMarker) {
|
|
|
315
328
|
return left || right;
|
|
316
329
|
}
|
|
317
330
|
|
|
318
|
-
export function installRules(raw, { migrateLegacy = true } = {}) {
|
|
331
|
+
export function installRules(raw, { migrateLegacy = true, rules = MEMORY_RULES } = {}) {
|
|
319
332
|
let next = raw;
|
|
320
333
|
if (migrateLegacy) {
|
|
321
334
|
next = removeRulesBlock(next, LEGACY_RULES_START, LEGACY_RULES_END);
|
|
322
335
|
}
|
|
323
336
|
const bounds = findManagedBlock(next, RULES_START, RULES_END);
|
|
324
337
|
if (bounds) {
|
|
325
|
-
return next.slice(0, bounds.start) +
|
|
338
|
+
return next.slice(0, bounds.start) + rules + next.slice(bounds.end);
|
|
326
339
|
}
|
|
327
|
-
return next ? `${next}\n${
|
|
340
|
+
return next ? `${next}\n${rules}\n` : `${rules}\n`;
|
|
328
341
|
}
|
|
329
342
|
|
|
330
343
|
export function removeRules(raw) {
|
|
@@ -579,10 +592,10 @@ function rulesConnected(raw) {
|
|
|
579
592
|
}
|
|
580
593
|
}
|
|
581
594
|
|
|
582
|
-
function rulesCurrent(raw) {
|
|
595
|
+
function rulesCurrent(raw, rules = MEMORY_RULES) {
|
|
583
596
|
try {
|
|
584
597
|
const bounds = findManagedBlock(raw, RULES_START, RULES_END);
|
|
585
|
-
return Boolean(bounds && raw.slice(bounds.start, bounds.end) ===
|
|
598
|
+
return Boolean(bounds && raw.slice(bounds.start, bounds.end) === rules);
|
|
586
599
|
} catch {
|
|
587
600
|
return false;
|
|
588
601
|
}
|
|
@@ -602,7 +615,10 @@ export async function inspectClients(clients, { url = "", apiKey = "", urlFor =
|
|
|
602
615
|
client,
|
|
603
616
|
config: configConnected(client, configRaw),
|
|
604
617
|
rules: client.rulesPath ? rulesConnected(rulesRaw) : null,
|
|
605
|
-
rulesCurrent: client.rulesPath ? rulesCurrent(
|
|
618
|
+
rulesCurrent: client.rulesPath ? rulesCurrent(
|
|
619
|
+
rulesRaw,
|
|
620
|
+
client.id === "codex" ? CODEX_MEMORY_RULES : MEMORY_RULES,
|
|
621
|
+
) : null,
|
|
606
622
|
...expected,
|
|
607
623
|
};
|
|
608
624
|
}),
|
|
@@ -631,8 +647,8 @@ export async function buildPlan(
|
|
|
631
647
|
}
|
|
632
648
|
const changes = [];
|
|
633
649
|
for (const client of clients) {
|
|
634
|
-
// urlFor lets
|
|
635
|
-
//
|
|
650
|
+
// urlFor lets every provider share one vault scope while retaining its
|
|
651
|
+
// own query profile and provenance in the installed URL.
|
|
636
652
|
const clientUrl = urlFor ? urlFor(client) : url;
|
|
637
653
|
const configRaw = await readOptional(client.configPath);
|
|
638
654
|
let configNext;
|
|
@@ -665,7 +681,10 @@ export async function buildPlan(
|
|
|
665
681
|
if (client.rulesPath && installInstructionRules) {
|
|
666
682
|
const rulesRaw = await readOptional(client.rulesPath);
|
|
667
683
|
const rulesNext = action === "connect"
|
|
668
|
-
? installRules(rulesRaw, {
|
|
684
|
+
? installRules(rulesRaw, {
|
|
685
|
+
migrateLegacy,
|
|
686
|
+
rules: client.id === "codex" ? CODEX_MEMORY_RULES : MEMORY_RULES,
|
|
687
|
+
})
|
|
669
688
|
: removeRules(rulesRaw);
|
|
670
689
|
addChange(
|
|
671
690
|
changes,
|
package/src/doctor.js
CHANGED
|
@@ -9,6 +9,29 @@ export const REQUIRED_TOOLS = [
|
|
|
9
9
|
"record_context_outcome",
|
|
10
10
|
"record_task_state",
|
|
11
11
|
"get_task_state",
|
|
12
|
+
"catalog_search",
|
|
13
|
+
"product_quote",
|
|
14
|
+
"grant_create",
|
|
15
|
+
"agent_consult",
|
|
16
|
+
"receipt_verify",
|
|
17
|
+
"grant_revoke",
|
|
18
|
+
];
|
|
19
|
+
export const CODEX_REQUIRED_TOOLS = [
|
|
20
|
+
"prepare_codex_context",
|
|
21
|
+
"search_memories",
|
|
22
|
+
"add_memory",
|
|
23
|
+
"supersede_memory",
|
|
24
|
+
"confirm_memory",
|
|
25
|
+
"get_event_status",
|
|
26
|
+
"record_context_outcome",
|
|
27
|
+
"team_read",
|
|
28
|
+
"team_note",
|
|
29
|
+
"catalog_search",
|
|
30
|
+
"product_quote",
|
|
31
|
+
"grant_create",
|
|
32
|
+
"agent_consult",
|
|
33
|
+
"receipt_verify",
|
|
34
|
+
"grant_revoke",
|
|
12
35
|
];
|
|
13
36
|
|
|
14
37
|
function addSensitiveValueVariants(variants, value) {
|
package/src/hooks.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// The hooks layer is the product thesis made real: memory that arrives
|
|
2
|
-
// without being asked. Claude Code
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// Desktop) rely on instruction rules until they grow hook systems.
|
|
2
|
+
// without being asked. Claude Code and Codex share the same scripts and
|
|
3
|
+
// ownership rules, while each receives only lifecycle events its client
|
|
4
|
+
// actually emits. Desktop continues to rely on instruction rules.
|
|
6
5
|
//
|
|
7
6
|
// First rule of touching ~/.claude/settings.json: it is the user's file.
|
|
8
7
|
// Foreign hooks (other tools register there too) must survive connect,
|
|
@@ -20,6 +19,7 @@ export const HOOK_SCRIPTS = [
|
|
|
20
19
|
"forget_sessionstart.py",
|
|
21
20
|
"forget_turnrecall.py",
|
|
22
21
|
"forget_capture.py",
|
|
22
|
+
"forget_bstate.py", // compact structured working state shared by both clients
|
|
23
23
|
"forget_project.py", // shared project-boundary detection (imported by the others)
|
|
24
24
|
"forget_projecttag.py", // PreToolUse: stamps memory/task writes with the cwd's project
|
|
25
25
|
];
|
|
@@ -47,6 +47,9 @@ export function hooksDirFor(options = {}) {
|
|
|
47
47
|
export function settingsPathFor(options = {}) {
|
|
48
48
|
const env = options.env ?? process.env;
|
|
49
49
|
const home = options.home ?? env.HOME ?? env.USERPROFILE ?? os.homedir();
|
|
50
|
+
if (options.clientId === "codex") {
|
|
51
|
+
return path.join(env.CODEX_HOME ?? path.join(home, ".codex"), "hooks.json");
|
|
52
|
+
}
|
|
50
53
|
return path.join(home, ".claude", "settings.json");
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -55,8 +58,8 @@ export function hookCommand(script, { hooksDir, url }) {
|
|
|
55
58
|
return `FORGET_MCP_URL=${shellQuote(url)} python3 ${shellQuote(scriptPath)}`;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
export function hookEntries({ hooksDir, url }) {
|
|
59
|
-
|
|
61
|
+
export function hookEntries({ hooksDir, url, clientId = "claude-code" }) {
|
|
62
|
+
const shared = {
|
|
60
63
|
SessionStart: {
|
|
61
64
|
command: hookCommand("forget_sessionstart.py", { hooksDir, url }),
|
|
62
65
|
timeout: 12,
|
|
@@ -67,6 +70,23 @@ export function hookEntries({ hooksDir, url }) {
|
|
|
67
70
|
timeout: 6,
|
|
68
71
|
statusMessage: "forget: checking relevant memories",
|
|
69
72
|
},
|
|
73
|
+
};
|
|
74
|
+
if (clientId === "codex") {
|
|
75
|
+
return {
|
|
76
|
+
...shared,
|
|
77
|
+
SessionStart: {
|
|
78
|
+
...shared.SessionStart,
|
|
79
|
+
matcher: "startup|resume|compact",
|
|
80
|
+
},
|
|
81
|
+
Stop: {
|
|
82
|
+
command: hookCommand("forget_capture.py", { hooksDir, url }),
|
|
83
|
+
timeout: 10,
|
|
84
|
+
statusMessage: "forget: capturing turn + outcome",
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
...shared,
|
|
70
90
|
PreCompact: {
|
|
71
91
|
command: hookCommand("forget_capture.py", { hooksDir, url }),
|
|
72
92
|
timeout: 10,
|
|
@@ -127,10 +147,11 @@ function validatedHooks(config, label) {
|
|
|
127
147
|
return hooks;
|
|
128
148
|
}
|
|
129
149
|
|
|
130
|
-
export function connectHooksSettings(raw, { hooksDir, url }) {
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
150
|
+
export function connectHooksSettings(raw, { hooksDir, url, clientId = "claude-code" }) {
|
|
151
|
+
const label = clientId === "codex" ? "hooks.json" : "settings.json";
|
|
152
|
+
const config = parseJsonStrict(raw, label);
|
|
153
|
+
const hooks = validatedHooks(config, label);
|
|
154
|
+
for (const [event, entry] of Object.entries(hookEntries({ hooksDir, url, clientId }))) {
|
|
134
155
|
const groups = withoutOurHooks(hooks[event] ?? []);
|
|
135
156
|
const group = {
|
|
136
157
|
hooks: [{
|
|
@@ -148,10 +169,11 @@ export function connectHooksSettings(raw, { hooksDir, url }) {
|
|
|
148
169
|
return `${JSON.stringify(config, null, 2)}\n`;
|
|
149
170
|
}
|
|
150
171
|
|
|
151
|
-
export function disconnectHooksSettings(raw) {
|
|
152
|
-
const
|
|
172
|
+
export function disconnectHooksSettings(raw, { clientId = "claude-code" } = {}) {
|
|
173
|
+
const label = clientId === "codex" ? "hooks.json" : "settings.json";
|
|
174
|
+
const config = parseJsonStrict(raw, label);
|
|
153
175
|
if (config.hooks === undefined) return raw;
|
|
154
|
-
const hooks = validatedHooks(config,
|
|
176
|
+
const hooks = validatedHooks(config, label);
|
|
155
177
|
let touched = false;
|
|
156
178
|
for (const [event, groups] of Object.entries(hooks)) {
|
|
157
179
|
const cleaned = withoutOurHooks(groups);
|
|
@@ -168,15 +190,19 @@ export function disconnectHooksSettings(raw) {
|
|
|
168
190
|
return `${JSON.stringify(config, null, 2)}\n`;
|
|
169
191
|
}
|
|
170
192
|
|
|
171
|
-
export function hooksInstalled(raw, { hooksDir } = {}) {
|
|
193
|
+
export function hooksInstalled(raw, { hooksDir, clientId = "claude-code" } = {}) {
|
|
172
194
|
let config;
|
|
173
195
|
try {
|
|
174
|
-
config = parseJsonStrict(raw, "settings.json");
|
|
196
|
+
config = parseJsonStrict(raw, clientId === "codex" ? "hooks.json" : "settings.json");
|
|
175
197
|
} catch {
|
|
176
198
|
return false;
|
|
177
199
|
}
|
|
178
200
|
if (!isObject(config.hooks)) return false;
|
|
179
|
-
const events = Object.keys(hookEntries({
|
|
201
|
+
const events = Object.keys(hookEntries({
|
|
202
|
+
hooksDir: hooksDir ?? "",
|
|
203
|
+
url: "http://x",
|
|
204
|
+
clientId,
|
|
205
|
+
}));
|
|
180
206
|
return events.every((event) => {
|
|
181
207
|
const groups = config.hooks[event];
|
|
182
208
|
return Array.isArray(groups) && groups.some(
|
|
@@ -214,20 +240,23 @@ export async function installHookScripts(hooksDir) {
|
|
|
214
240
|
return written;
|
|
215
241
|
}
|
|
216
242
|
|
|
217
|
-
export async function inspectHooks({ env } = {}) {
|
|
243
|
+
export async function inspectHooks({ env, clientId = "claude-code" } = {}) {
|
|
218
244
|
const hooksDir = hooksDirFor({ env });
|
|
219
|
-
const settingsPath = settingsPathFor({ env });
|
|
245
|
+
const settingsPath = settingsPathFor({ env, clientId });
|
|
220
246
|
let settingsRaw = "";
|
|
221
247
|
try {
|
|
222
248
|
settingsRaw = await readFile(settingsPath, "utf8");
|
|
223
249
|
} catch {
|
|
224
250
|
settingsRaw = "";
|
|
225
251
|
}
|
|
226
|
-
const registered = hooksInstalled(settingsRaw, { hooksDir });
|
|
252
|
+
const registered = hooksInstalled(settingsRaw, { hooksDir, clientId });
|
|
227
253
|
let scriptsPresent = true;
|
|
254
|
+
let sendsKey = true;
|
|
228
255
|
for (const script of HOOK_SCRIPTS) {
|
|
229
256
|
try {
|
|
230
|
-
await readFile(path.join(hooksDir, script), "utf8");
|
|
257
|
+
const body = await readFile(path.join(hooksDir, script), "utf8");
|
|
258
|
+
// only scripts that call the server need the Bearer helper
|
|
259
|
+
if (body.includes("FORGET_MCP_URL") && !body.includes("_auth_headers")) sendsKey = false;
|
|
231
260
|
} catch {
|
|
232
261
|
scriptsPresent = false;
|
|
233
262
|
}
|
|
@@ -238,6 +267,7 @@ export async function inspectHooks({ env } = {}) {
|
|
|
238
267
|
return {
|
|
239
268
|
registered,
|
|
240
269
|
scripts_present: scriptsPresent,
|
|
270
|
+
sends_key: sendsKey,
|
|
241
271
|
python3,
|
|
242
272
|
// registered-but-broken is a failure; absent hooks are a legitimate
|
|
243
273
|
// choice (--no-hooks), so they don't fail the check
|