gnosys 5.13.1 → 5.15.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/dist/cli.js +19 -1
- package/dist/index.js +4 -3
- package/dist/lib/ask.js +5 -1
- package/dist/lib/askCommand.js +4 -1
- package/dist/lib/chat/llmTurn.d.ts +9 -0
- package/dist/lib/chat/llmTurn.js +23 -6
- package/dist/lib/config.d.ts +0 -2
- package/dist/lib/config.js +25 -8
- package/dist/lib/dreamLaunchd.d.ts +65 -0
- package/dist/lib/dreamLaunchd.js +207 -0
- package/dist/lib/hybridSearchCommand.js +4 -1
- package/dist/lib/importCommand.js +5 -1
- package/dist/lib/interactiveGuard.d.ts +17 -0
- package/dist/lib/interactiveGuard.js +26 -0
- 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/dist/lib/setup.js +12 -1
- package/dist/lib/setupKeys.js +3 -0
- package/dist/lib/statusCommand.js +16 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1097,6 +1097,12 @@ program
|
|
|
1097
1097
|
console.error(`\nCould not write upgrade marker: ${err instanceof Error ? err.message : err}`);
|
|
1098
1098
|
console.error(`Running MCP servers will need to be restarted manually.`);
|
|
1099
1099
|
}
|
|
1100
|
+
// v5.15: a Node upgrade moves the node path hardcoded in the dream
|
|
1101
|
+
// LaunchAgent plist, silently killing the scheduler. Repair it here.
|
|
1102
|
+
const { repairDreamLaunchAgentAfterUpgrade } = await import("./lib/dreamLaunchd.js");
|
|
1103
|
+
const dreamRepairLine = await repairDreamLaunchAgentAfterUpgrade();
|
|
1104
|
+
if (dreamRepairLine)
|
|
1105
|
+
console.log(`\n${dreamRepairLine}`);
|
|
1100
1106
|
if (opts.sync === false || opts.yes) {
|
|
1101
1107
|
console.log(`\nDone. Run 'gnosys setup sync-projects' when you're ready to refresh registered projects.`);
|
|
1102
1108
|
return;
|
|
@@ -1307,6 +1313,15 @@ program
|
|
|
1307
1313
|
const { runRecallCommand } = await import("./lib/recallCommand.js");
|
|
1308
1314
|
await runRecallCommand(query, opts);
|
|
1309
1315
|
});
|
|
1316
|
+
// ─── gnosys recall-hook ──────────────────────────────────────────────────
|
|
1317
|
+
program
|
|
1318
|
+
.command("recall-hook")
|
|
1319
|
+
.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.")
|
|
1320
|
+
.option("--limit <n>", "Max memories to inject (default from config)")
|
|
1321
|
+
.action(async (opts) => {
|
|
1322
|
+
const { runRecallHookCommand } = await import("./lib/recallHookCommand.js");
|
|
1323
|
+
await runRecallHookCommand(opts);
|
|
1324
|
+
});
|
|
1310
1325
|
// ─── gnosys audit ────────────────────────────────────────────────────────
|
|
1311
1326
|
program
|
|
1312
1327
|
.command("audit")
|
|
@@ -1855,10 +1870,13 @@ if (!isTestEnv()) {
|
|
|
1855
1870
|
// console.log during boot corrupts the protocol and the host (Grok, Codex,
|
|
1856
1871
|
// etc.) sees the server as [unavailable]. Suppress the nag in serve mode.
|
|
1857
1872
|
const isServeCmd = process.argv.slice(2).some(a => a === "serve");
|
|
1873
|
+
// v5.14.0: recall-hook runs on every Claude Code prompt — keep its
|
|
1874
|
+
// stderr quiet too so hook error logs stay clean.
|
|
1875
|
+
const isHookCmd = process.argv.slice(2).some(a => a === "recall-hook");
|
|
1858
1876
|
// v5.9.3 Phase H: fire on any mismatch (upgrade OR downgrade).
|
|
1859
1877
|
const mismatch = lastVersion !== null && lastVersion !== undefined &&
|
|
1860
1878
|
compareSemver(currentVersion, lastVersion) !== 0;
|
|
1861
|
-
if (mismatch && !isUpgradeCmd && !isSetupSyncCmd && !isServeCmd) {
|
|
1879
|
+
if (mismatch && !isUpgradeCmd && !isSetupSyncCmd && !isServeCmd && !isHookCmd) {
|
|
1862
1880
|
// v5.9.3 Phase H: emit on STDERR (was stdout). Safer invariant per
|
|
1863
1881
|
// deci-045 — stdout is reserved for command output.
|
|
1864
1882
|
const isMajorOrMinor = (() => {
|
package/dist/index.js
CHANGED
|
@@ -1703,7 +1703,8 @@ regTool("gnosys_import", "Bulk import structured data (CSV, JSON, JSONL) into Gn
|
|
|
1703
1703
|
skipExisting,
|
|
1704
1704
|
limit,
|
|
1705
1705
|
offset,
|
|
1706
|
-
|
|
1706
|
+
// v5.15: explicit param wins; otherwise config importConcurrency.
|
|
1707
|
+
concurrency: concurrency ?? ctx.config?.importConcurrency,
|
|
1707
1708
|
batchCommit: true,
|
|
1708
1709
|
});
|
|
1709
1710
|
// Reindex after import
|
|
@@ -2246,7 +2247,7 @@ async function reindexAllStores() {
|
|
|
2246
2247
|
//
|
|
2247
2248
|
// Priority 1 + audience: assistant = hosts inject this before every message.
|
|
2248
2249
|
regResource("gnosys_recall", "gnosys://recall", {
|
|
2249
|
-
description: "
|
|
2250
|
+
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
2251
|
mimeType: "text/markdown",
|
|
2251
2252
|
annotations: {
|
|
2252
2253
|
audience: ["assistant"],
|
|
@@ -2294,7 +2295,7 @@ regResource("gnosys_recall", "gnosys://recall", {
|
|
|
2294
2295
|
// ─── Tool: gnosys_recall (query-specific fallback) ──────────────────────
|
|
2295
2296
|
// For hosts that don't support MCP Resources, or when the agent wants to
|
|
2296
2297
|
// 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.
|
|
2298
|
+
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
2299
|
query: z
|
|
2299
2300
|
.string()
|
|
2300
2301
|
.describe("What the agent is currently working on. Use keywords. Example: 'auth JWT middleware' or 'database migration schema'"),
|
package/dist/lib/ask.js
CHANGED
|
@@ -289,7 +289,11 @@ export class GnosysAsk {
|
|
|
289
289
|
}
|
|
290
290
|
return usedArchiveIds;
|
|
291
291
|
}
|
|
292
|
-
catch {
|
|
292
|
+
catch (err) {
|
|
293
|
+
// Degrade gracefully, but leave a trace on stderr — a hard dearchive
|
|
294
|
+
// failure previously looked identical to "no archive hits"
|
|
295
|
+
// (sprint 2026-07-02).
|
|
296
|
+
console.error(`gnosys: dearchive failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
293
297
|
return [];
|
|
294
298
|
}
|
|
295
299
|
}
|
package/dist/lib/askCommand.js
CHANGED
|
@@ -132,7 +132,10 @@ export async function runAskCommand(getResolver, question, opts) {
|
|
|
132
132
|
const writeTarget = resolver.getWriteTarget();
|
|
133
133
|
if (writeTarget) {
|
|
134
134
|
const { GnosysMaintenanceEngine } = await import("./maintenance.js");
|
|
135
|
-
await GnosysMaintenanceEngine.reinforceBatch(writeTarget.store, result.sources.map((s) => s.relativePath)).catch(() => {
|
|
135
|
+
await GnosysMaintenanceEngine.reinforceBatch(writeTarget.store, result.sources.map((s) => s.relativePath)).catch((err) => {
|
|
136
|
+
// Best-effort, but don't be fully silent (sprint 2026-07-02).
|
|
137
|
+
console.error(`gnosys: reinforcement skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
138
|
+
});
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
141
|
}
|
|
@@ -42,6 +42,15 @@ export interface LLMTurnResult {
|
|
|
42
42
|
result: string;
|
|
43
43
|
}>;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Build the system prompt for a chat turn.
|
|
47
|
+
*
|
|
48
|
+
* v5.15: wires two previously-unread config knobs —
|
|
49
|
+
* `chat.systemPromptPrefix` (prepended before the base prompt) and
|
|
50
|
+
* `chat.toolsEnabled` (false omits the gnosys-tool addendum entirely).
|
|
51
|
+
* Exported for tests.
|
|
52
|
+
*/
|
|
53
|
+
export declare function composeSystemPrompt(config: GnosysConfig, recalled: RecalledMemory[] | undefined): string;
|
|
45
54
|
/**
|
|
46
55
|
* Run a single turn against the LLM. Loops if the assistant emits
|
|
47
56
|
* gnosys-tool fences: each tool is executed in-process, the result is
|
package/dist/lib/chat/llmTurn.js
CHANGED
|
@@ -12,9 +12,23 @@ import { CHOOSE_SYSTEM_PROMPT_ADDENDUM } from "./choose.js";
|
|
|
12
12
|
import { buildToolsSystemPrompt, findTool } from "./tools.js";
|
|
13
13
|
import { extractToolFences } from "./toolFence.js";
|
|
14
14
|
const BASE_SYSTEM_PROMPT = `You are an assistant inside the Gnosys terminal chat — a memory-aware REPL. The user has persistent memory across sessions; relevant memories are injected as <memory id="..."> blocks before their question. Cite memory IDs in square brackets like [deci-037] when you use them. Be concise and direct. Markdown renders.${CHOOSE_SYSTEM_PROMPT_ADDENDUM}`;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Build the system prompt for a chat turn.
|
|
17
|
+
*
|
|
18
|
+
* v5.15: wires two previously-unread config knobs —
|
|
19
|
+
* `chat.systemPromptPrefix` (prepended before the base prompt) and
|
|
20
|
+
* `chat.toolsEnabled` (false omits the gnosys-tool addendum entirely).
|
|
21
|
+
* Exported for tests.
|
|
22
|
+
*/
|
|
23
|
+
export function composeSystemPrompt(config, recalled) {
|
|
24
|
+
const toolsEnabled = config.chat?.toolsEnabled !== false;
|
|
25
|
+
let base = toolsEnabled
|
|
26
|
+
? `${BASE_SYSTEM_PROMPT}\n${buildToolsSystemPrompt()}`
|
|
27
|
+
: BASE_SYSTEM_PROMPT;
|
|
28
|
+
const prefix = config.chat?.systemPromptPrefix;
|
|
29
|
+
if (typeof prefix === "string" && prefix.trim().length > 0) {
|
|
30
|
+
base = `${prefix.trim()}\n\n${base}`;
|
|
31
|
+
}
|
|
18
32
|
if (!recalled || recalled.length === 0)
|
|
19
33
|
return base;
|
|
20
34
|
return `${base}\n\n${formatRecallForPrompt(recalled)}`;
|
|
@@ -47,8 +61,11 @@ export async function runTurn(config, opts) {
|
|
|
47
61
|
// falls back to defaultProvider when no chat override is set, so existing
|
|
48
62
|
// installs keep working without a chat-specific config.
|
|
49
63
|
const provider = getLLMProvider(config, "chat");
|
|
50
|
-
const system = composeSystemPrompt(opts.recalled);
|
|
51
|
-
|
|
64
|
+
const system = composeSystemPrompt(config, opts.recalled);
|
|
65
|
+
// v5.15: when chat.toolsEnabled === false the first response is final —
|
|
66
|
+
// no tool-fence execution loop.
|
|
67
|
+
const toolsEnabled = config.chat?.toolsEnabled !== false;
|
|
68
|
+
const maxIterations = toolsEnabled ? (opts.maxToolIterations ?? 4) : 1;
|
|
52
69
|
const toolCalls = [];
|
|
53
70
|
let toolPreamble;
|
|
54
71
|
let combinedText = "";
|
|
@@ -64,7 +81,7 @@ export async function runTurn(config, opts) {
|
|
|
64
81
|
combinedText += (combinedText ? "\n\n" : "") + chunk;
|
|
65
82
|
// Look for tool fences; if none, this iteration's chunk IS the answer.
|
|
66
83
|
const extraction = extractToolFences(chunk);
|
|
67
|
-
if (!extraction || extraction.calls.length === 0) {
|
|
84
|
+
if (!toolsEnabled || !extraction || extraction.calls.length === 0) {
|
|
68
85
|
break;
|
|
69
86
|
}
|
|
70
87
|
// Run each tool, accumulate results into the next prompt as a system block
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -183,7 +183,6 @@ export declare const GnosysConfigSchema: z.ZodObject<{
|
|
|
183
183
|
openai: "openai";
|
|
184
184
|
}>>;
|
|
185
185
|
defaultModel: z.ZodOptional<z.ZodString>;
|
|
186
|
-
bulkIngestionBatchSize: z.ZodDefault<z.ZodNumber>;
|
|
187
186
|
importConcurrency: z.ZodDefault<z.ZodNumber>;
|
|
188
187
|
autoCommit: z.ZodDefault<z.ZodBoolean>;
|
|
189
188
|
llmRetryAttempts: z.ZodDefault<z.ZodNumber>;
|
|
@@ -241,7 +240,6 @@ export declare const GnosysConfigSchema: z.ZodObject<{
|
|
|
241
240
|
}, z.core.$strip>>;
|
|
242
241
|
chat: z.ZodDefault<z.ZodObject<{
|
|
243
242
|
toolsEnabled: z.ZodDefault<z.ZodBoolean>;
|
|
244
|
-
autoSummarizeAfterTurns: z.ZodDefault<z.ZodNumber>;
|
|
245
243
|
systemPromptPrefix: z.ZodDefault<z.ZodString>;
|
|
246
244
|
}, z.core.$strip>>;
|
|
247
245
|
multimodal: z.ZodDefault<z.ZodObject<{
|
package/dist/lib/config.js
CHANGED
|
@@ -137,9 +137,6 @@ const RecallConfigSchema = z.object({
|
|
|
137
137
|
const ChatConfigSchema = z.object({
|
|
138
138
|
/** Allow LLM to call gnosys tools via gnosys-tool fences (default: true). */
|
|
139
139
|
toolsEnabled: z.boolean().default(true),
|
|
140
|
-
/** Suggest /save-turn after N consecutive turns where no memory was written.
|
|
141
|
-
* 0 disables the nudge. Heuristic only — never writes without consent. */
|
|
142
|
-
autoSummarizeAfterTurns: z.number().int().min(0).default(0),
|
|
143
140
|
/** Prepended to the chat system prompt. Use for persona / style / domain hints. */
|
|
144
141
|
systemPromptPrefix: z.string().default(""),
|
|
145
142
|
});
|
|
@@ -210,8 +207,6 @@ export const GnosysConfigSchema = z.object({
|
|
|
210
207
|
.optional(),
|
|
211
208
|
/** @deprecated Use llm.anthropic.model or llm.ollama.model instead */
|
|
212
209
|
defaultModel: z.string().optional(),
|
|
213
|
-
/** Max records per batch commit during bulk import */
|
|
214
|
-
bulkIngestionBatchSize: z.number().int().min(1).max(10000).default(500),
|
|
215
210
|
/** Parallel LLM calls during import */
|
|
216
211
|
importConcurrency: z.number().int().min(1).max(20).default(5),
|
|
217
212
|
/** Enable auto-commit on every write (disable for manual git) */
|
|
@@ -259,7 +254,6 @@ export const GnosysConfigSchema = z.object({
|
|
|
259
254
|
/** Chat TUI — interactive chat configuration (v5.8.0) */
|
|
260
255
|
chat: ChatConfigSchema.default({
|
|
261
256
|
toolsEnabled: true,
|
|
262
|
-
autoSummarizeAfterTurns: 0,
|
|
263
257
|
systemPromptPrefix: "",
|
|
264
258
|
}),
|
|
265
259
|
/** Multimodal ingestion — PDF, audio, image, video processing */
|
|
@@ -483,6 +477,30 @@ function deepMergeConfig(base, override) {
|
|
|
483
477
|
}
|
|
484
478
|
return result;
|
|
485
479
|
}
|
|
480
|
+
// ─── Removed-knob deprecation warnings (v5.15) ──────────────────────────
|
|
481
|
+
//
|
|
482
|
+
// These knobs were documented but never read by any code ("settings that
|
|
483
|
+
// lie"), so v5.15 removed them from the schema. Zod strips unknown keys on
|
|
484
|
+
// parse, so old config files keep loading — but warn once per process (on
|
|
485
|
+
// stderr only; MCP serve stdout must stay clean for JSON-RPC).
|
|
486
|
+
const warnedRemovedKeys = new Set();
|
|
487
|
+
function warnRemovedConfigKeys(raw) {
|
|
488
|
+
const found = [];
|
|
489
|
+
if ("bulkIngestionBatchSize" in raw)
|
|
490
|
+
found.push("bulkIngestionBatchSize");
|
|
491
|
+
const chat = raw.chat;
|
|
492
|
+
if (typeof chat === "object" &&
|
|
493
|
+
chat !== null &&
|
|
494
|
+
"autoSummarizeAfterTurns" in chat) {
|
|
495
|
+
found.push("chat.autoSummarizeAfterTurns");
|
|
496
|
+
}
|
|
497
|
+
for (const key of found) {
|
|
498
|
+
if (warnedRemovedKeys.has(key))
|
|
499
|
+
continue;
|
|
500
|
+
warnedRemovedKeys.add(key);
|
|
501
|
+
console.error(`gnosys: config option "${key}" was removed in v5.15 and is ignored`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
486
504
|
export async function loadConfig(storePath) {
|
|
487
505
|
const configPath = path.join(storePath, "gnosys.json");
|
|
488
506
|
const globalPath = path.join(getGnosysHome(), "gnosys.json");
|
|
@@ -510,6 +528,7 @@ export async function loadConfig(storePath) {
|
|
|
510
528
|
if (raw === null) {
|
|
511
529
|
return DEFAULT_CONFIG;
|
|
512
530
|
}
|
|
531
|
+
warnRemovedConfigKeys(raw);
|
|
513
532
|
const migrated = migrateConfig(raw);
|
|
514
533
|
const parsed = GnosysConfigSchema.parse(migrated);
|
|
515
534
|
return applyDreamProviderInheritance(parsed, migrated);
|
|
@@ -579,7 +598,6 @@ export function generateConfigTemplate() {
|
|
|
579
598
|
},
|
|
580
599
|
},
|
|
581
600
|
taskModels: {},
|
|
582
|
-
bulkIngestionBatchSize: 500,
|
|
583
601
|
importConcurrency: 5,
|
|
584
602
|
autoCommit: true,
|
|
585
603
|
llmRetryAttempts: 3,
|
|
@@ -608,7 +626,6 @@ export function generateConfigTemplate() {
|
|
|
608
626
|
},
|
|
609
627
|
chat: {
|
|
610
628
|
toolsEnabled: true,
|
|
611
|
-
autoSummarizeAfterTurns: 0,
|
|
612
629
|
systemPromptPrefix: "",
|
|
613
630
|
},
|
|
614
631
|
}, null, 2);
|
|
@@ -1,2 +1,67 @@
|
|
|
1
|
+
export interface LaunchctlResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
message: string;
|
|
4
|
+
}
|
|
5
|
+
/** Pure helper: argv for loading a LaunchAgent plist. Exported for tests. */
|
|
6
|
+
export declare function launchctlLoadArgs(plistFile: string): string[];
|
|
7
|
+
/** Pure helper: argv for unloading a LaunchAgent plist. Exported for tests. */
|
|
8
|
+
export declare function launchctlUnloadArgs(plistFile: string): string[];
|
|
9
|
+
/**
|
|
10
|
+
* Pure helper: interpret a launchctl load failure. "Already loaded" is a
|
|
11
|
+
* success for our purposes (the agent is active). Exported for tests.
|
|
12
|
+
*/
|
|
13
|
+
export declare function interpretLaunchctlLoadError(stderr: string): LaunchctlResult;
|
|
14
|
+
/**
|
|
15
|
+
* Best-effort `launchctl load` of the dream LaunchAgent so scheduled runs
|
|
16
|
+
* start immediately instead of waiting for the next login. (v5.14.x sprint,
|
|
17
|
+
* pre-approved.) Never throws.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadDreamLaunchAgent(plistFile: string): LaunchctlResult;
|
|
20
|
+
/**
|
|
21
|
+
* Best-effort `launchctl unload` before removing the plist, so disabling
|
|
22
|
+
* Dream Mode deactivates the schedule immediately. Never throws.
|
|
23
|
+
*/
|
|
24
|
+
export declare function unloadDreamLaunchAgent(plistFile: string): LaunchctlResult;
|
|
1
25
|
export declare function installDreamLaunchAgent(): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Pure helper: extract the node and cli paths from a dream LaunchAgent
|
|
28
|
+
* plist body. Template shape (installDreamLaunchAgent): the first
|
|
29
|
+
* `<string>` is the Label, the second is the node binary path, the third
|
|
30
|
+
* is the cli path. Exported for tests.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseDreamPlistPaths(body: string): {
|
|
33
|
+
nodePath?: string;
|
|
34
|
+
cliPath?: string;
|
|
35
|
+
};
|
|
36
|
+
export interface DreamLaunchAgentHealth {
|
|
37
|
+
installed: boolean;
|
|
38
|
+
loaded: boolean;
|
|
39
|
+
nodeExists: boolean;
|
|
40
|
+
cliExists: boolean;
|
|
41
|
+
healthy: boolean;
|
|
42
|
+
problems: string[];
|
|
43
|
+
plistFile: string | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Health-check the dream LaunchAgent. The plist hardcodes absolute node +
|
|
47
|
+
* cli paths (e.g. ~/.nvm/versions/node/vX.Y.Z/bin/node), so a Node upgrade
|
|
48
|
+
* silently kills the scheduler — this detects that. Never throws.
|
|
49
|
+
*/
|
|
50
|
+
export declare function checkDreamLaunchAgent(): DreamLaunchAgentHealth;
|
|
51
|
+
/**
|
|
52
|
+
* Repair the dream LaunchAgent: rewrite the plist with the current
|
|
53
|
+
* process's node + cli paths, then reload it. Best-effort — never throws.
|
|
54
|
+
*/
|
|
55
|
+
export declare function repairDreamLaunchAgent(): {
|
|
56
|
+
ok: boolean;
|
|
57
|
+
message: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Post-upgrade hook: when Dream Mode is enabled and the LaunchAgent is
|
|
61
|
+
* installed but unhealthy (typically because a Node upgrade moved the
|
|
62
|
+
* hardcoded node path), repair it. Returns a printable status line, or
|
|
63
|
+
* null when nothing applies (non-darwin, dream disabled, agent healthy
|
|
64
|
+
* or not installed). Never throws.
|
|
65
|
+
*/
|
|
66
|
+
export declare function repairDreamLaunchAgentAfterUpgrade(): Promise<string | null>;
|
|
2
67
|
export declare function uninstallDreamLaunchAgent(): string | null;
|
package/dist/lib/dreamLaunchd.js
CHANGED
|
@@ -1,7 +1,73 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
1
2
|
import fs from "fs";
|
|
2
3
|
import os from "os";
|
|
3
4
|
import path from "path";
|
|
4
5
|
const LABEL = "com.gnosys.dream";
|
|
6
|
+
/** Pure helper: argv for loading a LaunchAgent plist. Exported for tests. */
|
|
7
|
+
export function launchctlLoadArgs(plistFile) {
|
|
8
|
+
return ["load", "-w", plistFile];
|
|
9
|
+
}
|
|
10
|
+
/** Pure helper: argv for unloading a LaunchAgent plist. Exported for tests. */
|
|
11
|
+
export function launchctlUnloadArgs(plistFile) {
|
|
12
|
+
return ["unload", plistFile];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Pure helper: interpret a launchctl load failure. "Already loaded" is a
|
|
16
|
+
* success for our purposes (the agent is active). Exported for tests.
|
|
17
|
+
*/
|
|
18
|
+
export function interpretLaunchctlLoadError(stderr) {
|
|
19
|
+
const text = stderr.trim();
|
|
20
|
+
if (/already loaded|service already loaded|Load failed: 5/i.test(text)) {
|
|
21
|
+
return { ok: true, message: "launchd agent already loaded" };
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
message: `launchctl load failed${text ? `: ${text}` : ""} — the agent will load at next login`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Best-effort `launchctl load` of the dream LaunchAgent so scheduled runs
|
|
30
|
+
* start immediately instead of waiting for the next login. (v5.14.x sprint,
|
|
31
|
+
* pre-approved.) Never throws.
|
|
32
|
+
*/
|
|
33
|
+
export function loadDreamLaunchAgent(plistFile) {
|
|
34
|
+
if (process.platform !== "darwin")
|
|
35
|
+
return { ok: false, message: "launchctl unavailable (not macOS)" };
|
|
36
|
+
try {
|
|
37
|
+
execFileSync("launchctl", launchctlLoadArgs(plistFile), {
|
|
38
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
39
|
+
timeout: 10_000,
|
|
40
|
+
});
|
|
41
|
+
return { ok: true, message: "launchd agent loaded — scheduled dream runs are active now" };
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const stderr = err && typeof err === "object" && "stderr" in err && err.stderr
|
|
45
|
+
? String(err.stderr)
|
|
46
|
+
: err instanceof Error
|
|
47
|
+
? err.message
|
|
48
|
+
: String(err);
|
|
49
|
+
return interpretLaunchctlLoadError(stderr);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Best-effort `launchctl unload` before removing the plist, so disabling
|
|
54
|
+
* Dream Mode deactivates the schedule immediately. Never throws.
|
|
55
|
+
*/
|
|
56
|
+
export function unloadDreamLaunchAgent(plistFile) {
|
|
57
|
+
if (process.platform !== "darwin")
|
|
58
|
+
return { ok: false, message: "launchctl unavailable (not macOS)" };
|
|
59
|
+
try {
|
|
60
|
+
execFileSync("launchctl", launchctlUnloadArgs(plistFile), {
|
|
61
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
62
|
+
timeout: 10_000,
|
|
63
|
+
});
|
|
64
|
+
return { ok: true, message: "launchd agent unloaded" };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Not loaded (or launchctl unavailable) — nothing to deactivate.
|
|
68
|
+
return { ok: true, message: "launchd agent was not loaded" };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
5
71
|
function plistPath() {
|
|
6
72
|
return path.join(os.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
7
73
|
}
|
|
@@ -58,10 +124,151 @@ export function installDreamLaunchAgent() {
|
|
|
58
124
|
fs.writeFileSync(file, body, "utf8");
|
|
59
125
|
return file;
|
|
60
126
|
}
|
|
127
|
+
function xmlUnescape(value) {
|
|
128
|
+
return value
|
|
129
|
+
.replace(/"/g, '"')
|
|
130
|
+
.replace(/>/g, ">")
|
|
131
|
+
.replace(/</g, "<")
|
|
132
|
+
.replace(/&/g, "&");
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Pure helper: extract the node and cli paths from a dream LaunchAgent
|
|
136
|
+
* plist body. Template shape (installDreamLaunchAgent): the first
|
|
137
|
+
* `<string>` is the Label, the second is the node binary path, the third
|
|
138
|
+
* is the cli path. Exported for tests.
|
|
139
|
+
*/
|
|
140
|
+
export function parseDreamPlistPaths(body) {
|
|
141
|
+
const strings = [...body.matchAll(/<string>([^<]*)<\/string>/g)].map((m) => xmlUnescape(m[1]));
|
|
142
|
+
return {
|
|
143
|
+
nodePath: strings.length > 1 ? strings[1] : undefined,
|
|
144
|
+
cliPath: strings.length > 2 ? strings[2] : undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Health-check the dream LaunchAgent. The plist hardcodes absolute node +
|
|
149
|
+
* cli paths (e.g. ~/.nvm/versions/node/vX.Y.Z/bin/node), so a Node upgrade
|
|
150
|
+
* silently kills the scheduler — this detects that. Never throws.
|
|
151
|
+
*/
|
|
152
|
+
export function checkDreamLaunchAgent() {
|
|
153
|
+
if (process.platform !== "darwin") {
|
|
154
|
+
return {
|
|
155
|
+
installed: false,
|
|
156
|
+
loaded: false,
|
|
157
|
+
nodeExists: false,
|
|
158
|
+
cliExists: false,
|
|
159
|
+
healthy: false,
|
|
160
|
+
problems: ["launchd unavailable (not macOS)"],
|
|
161
|
+
plistFile: null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const file = plistPath();
|
|
165
|
+
if (!fs.existsSync(file)) {
|
|
166
|
+
return {
|
|
167
|
+
installed: false,
|
|
168
|
+
loaded: false,
|
|
169
|
+
nodeExists: false,
|
|
170
|
+
cliExists: false,
|
|
171
|
+
healthy: false,
|
|
172
|
+
problems: ["launchd agent not installed"],
|
|
173
|
+
plistFile: file,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const problems = [];
|
|
177
|
+
let nodeExists = false;
|
|
178
|
+
let cliExists = false;
|
|
179
|
+
try {
|
|
180
|
+
const body = fs.readFileSync(file, "utf8");
|
|
181
|
+
const { nodePath, cliPath } = parseDreamPlistPaths(body);
|
|
182
|
+
nodeExists = nodePath !== undefined && fs.existsSync(nodePath);
|
|
183
|
+
if (!nodeExists) {
|
|
184
|
+
problems.push(`node binary missing at ${nodePath ?? "(unknown)"} — a Node upgrade likely moved it; run repair to rewrite the agent`);
|
|
185
|
+
}
|
|
186
|
+
// "gnosys" (bare command) counts as existing — resolved via PATH.
|
|
187
|
+
cliExists =
|
|
188
|
+
cliPath !== undefined && (cliPath === "gnosys" || fs.existsSync(cliPath));
|
|
189
|
+
if (!cliExists) {
|
|
190
|
+
problems.push(`gnosys cli missing at ${cliPath ?? "(unknown)"}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
problems.push(`could not read plist: ${err instanceof Error ? err.message : String(err)}`);
|
|
195
|
+
}
|
|
196
|
+
let loaded = false;
|
|
197
|
+
try {
|
|
198
|
+
execFileSync("launchctl", ["list", LABEL], {
|
|
199
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
200
|
+
timeout: 10_000,
|
|
201
|
+
});
|
|
202
|
+
loaded = true;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
problems.push("launchd agent not loaded");
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
installed: true,
|
|
209
|
+
loaded,
|
|
210
|
+
nodeExists,
|
|
211
|
+
cliExists,
|
|
212
|
+
healthy: loaded && nodeExists && cliExists,
|
|
213
|
+
problems,
|
|
214
|
+
plistFile: file,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Repair the dream LaunchAgent: rewrite the plist with the current
|
|
219
|
+
* process's node + cli paths, then reload it. Best-effort — never throws.
|
|
220
|
+
*/
|
|
221
|
+
export function repairDreamLaunchAgent() {
|
|
222
|
+
const file = installDreamLaunchAgent();
|
|
223
|
+
if (!file) {
|
|
224
|
+
return { ok: false, message: "launchd unavailable (not macOS)" };
|
|
225
|
+
}
|
|
226
|
+
unloadDreamLaunchAgent(file);
|
|
227
|
+
const load = loadDreamLaunchAgent(file);
|
|
228
|
+
if (!load.ok) {
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
message: `agent reinstalled but reload failed: ${load.message}`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return { ok: true, message: "dream launchd agent repaired and reloaded" };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Post-upgrade hook: when Dream Mode is enabled and the LaunchAgent is
|
|
238
|
+
* installed but unhealthy (typically because a Node upgrade moved the
|
|
239
|
+
* hardcoded node path), repair it. Returns a printable status line, or
|
|
240
|
+
* null when nothing applies (non-darwin, dream disabled, agent healthy
|
|
241
|
+
* or not installed). Never throws.
|
|
242
|
+
*/
|
|
243
|
+
export async function repairDreamLaunchAgentAfterUpgrade() {
|
|
244
|
+
if (process.platform !== "darwin")
|
|
245
|
+
return null;
|
|
246
|
+
try {
|
|
247
|
+
const { loadConfig } = await import("./config.js");
|
|
248
|
+
const { getGnosysHome } = await import("./paths.js");
|
|
249
|
+
const config = await loadConfig(getGnosysHome());
|
|
250
|
+
if (!config.dream?.enabled)
|
|
251
|
+
return null;
|
|
252
|
+
const health = checkDreamLaunchAgent();
|
|
253
|
+
if (!health.installed || health.healthy)
|
|
254
|
+
return null;
|
|
255
|
+
const repair = repairDreamLaunchAgent();
|
|
256
|
+
return repair.ok
|
|
257
|
+
? "✓ dream launchd agent repaired (node path had changed)"
|
|
258
|
+
: `⚠ dream launchd agent unhealthy and repair failed: ${repair.message}`;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
61
264
|
export function uninstallDreamLaunchAgent() {
|
|
62
265
|
if (process.platform !== "darwin")
|
|
63
266
|
return null;
|
|
64
267
|
const file = plistPath();
|
|
268
|
+
// Unload before unlinking — launchctl needs the plist on disk to resolve
|
|
269
|
+
// the label, and this deactivates the schedule immediately. Best-effort.
|
|
270
|
+
if (fs.existsSync(file))
|
|
271
|
+
unloadDreamLaunchAgent(file);
|
|
65
272
|
try {
|
|
66
273
|
fs.unlinkSync(file);
|
|
67
274
|
}
|
|
@@ -92,7 +92,10 @@ export async function runHybridSearchCommand(getResolver, query, opts) {
|
|
|
92
92
|
const writeTarget = resolver.getWriteTarget();
|
|
93
93
|
if (writeTarget) {
|
|
94
94
|
const { GnosysMaintenanceEngine } = await import("./maintenance.js");
|
|
95
|
-
await GnosysMaintenanceEngine.reinforceBatch(writeTarget.store, results.map((r) => r.relativePath)).catch(() => {
|
|
95
|
+
await GnosysMaintenanceEngine.reinforceBatch(writeTarget.store, results.map((r) => r.relativePath)).catch((err) => {
|
|
96
|
+
// Best-effort, but don't be fully silent (sprint 2026-07-02).
|
|
97
|
+
console.error(`gnosys: reinforcement skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
98
|
+
});
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
search.close();
|
|
@@ -33,7 +33,11 @@ export async function runImportCommand(getResolver, fileOrUrl, opts) {
|
|
|
33
33
|
const ingestion = new GnosysIngestion(writeTarget.store, tagRegistry);
|
|
34
34
|
const format = opts.format;
|
|
35
35
|
const mode = opts.mode;
|
|
36
|
-
|
|
36
|
+
// v5.15: --concurrency CLI flag wins; otherwise the config's
|
|
37
|
+
// importConcurrency (schema default 5) applies.
|
|
38
|
+
const { loadConfig } = await import("./config.js");
|
|
39
|
+
const config = await loadConfig(writeTarget.store.getStorePath());
|
|
40
|
+
const concurrency = opts.concurrency || config.importConcurrency;
|
|
37
41
|
// Show estimate for LLM mode
|
|
38
42
|
if (mode === "llm") {
|
|
39
43
|
console.error(`Mode: LLM (concurrency: ${concurrency})`);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard for interactive prompt flows (v5.14.x sprint, pre-approved).
|
|
3
|
+
*
|
|
4
|
+
* Interactive setup flows crash with ERR_USE_AFTER_CLOSE when stdin is
|
|
5
|
+
* closed (e.g. `gnosys setup dream` run non-interactively from a script
|
|
6
|
+
* or CI). Instead of a stack trace, print a friendly one-line error and
|
|
7
|
+
* exit 1.
|
|
8
|
+
*/
|
|
9
|
+
/** Returns true when stdin can host an interactive prompt session. */
|
|
10
|
+
export declare function stdinIsInteractive(): boolean;
|
|
11
|
+
/** Human-readable message for a blocked interactive flow. */
|
|
12
|
+
export declare function nonInteractiveMessage(flow: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Exit with a friendly one-liner when stdin is not a TTY.
|
|
15
|
+
* Call at the entry of interactive prompt flows, before any readline use.
|
|
16
|
+
*/
|
|
17
|
+
export declare function guardInteractiveStdin(flow: string): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard for interactive prompt flows (v5.14.x sprint, pre-approved).
|
|
3
|
+
*
|
|
4
|
+
* Interactive setup flows crash with ERR_USE_AFTER_CLOSE when stdin is
|
|
5
|
+
* closed (e.g. `gnosys setup dream` run non-interactively from a script
|
|
6
|
+
* or CI). Instead of a stack trace, print a friendly one-line error and
|
|
7
|
+
* exit 1.
|
|
8
|
+
*/
|
|
9
|
+
/** Returns true when stdin can host an interactive prompt session. */
|
|
10
|
+
export function stdinIsInteractive() {
|
|
11
|
+
return Boolean(process.stdin.isTTY);
|
|
12
|
+
}
|
|
13
|
+
/** Human-readable message for a blocked interactive flow. */
|
|
14
|
+
export function nonInteractiveMessage(flow) {
|
|
15
|
+
return `gnosys ${flow} is interactive and requires a terminal (stdin is not a TTY). Re-run from an interactive shell.`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Exit with a friendly one-liner when stdin is not a TTY.
|
|
19
|
+
* Call at the entry of interactive prompt flows, before any readline use.
|
|
20
|
+
*/
|
|
21
|
+
export function guardInteractiveStdin(flow) {
|
|
22
|
+
if (stdinIsInteractive())
|
|
23
|
+
return;
|
|
24
|
+
console.error(nonInteractiveMessage(flow));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
@@ -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/dist/lib/setup.js
CHANGED
|
@@ -14,6 +14,7 @@ import fsSync from "fs";
|
|
|
14
14
|
import path from "path";
|
|
15
15
|
import os from "os";
|
|
16
16
|
import { execSync } from "child_process";
|
|
17
|
+
import { guardInteractiveStdin } from "./interactiveGuard.js";
|
|
17
18
|
import { loadConfig, updateConfig, resolveTaskModel, getProviderModel, } from "./config.js";
|
|
18
19
|
import { isApiKeyValidationError, validateModel } from "./modelValidation.js";
|
|
19
20
|
import { buildOpenRouterTiers, OPENROUTER_STATIC_TIERS, } from "./openrouterTiers.js";
|
|
@@ -1162,6 +1163,7 @@ export async function runSetup(opts) {
|
|
|
1162
1163
|
};
|
|
1163
1164
|
}
|
|
1164
1165
|
// ─── Interactive mode ─────────────────────────────────────────────────
|
|
1166
|
+
guardInteractiveStdin("setup");
|
|
1165
1167
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
1166
1168
|
let setupCompleted = false;
|
|
1167
1169
|
// Handle Ctrl+C gracefully — only show "cancelled" if setup didn't finish
|
|
@@ -2130,6 +2132,8 @@ export function buildTaskModelsPatchFromAccepted(accepted, currentByTask, select
|
|
|
2130
2132
|
export async function runModelsSetup(opts = {}) {
|
|
2131
2133
|
const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
|
|
2132
2134
|
const ownsRl = !opts.rl;
|
|
2135
|
+
if (ownsRl)
|
|
2136
|
+
guardInteractiveStdin("setup models");
|
|
2133
2137
|
const rl = opts.rl ?? createInterface({ input: stdin, output: stdout });
|
|
2134
2138
|
try {
|
|
2135
2139
|
const { Header } = await import("./setup/ui/header.js");
|
|
@@ -2519,6 +2523,8 @@ async function runModelsTaskRoutingSetup(ctx) {
|
|
|
2519
2523
|
export async function runDreamSetup(opts = {}) {
|
|
2520
2524
|
const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
|
|
2521
2525
|
const ownsRl = !opts.rl;
|
|
2526
|
+
if (ownsRl)
|
|
2527
|
+
guardInteractiveStdin("setup dream");
|
|
2522
2528
|
const rl = opts.rl ?? createInterface({ input: stdin, output: stdout });
|
|
2523
2529
|
try {
|
|
2524
2530
|
// v5.9.3 Screen 7 — three grouped sub-screens (7.0 enable, 7.1
|
|
@@ -2757,8 +2763,11 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2757
2763
|
// Reset consecutive failure counter on a fresh setup so Layer 4
|
|
2758
2764
|
// doesn't fire immediately based on stale history.
|
|
2759
2765
|
localDb.resetDreamConsecutiveFailures();
|
|
2760
|
-
const { installDreamLaunchAgent } = await import("./dreamLaunchd.js");
|
|
2766
|
+
const { installDreamLaunchAgent, loadDreamLaunchAgent } = await import("./dreamLaunchd.js");
|
|
2761
2767
|
const launchdPath = installDreamLaunchAgent();
|
|
2768
|
+
// Best-effort immediate activation so the schedule doesn't wait for the
|
|
2769
|
+
// next login (v5.14.x sprint, pre-approved).
|
|
2770
|
+
const launchdLoad = launchdPath ? loadDreamLaunchAgent(launchdPath) : null;
|
|
2762
2771
|
localDb.close();
|
|
2763
2772
|
remoteDb?.close();
|
|
2764
2773
|
// Final Diff block per the design — provider/machine + the two
|
|
@@ -2789,6 +2798,8 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2789
2798
|
printStatus("progress", `scheduled dream checks run nightly (${scheduleStartHour}:00-${scheduleEndHour}:00) on ${dreamerName}`);
|
|
2790
2799
|
if (launchdPath)
|
|
2791
2800
|
printStatus("ok", "launchd agent installed", launchdPath);
|
|
2801
|
+
if (launchdLoad)
|
|
2802
|
+
printStatus(launchdLoad.ok ? "ok" : "warn", launchdLoad.message);
|
|
2792
2803
|
printStatus("progress", "check status anytime with `gnosys status --system`");
|
|
2793
2804
|
}
|
|
2794
2805
|
finally {
|
package/dist/lib/setupKeys.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "readline/promises";
|
|
2
2
|
import { stdin, stdout } from "process";
|
|
3
|
+
import { guardInteractiveStdin } from "./interactiveGuard.js";
|
|
3
4
|
import fs from "fs/promises";
|
|
4
5
|
import fsSync from "fs";
|
|
5
6
|
import os from "os";
|
|
@@ -522,6 +523,8 @@ export function renderProviderTable(providers) {
|
|
|
522
523
|
}
|
|
523
524
|
export async function runKeysSetup(opts) {
|
|
524
525
|
const ownReadline = !opts?.rl;
|
|
526
|
+
if (ownReadline)
|
|
527
|
+
guardInteractiveStdin("setup keys");
|
|
525
528
|
const rl = opts?.rl ?? createInterface({ input: stdin, output: stdout });
|
|
526
529
|
try {
|
|
527
530
|
while (true) {
|
|
@@ -86,6 +86,22 @@ export async function runStatusCommand(opts, deps) {
|
|
|
86
86
|
}
|
|
87
87
|
const data = await collectDashboardData(resolver, cfg, deps.pkgVersion, dashDb ?? undefined);
|
|
88
88
|
console.log(opts.json ? formatDashboardJSON(data) : formatDashboard(data));
|
|
89
|
+
// v5.15: dream launchd agent health (read-only — repair happens in
|
|
90
|
+
// `gnosys upgrade`). macOS + dream enabled only; skip in JSON mode
|
|
91
|
+
// to keep the JSON payload shape stable.
|
|
92
|
+
if (!opts.json && process.platform === "darwin" && cfg.dream?.enabled) {
|
|
93
|
+
const { checkDreamLaunchAgent } = await import("./dreamLaunchd.js");
|
|
94
|
+
const health = checkDreamLaunchAgent();
|
|
95
|
+
console.log("\nDream launchd agent:");
|
|
96
|
+
if (health.healthy) {
|
|
97
|
+
console.log(" ✓ healthy (installed, loaded, node + cli paths valid)");
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
for (const problem of health.problems) {
|
|
101
|
+
console.log(` ⚠ ${problem}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
89
105
|
}
|
|
90
106
|
catch (err) {
|
|
91
107
|
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gnosys",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.15.1",
|
|
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",
|