forget-connect 0.5.0 → 0.5.2

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.
@@ -0,0 +1,25 @@
1
+ ---
2
+ description: forget settings — status, doctor, weekly report, cloud usage
3
+ argument-hint: "[doctor|weekly|usage]"
4
+ allowed-tools: Bash(~/.forget/venv/bin/forget-server recall:*), Bash(~/.forget/venv/bin/forget-server status:*), Bash(~/.forget/venv/bin/forget-server doctor:*), Bash(~/.forget/venv/bin/forget-server weekly:*), Bash(npx forget-connect doctor:*)
5
+ ---
6
+ <!-- forget-connect:command v1 — delete this line to keep your edits across upgrades -->
7
+
8
+ Recall dial:
9
+ !`~/.forget/venv/bin/forget-server recall status 2>/dev/null`
10
+
11
+ Argument: "$ARGUMENTS"
12
+
13
+ - `doctor`: run `~/.forget/venv/bin/forget-server doctor`. If every line is ✓,
14
+ say "healthy" in one line; if any ✗, explain only that line's prescription (→)
15
+ and ask before running it.
16
+ - `weekly`: run `~/.forget/venv/bin/forget-server weekly`; summarize the numbers only.
17
+ - `usage`: run `~/.forget/venv/bin/forget-server recall cloud-usage` and show the
18
+ output as is.
19
+ - No argument: combine the dial output above with
20
+ `~/.forget/venv/bin/forget-server status` into a settings screen of at most
21
+ 4 lines: dial / server + memory count / LLM / anything unusual (only when
22
+ present). End with a short pointer: gear changes are /forget.
23
+ If deep recall is off: a running local LLM (Ollama · LM Studio) attaches
24
+ automatically; forget cloud (forget.sh/cloud) is the no-GPU alternative —
25
+ never install anything on the user's behalf.
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: Recall dial — pick a gear (low·medium·high·extra)
3
+ argument-hint: "[low|medium|high|extra]"
4
+ allowed-tools: Bash(~/.forget/venv/bin/forget-server recall:*)
5
+ ---
6
+ <!-- forget-connect:command v1 — delete this line to keep your edits across upgrades -->
7
+
8
+ Current dial:
9
+ !`~/.forget/venv/bin/forget-server recall status 2>/dev/null | head -1`
10
+
11
+ Argument: "$ARGUMENTS"
12
+
13
+ - If the argument is a gear name (low|medium|high|extra), immediately run
14
+ `~/.forget/venv/bin/forget-server recall use <gear>` and print only the
15
+ dial line from its output — no commentary.
16
+ - If there is no argument, use AskUserQuestion to let the user pick a gear
17
+ (arrow keys). Question: "Recall gear" / header: "recall" / 4 options,
18
+ appending "(current)" to the label of the current gear:
19
+ · low — instant search (~0.2s, no LLM)
20
+ · medium — currently shares low's engine
21
+ · high — an LLM reads 40 candidates (~3s)
22
+ · extra — reads 100 candidates in full (~5s)
23
+ Apply the selection with `recall use` and print only the dial line.
24
+ - Any other argument: answer with one line — "Settings live in /forget-settings".
@@ -24,6 +24,42 @@ FORGET_URL = os.environ.get("FORGET_MCP_URL", "http://127.0.0.1:8000/mcp")
24
24
  STATE_DIR = os.path.expanduser("~/.forget/hooks/state")
25
25
  CAPSULE_CHAR_BUDGET = 1_600 # ~400 tokens
26
26
  HANDOFF_MAX_AGE_SECONDS = 48 * 3600 # a stale shift-note is worse than none
27
+ # The server capability these hooks are built against. The capsule response
28
+ # carries server_version from 0.3.9 on; older servers silently drop arguments
29
+ # these hooks send (project layering) — that mismatch must never be silent.
30
+ REQUIRED_SERVER_VERSION = "0.3.9"
31
+
32
+
33
+ def _version_tuple(value: str) -> tuple:
34
+ parts = []
35
+ for chunk in str(value or "").split("."):
36
+ digits = "".join(ch for ch in chunk if ch.isdigit())
37
+ parts.append(int(digits) if digits else 0)
38
+ return tuple(parts or [0])
39
+
40
+
41
+ def _version_notice(server_version: str) -> str:
42
+ """One warning line for the capsule header area, or ''.
43
+
44
+ Two signals, no network: the server's self-reported version (mismatch =
45
+ features silently lost), and the update-check cache that doctor/status
46
+ maintain (a local file — hooks never call out).
47
+ """
48
+ if not server_version:
49
+ return "⚠ 서버가 버전을 안 밝힘(≤0.3.8) — 훅 기능 일부가 조용히 무시될 수 있음. 처방: forget-server upgrade"
50
+ if _version_tuple(server_version) < _version_tuple(REQUIRED_SERVER_VERSION):
51
+ return (
52
+ f"⚠ 서버 {server_version} < 훅 요구 {REQUIRED_SERVER_VERSION} — "
53
+ "프로젝트 층 등이 조용히 무시됨. 처방: forget-server upgrade"
54
+ )
55
+ try:
56
+ with open(os.path.expanduser("~/.forget/update-check.json"), encoding="utf-8") as fh:
57
+ latest = str(json.load(fh).get("latest") or "")
58
+ if latest and _version_tuple(server_version) < _version_tuple(latest):
59
+ return f"새 버전 {latest} 나옴 (현재 {server_version}) — forget-server upgrade"
60
+ except Exception:
61
+ pass
62
+ return ""
27
63
 
28
64
 
29
65
  def _consume_handoff() -> str:
@@ -97,10 +133,13 @@ def main() -> None:
97
133
  if isinstance(items, list) and items:
98
134
  capsule = "\n".join(f"- {item}" for item in items[:6] if isinstance(item, str))
99
135
  handoff = _consume_handoff()
136
+ notice = _version_notice(str(result.get("server_version") or ""))
100
137
  if not capsule and not handoff:
101
- return # low confidence → silence
138
+ return # low confidence → silence (version nags don't earn a lone injection)
102
139
  shown = capsule[:CAPSULE_CHAR_BUDGET]
103
140
  parts = []
141
+ if notice:
142
+ parts.append(f"[forget 버전] {notice}")
104
143
  if handoff:
105
144
  parts.append(handoff)
106
145
  if shown:
@@ -118,7 +118,9 @@ def main() -> None:
118
118
  # possible, but only when the user asks for it — and it says so when it does.
119
119
  project = None if scope_disabled() else project_key_for_path(hook_input.get("cwd") or os.getcwd())
120
120
  crossed = bool(project) and wants_cross_project(prompt)
121
- search_args: dict = {"query": prompt[:300], "top_k": MAX_RECALLS + 2}
121
+ # Per-turn ambient recall stays on the instant path — the recall dial is
122
+ # the user's explicit choice, not a tax on every keystroke.
123
+ search_args: dict = {"query": prompt[:300], "top_k": MAX_RECALLS + 2, "recall": "low"}
122
124
  if project and not crossed:
123
125
  search_args["filters"] = layered_filter(project)
124
126
  result = _rpc("search_memories", search_args)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forget-connect",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Safely connect Claude Code, Codex, and Claude Desktop to Forget memory.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -22,11 +22,14 @@ import {
22
22
  } from "./core.js";
23
23
  import { doctorRemote } from "./doctor.js";
24
24
  import {
25
+ commandsDirFor,
25
26
  connectHooksSettings,
26
27
  disconnectHooksSettings,
27
28
  hooksDirFor,
28
29
  inspectHooks,
30
+ installCommandAssets,
29
31
  installHookScripts,
32
+ removeCommandAssets,
30
33
  removeHookScripts,
31
34
  settingsPathFor,
32
35
  } from "./hooks.js";
@@ -643,9 +646,11 @@ export async function run(argv = process.argv.slice(2), env = process.env) {
643
646
  }
644
647
  if (manageHooks && options.action === "connect") {
645
648
  stdout.write(`Would install hook scripts into ${hooksDir}\n`);
649
+ stdout.write(`Would install slash commands (/forget, /forget-settings) into ${commandsDirFor({ env })}\n`);
646
650
  }
647
651
  if (manageHooks && options.action === "disconnect") {
648
652
  stdout.write(`Would remove hook scripts from ${hooksDir}\n`);
653
+ stdout.write(`Would remove our slash commands from ${commandsDirFor({ env })} (user-edited files are kept)\n`);
649
654
  }
650
655
  return;
651
656
  }
@@ -656,6 +661,11 @@ export async function run(argv = process.argv.slice(2), env = process.env) {
656
661
  hookScriptPaths = options.action === "connect"
657
662
  ? await installHookScripts(hooksDir)
658
663
  : await removeHookScripts(hooksDir);
664
+ const commandsDir = commandsDirFor({ env });
665
+ const commandPaths = options.action === "connect"
666
+ ? await installCommandAssets(commandsDir)
667
+ : await removeCommandAssets(commandsDir);
668
+ hookScriptPaths = hookScriptPaths.concat(commandPaths);
659
669
  }
660
670
  const verb = options.action === "connect" ? "Connected" : "Disconnected";
661
671
  if (!result.changed.length && !hookScriptPaths.length) {
@@ -677,6 +687,7 @@ export async function run(argv = process.argv.slice(2), env = process.env) {
677
687
  }
678
688
  if (manageHooks) {
679
689
  stdout.write(" hooks: session capsule, per-turn recall, conflict alerts, session capture (needs python3 on PATH)\n");
690
+ stdout.write(" commands: /forget (the recall dial) · /forget-settings (status, doctor, cloud usage)\n");
680
691
  }
681
692
  if (options.hosted && !options.scope) {
682
693
  stdout.write(
package/src/hooks.js CHANGED
@@ -258,3 +258,57 @@ export async function removeHookScripts(hooksDir) {
258
258
  }
259
259
  return removed;
260
260
  }
261
+
262
+ // --- slash commands (/forget, /forget-settings) — the dial in the editor ---
263
+ //
264
+ // Same file-ownership rule as settings.json: a command file without our
265
+ // marker is the user's (hand-written or edited with the marker removed) and
266
+ // is never touched — not on connect, not on disconnect.
267
+
268
+ export const COMMAND_ASSETS = ["forget.md", "forget-settings.md"];
269
+ export const COMMAND_MARKER = "forget-connect:command";
270
+
271
+ export function commandsDirFor(options = {}) {
272
+ const env = options.env ?? process.env;
273
+ const home = options.home ?? env.HOME ?? env.USERPROFILE ?? os.homedir();
274
+ return path.join(home, ".claude", "commands");
275
+ }
276
+
277
+ function commandAssetsDir() {
278
+ return fileURLToPath(new URL("../assets/commands/", import.meta.url));
279
+ }
280
+
281
+ export async function installCommandAssets(commandsDir) {
282
+ const written = [];
283
+ for (const name of COMMAND_ASSETS) {
284
+ const target = path.join(commandsDir, name);
285
+ let existing = null;
286
+ try {
287
+ existing = await readFile(target, "utf8");
288
+ } catch (error) {
289
+ if (!error || error.code !== "ENOENT") throw error;
290
+ }
291
+ if (existing !== null && !existing.includes(COMMAND_MARKER)) continue;
292
+ const content = await readFile(path.join(commandAssetsDir(), name), "utf8");
293
+ if (existing === content) continue;
294
+ await atomicWrite(target, content, 0o644);
295
+ written.push(target);
296
+ }
297
+ return written;
298
+ }
299
+
300
+ export async function removeCommandAssets(commandsDir) {
301
+ const removed = [];
302
+ for (const name of COMMAND_ASSETS) {
303
+ const target = path.join(commandsDir, name);
304
+ try {
305
+ const existing = await readFile(target, "utf8");
306
+ if (!existing.includes(COMMAND_MARKER)) continue;
307
+ await unlink(target);
308
+ removed.push(target);
309
+ } catch (error) {
310
+ if (!error || error.code !== "ENOENT") throw error;
311
+ }
312
+ }
313
+ return removed;
314
+ }