gnosys 5.13.1 → 5.14.0
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/dist/cli.js +13 -1
- package/dist/index.js +2 -2
- package/dist/lib/projectIdentity.js +48 -14
- package/dist/lib/recall.d.ts +4 -3
- package/dist/lib/recall.js +12 -1
- package/dist/lib/recallCommand.js +24 -9
- package/dist/lib/recallHookCommand.d.ts +30 -0
- package/dist/lib/recallHookCommand.js +85 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1307,6 +1307,15 @@ program
|
|
|
1307
1307
|
const { runRecallCommand } = await import("./lib/recallCommand.js");
|
|
1308
1308
|
await runRecallCommand(query, opts);
|
|
1309
1309
|
});
|
|
1310
|
+
// ─── gnosys recall-hook ──────────────────────────────────────────────────
|
|
1311
|
+
program
|
|
1312
|
+
.command("recall-hook")
|
|
1313
|
+
.description("Claude Code hook entry — reads the hook event JSON from stdin and prints a <gnosys-recall> context block. Wired automatically by gnosys init into UserPromptSubmit + SessionStart.")
|
|
1314
|
+
.option("--limit <n>", "Max memories to inject (default from config)")
|
|
1315
|
+
.action(async (opts) => {
|
|
1316
|
+
const { runRecallHookCommand } = await import("./lib/recallHookCommand.js");
|
|
1317
|
+
await runRecallHookCommand(opts);
|
|
1318
|
+
});
|
|
1310
1319
|
// ─── gnosys audit ────────────────────────────────────────────────────────
|
|
1311
1320
|
program
|
|
1312
1321
|
.command("audit")
|
|
@@ -1855,10 +1864,13 @@ if (!isTestEnv()) {
|
|
|
1855
1864
|
// console.log during boot corrupts the protocol and the host (Grok, Codex,
|
|
1856
1865
|
// etc.) sees the server as [unavailable]. Suppress the nag in serve mode.
|
|
1857
1866
|
const isServeCmd = process.argv.slice(2).some(a => a === "serve");
|
|
1867
|
+
// v5.14.0: recall-hook runs on every Claude Code prompt — keep its
|
|
1868
|
+
// stderr quiet too so hook error logs stay clean.
|
|
1869
|
+
const isHookCmd = process.argv.slice(2).some(a => a === "recall-hook");
|
|
1858
1870
|
// v5.9.3 Phase H: fire on any mismatch (upgrade OR downgrade).
|
|
1859
1871
|
const mismatch = lastVersion !== null && lastVersion !== undefined &&
|
|
1860
1872
|
compareSemver(currentVersion, lastVersion) !== 0;
|
|
1861
|
-
if (mismatch && !isUpgradeCmd && !isSetupSyncCmd && !isServeCmd) {
|
|
1873
|
+
if (mismatch && !isUpgradeCmd && !isSetupSyncCmd && !isServeCmd && !isHookCmd) {
|
|
1862
1874
|
// v5.9.3 Phase H: emit on STDERR (was stdout). Safer invariant per
|
|
1863
1875
|
// deci-045 — stdout is reserved for command output.
|
|
1864
1876
|
const isMajorOrMinor = (() => {
|
package/dist/index.js
CHANGED
|
@@ -2246,7 +2246,7 @@ async function reindexAllStores() {
|
|
|
2246
2246
|
//
|
|
2247
2247
|
// Priority 1 + audience: assistant = hosts inject this before every message.
|
|
2248
2248
|
regResource("gnosys_recall", "gnosys://recall", {
|
|
2249
|
-
description: "
|
|
2249
|
+
description: "Top-memory injection block for hosts that read MCP resources. Returns a <gnosys-recall> block with [[wikilinks]] and relevance scores. NOTE: most hosts (Claude Code, Cursor) do NOT auto-read MCP resources per turn — for true automatic injection use the hooks `gnosys init` installs (UserPromptSubmit/SessionStart → `gnosys recall-hook`). Configure aggressiveness in gnosys.json: recall.aggressive (default: true).",
|
|
2250
2250
|
mimeType: "text/markdown",
|
|
2251
2251
|
annotations: {
|
|
2252
2252
|
audience: ["assistant"],
|
|
@@ -2294,7 +2294,7 @@ regResource("gnosys_recall", "gnosys://recall", {
|
|
|
2294
2294
|
// ─── Tool: gnosys_recall (query-specific fallback) ──────────────────────
|
|
2295
2295
|
// For hosts that don't support MCP Resources, or when the agent wants to
|
|
2296
2296
|
// recall memories for a specific query. The resource above is preferred.
|
|
2297
|
-
regTool("gnosys_recall", "Fast memory recall — inject relevant memories as context. Returns <gnosys-recall> block. In aggressive mode (default), always returns top memories even at medium relevance.
|
|
2297
|
+
regTool("gnosys_recall", "Fast memory recall — inject relevant memories as context. Returns <gnosys-recall> block. In aggressive mode (default), always returns top memories even at medium relevance. A wildcard query ('*') returns the top memories by reinforcement/confidence/recency. Hosts with gnosys hooks installed (via gnosys init) already get this automatically per prompt.", {
|
|
2298
2298
|
query: z
|
|
2299
2299
|
.string()
|
|
2300
2300
|
.describe("What the agent is currently working on. Use keywords. Example: 'auth JWT middleware' or 'database migration schema'"),
|
|
@@ -202,29 +202,61 @@ export async function configureClaudeCode(projectDir) {
|
|
|
202
202
|
catch {
|
|
203
203
|
// No settings yet
|
|
204
204
|
}
|
|
205
|
-
//
|
|
205
|
+
// v5.14.0: the hook command is `gnosys recall-hook` — it reads the hook
|
|
206
|
+
// event JSON from stdin (per the Claude Code hooks contract) and prints
|
|
207
|
+
// a <gnosys-recall> block, which Claude Code adds to the model context.
|
|
208
|
+
// Pre-5.14 installs wrote `gnosys recall --query ... --projectRoot ...`,
|
|
209
|
+
// options that never existed — the hook failed silently on every
|
|
210
|
+
// session start. Detect and heal that shape below.
|
|
211
|
+
const hookCommand = "gnosys recall-hook 2>/dev/null || true";
|
|
206
212
|
const gnosysHook = {
|
|
207
213
|
type: "command",
|
|
208
|
-
command:
|
|
214
|
+
command: hookCommand,
|
|
209
215
|
timeout: 10,
|
|
210
216
|
};
|
|
211
|
-
|
|
217
|
+
const isGnosysHookCmd = (h) => typeof h.command === "string" &&
|
|
218
|
+
(h.command.includes("gnosys recall-hook") ||
|
|
219
|
+
// the exact broken pre-5.14 shape only — a user's own `gnosys recall
|
|
220
|
+
// <topic>` hook must not count as "already configured"
|
|
221
|
+
h.command.includes("gnosys recall --query"));
|
|
222
|
+
const isBrokenLegacyCmd = (h) => typeof h.command === "string" && h.command.includes("gnosys recall --query");
|
|
223
|
+
// Merge into existing hooks without clobbering other entries
|
|
212
224
|
const hooks = (settings.hooks || {});
|
|
225
|
+
let changed = false;
|
|
226
|
+
// Heal the broken legacy command wherever it appears
|
|
227
|
+
for (const eventName of Object.keys(hooks)) {
|
|
228
|
+
for (const entry of (hooks[eventName] || [])) {
|
|
229
|
+
const entryHooks = (entry.hooks || []);
|
|
230
|
+
for (const h of entryHooks) {
|
|
231
|
+
if (isBrokenLegacyCmd(h)) {
|
|
232
|
+
h.command = hookCommand;
|
|
233
|
+
changed = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// SessionStart: top memories at startup/resume/compact
|
|
213
239
|
const sessionStartEntries = (hooks.SessionStart || []);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return entryHooks.some((h) => typeof h.command === "string" && h.command.includes("gnosys recall"));
|
|
218
|
-
});
|
|
219
|
-
if (!hasGnosysHook) {
|
|
220
|
-
sessionStartEntries.push({
|
|
221
|
-
matcher: "startup|resume|compact",
|
|
222
|
-
hooks: [gnosysHook],
|
|
223
|
-
});
|
|
240
|
+
const hasSessionHook = sessionStartEntries.some((entry) => (entry.hooks || []).some(isGnosysHookCmd));
|
|
241
|
+
if (!hasSessionHook) {
|
|
242
|
+
sessionStartEntries.push({ matcher: "startup|resume|compact", hooks: [gnosysHook] });
|
|
224
243
|
hooks.SessionStart = sessionStartEntries;
|
|
244
|
+
changed = true;
|
|
245
|
+
}
|
|
246
|
+
// UserPromptSubmit: per-prompt recall with the prompt text as the query —
|
|
247
|
+
// this is the "automatic memory injection" path (no matcher: always fires)
|
|
248
|
+
const promptEntries = (hooks.UserPromptSubmit || []);
|
|
249
|
+
const hasPromptHook = promptEntries.some((entry) => (entry.hooks || []).some(isGnosysHookCmd));
|
|
250
|
+
if (!hasPromptHook) {
|
|
251
|
+
promptEntries.push({ hooks: [gnosysHook] });
|
|
252
|
+
hooks.UserPromptSubmit = promptEntries;
|
|
253
|
+
changed = true;
|
|
254
|
+
}
|
|
255
|
+
if (changed) {
|
|
225
256
|
settings.hooks = hooks;
|
|
226
257
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
227
258
|
}
|
|
259
|
+
const hasGnosysHook = !changed;
|
|
228
260
|
// v5.8.4: also register the MCP server itself (not just the recall hook),
|
|
229
261
|
// so `gnosys init` is a one-stop shop. Previously the user had to ALSO
|
|
230
262
|
// run `gnosys setup` for agents to actually call gnosys MCP tools.
|
|
@@ -234,7 +266,9 @@ export async function configureClaudeCode(projectDir) {
|
|
|
234
266
|
configured: true,
|
|
235
267
|
filePath: settingsPath,
|
|
236
268
|
details: [
|
|
237
|
-
hasGnosysHook
|
|
269
|
+
hasGnosysHook
|
|
270
|
+
? "Recall hooks already configured"
|
|
271
|
+
: "Recall hooks configured (SessionStart + UserPromptSubmit → gnosys recall-hook)",
|
|
238
272
|
mcpResult.success ? mcpResult.message : `MCP register skipped: ${mcpResult.message}`,
|
|
239
273
|
].join("; "),
|
|
240
274
|
};
|
package/dist/lib/recall.d.ts
CHANGED
|
@@ -62,9 +62,10 @@ interface RecallMemory {
|
|
|
62
62
|
*/
|
|
63
63
|
export declare function recall(query: string, options: {
|
|
64
64
|
limit?: number;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
/** Optional in v5.14: callers on the DB fast path (hooks) skip the file-store deps. */
|
|
66
|
+
search?: GnosysSearch;
|
|
67
|
+
resolver?: GnosysResolver;
|
|
68
|
+
storePath?: string;
|
|
68
69
|
traceId?: string;
|
|
69
70
|
recallConfig?: RecallConfig;
|
|
70
71
|
/** v2.0: When provided, recall uses SQLite directly — no filesystem reads */
|
package/dist/lib/recall.js
CHANGED
|
@@ -72,6 +72,17 @@ export async function recall(query, options) {
|
|
|
72
72
|
return recallFromDb(query, options.gnosysDb, limit, cfg, options.traceId, options.pendingOverlay);
|
|
73
73
|
}
|
|
74
74
|
// ─── v1.x legacy path (filesystem + search.db) ────────────────────
|
|
75
|
+
// v5.14: DB-only callers (recall-hook) pass no file-store deps; without
|
|
76
|
+
// them and without a central DB there is nothing to search.
|
|
77
|
+
if (!options.search || !options.resolver) {
|
|
78
|
+
return {
|
|
79
|
+
memories: [],
|
|
80
|
+
totalActive: 0,
|
|
81
|
+
totalArchived: 0,
|
|
82
|
+
recallTimeMs: Math.round((performance.now() - start) * 100) / 100,
|
|
83
|
+
aggressive: cfg.aggressive,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
75
86
|
const memories = [];
|
|
76
87
|
// Step 1: Fast keyword search on active memories (FTS5 — sub-10ms)
|
|
77
88
|
const fetchLimit = Math.max(limit * 2, 15);
|
|
@@ -96,7 +107,7 @@ export async function recall(query, options) {
|
|
|
96
107
|
}
|
|
97
108
|
// Step 2: Archive fallback if active results are thin
|
|
98
109
|
let totalArchived = 0;
|
|
99
|
-
if (memories.length < limit) {
|
|
110
|
+
if (memories.length < limit && options.storePath) {
|
|
100
111
|
try {
|
|
101
112
|
const archive = new GnosysArchive(options.storePath);
|
|
102
113
|
if (archive.isAvailable()) {
|
|
@@ -70,7 +70,7 @@ export async function runRecallCommand(query, opts) {
|
|
|
70
70
|
}
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
|
-
//
|
|
73
|
+
// Default recall path
|
|
74
74
|
const resolver = new GnosysResolver();
|
|
75
75
|
await resolver.resolve();
|
|
76
76
|
const stores = resolver.getStores();
|
|
@@ -91,14 +91,29 @@ export async function runRecallCommand(query, opts) {
|
|
|
91
91
|
// Build search index
|
|
92
92
|
const search = new GnosysSearch(storePath);
|
|
93
93
|
await search.addStoreMemories(stores[0].store);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
// v5.14.0: recall from the central DB (the brain) like the MCP tool
|
|
95
|
+
// does — the CLI previously only searched the project file store, so
|
|
96
|
+
// `gnosys recall` (and the SessionStart hook built on it) returned
|
|
97
|
+
// nothing on DB-only installs. Snapshot-aware via clientReadResolve;
|
|
98
|
+
// falls back to the file-store path when no central DB exists.
|
|
99
|
+
const { resolveClientRead } = await import("./clientReadResolve.js");
|
|
100
|
+
const clientRead = resolveClientRead();
|
|
101
|
+
let result;
|
|
102
|
+
try {
|
|
103
|
+
result = await recall(query, {
|
|
104
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
105
|
+
search,
|
|
106
|
+
resolver,
|
|
107
|
+
storePath,
|
|
108
|
+
traceId: opts.traceId,
|
|
109
|
+
recallConfig,
|
|
110
|
+
gnosysDb: clientRead?.db,
|
|
111
|
+
pendingOverlay: clientRead?.pendingOverlay,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
clientRead?.release();
|
|
116
|
+
}
|
|
102
117
|
if (opts.json) {
|
|
103
118
|
console.log(JSON.stringify(result, null, 2));
|
|
104
119
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gnosys recall-hook` — Claude Code hook entry point (v5.14.0).
|
|
3
|
+
*
|
|
4
|
+
* This is what makes "automatic memory injection" real: Claude Code never
|
|
5
|
+
* auto-reads MCP resources, but its hooks contract
|
|
6
|
+
* (code.claude.com/docs/en/hooks.md) adds a hook's plain stdout to the
|
|
7
|
+
* model context on exit 0. `gnosys init` wires this command into
|
|
8
|
+
* UserPromptSubmit (every prompt, query = the prompt text) and
|
|
9
|
+
* SessionStart (startup/resume/compact, wildcard = top memories).
|
|
10
|
+
*
|
|
11
|
+
* Contract obligations:
|
|
12
|
+
* - FAST: runs on every prompt. DB fast path only — no file stores, no
|
|
13
|
+
* embeddings, no model loads. (~150ms cold including node startup.)
|
|
14
|
+
* - NEVER breaks the prompt: always exit 0; every failure is silent.
|
|
15
|
+
* - Empty stdout when there is nothing to inject (no noise strings).
|
|
16
|
+
* - Output stays well under the 10,000-char hook cap.
|
|
17
|
+
*/
|
|
18
|
+
export type HookEvent = {
|
|
19
|
+
hook_event_name?: string;
|
|
20
|
+
prompt_text?: string;
|
|
21
|
+
/** Older docs name the field `prompt`; accept both. */
|
|
22
|
+
prompt?: string;
|
|
23
|
+
source?: string;
|
|
24
|
+
cwd?: string;
|
|
25
|
+
};
|
|
26
|
+
/** Parse the hook event JSON into a recall query ("*" = top memories). */
|
|
27
|
+
export declare function hookQueryFromStdin(raw: string): string;
|
|
28
|
+
export declare function runRecallHookCommand(opts?: {
|
|
29
|
+
limit?: string;
|
|
30
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gnosys recall-hook` — Claude Code hook entry point (v5.14.0).
|
|
3
|
+
*
|
|
4
|
+
* This is what makes "automatic memory injection" real: Claude Code never
|
|
5
|
+
* auto-reads MCP resources, but its hooks contract
|
|
6
|
+
* (code.claude.com/docs/en/hooks.md) adds a hook's plain stdout to the
|
|
7
|
+
* model context on exit 0. `gnosys init` wires this command into
|
|
8
|
+
* UserPromptSubmit (every prompt, query = the prompt text) and
|
|
9
|
+
* SessionStart (startup/resume/compact, wildcard = top memories).
|
|
10
|
+
*
|
|
11
|
+
* Contract obligations:
|
|
12
|
+
* - FAST: runs on every prompt. DB fast path only — no file stores, no
|
|
13
|
+
* embeddings, no model loads. (~150ms cold including node startup.)
|
|
14
|
+
* - NEVER breaks the prompt: always exit 0; every failure is silent.
|
|
15
|
+
* - Empty stdout when there is nothing to inject (no noise strings).
|
|
16
|
+
* - Output stays well under the 10,000-char hook cap.
|
|
17
|
+
*/
|
|
18
|
+
/** Max chars of the user prompt used as the recall query. */
|
|
19
|
+
const MAX_QUERY_CHARS = 400;
|
|
20
|
+
/** Keep comfortably under Claude Code's 10k hook-output cap. */
|
|
21
|
+
const MAX_OUTPUT_CHARS = 8_000;
|
|
22
|
+
/** Parse the hook event JSON into a recall query ("*" = top memories). */
|
|
23
|
+
export function hookQueryFromStdin(raw) {
|
|
24
|
+
try {
|
|
25
|
+
const evt = JSON.parse(raw);
|
|
26
|
+
const prompt = evt.prompt_text ?? evt.prompt;
|
|
27
|
+
if (typeof prompt === "string" && prompt.trim().length > 0) {
|
|
28
|
+
return prompt.trim().slice(0, MAX_QUERY_CHARS);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Not JSON (manual invocation / future contract change) — wildcard.
|
|
33
|
+
}
|
|
34
|
+
return "*";
|
|
35
|
+
}
|
|
36
|
+
async function readStdin(timeoutMs) {
|
|
37
|
+
if (process.stdin.isTTY)
|
|
38
|
+
return "";
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
let data = "";
|
|
41
|
+
const timer = setTimeout(() => resolve(data), timeoutMs);
|
|
42
|
+
timer.unref?.();
|
|
43
|
+
process.stdin.setEncoding("utf8");
|
|
44
|
+
process.stdin.on("data", (chunk) => {
|
|
45
|
+
data += chunk;
|
|
46
|
+
});
|
|
47
|
+
process.stdin.on("end", () => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
resolve(data);
|
|
50
|
+
});
|
|
51
|
+
process.stdin.on("error", () => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
resolve(data);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export async function runRecallHookCommand(opts = {}) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = await readStdin(1_000);
|
|
60
|
+
const query = hookQueryFromStdin(raw);
|
|
61
|
+
const { resolveClientRead } = await import("./clientReadResolve.js");
|
|
62
|
+
const clientRead = resolveClientRead();
|
|
63
|
+
if (!clientRead)
|
|
64
|
+
return; // no central DB — inject nothing, exit 0
|
|
65
|
+
try {
|
|
66
|
+
const { recall, formatRecall } = await import("./recall.js");
|
|
67
|
+
const result = await recall(query, {
|
|
68
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
69
|
+
gnosysDb: clientRead.db,
|
|
70
|
+
pendingOverlay: clientRead.pendingOverlay,
|
|
71
|
+
traceId: "claude-code-hook",
|
|
72
|
+
});
|
|
73
|
+
if (result.memories.length === 0)
|
|
74
|
+
return; // empty stdout = no injection
|
|
75
|
+
const block = formatRecall(result);
|
|
76
|
+
process.stdout.write(block.length > MAX_OUTPUT_CHARS ? `${block.slice(0, MAX_OUTPUT_CHARS)}\n</gnosys-recall>\n` : `${block}\n`);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
clientRead.release();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Never fail the user's prompt over memory recall.
|
|
84
|
+
}
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gnosys",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.14.0",
|
|
4
4
|
"description": "Gnosys — Persistent Memory for AI Agents. Sandbox-first runtime, central SQLite brain, federated search, Dream Mode, Web Knowledge Base, Obsidian export.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|