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
|
-
|
|
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
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
|
+
}
|