gnosys 5.14.0 → 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 +6 -0
- package/dist/index.js +2 -1
- 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/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;
|
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
|
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
|
+
}
|
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",
|