forget-connect 0.5.1 → 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".
@@ -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.1",
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
+ }