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/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) + MEMORY_RULES + next.slice(bounds.end);
338
+ return next.slice(0, bounds.start) + rules + next.slice(bounds.end);
326
339
  }
327
- return next ? `${next}\n${MEMORY_RULES}\n` : `${MEMORY_RULES}\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) === MEMORY_RULES);
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(rulesRaw) : null,
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 connect scope each client into its own memory pool
635
- // (/mcp/{app}/http/{user}) while everything else shares one plan.
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, { migrateLegacy })
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 gets four harness hooks — a session-start
3
- // context capsule, per-turn push recall with conflict-zone alerts, and
4
- // session capture feeding the outcome flywheel. Other clients (Codex,
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
- return {
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 config = parseJsonStrict(raw, "settings.json");
132
- const hooks = validatedHooks(config, "settings.json");
133
- for (const [event, entry] of Object.entries(hookEntries({ hooksDir, url }))) {
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 config = parseJsonStrict(raw, "settings.json");
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, "settings.json");
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({ hooksDir: hooksDir ?? "", url: "http://x" }));
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