gnosys 5.11.0 → 5.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,6 +36,7 @@ gnosys setup # configures provider, API key, and your IDE/agent
36
36
  ## Quick start
37
37
 
38
38
  ```bash
39
+ gnosys setup ides # wire MCP into your IDEs (once)
39
40
  cd your-project
40
41
  gnosys init # register the project
41
42
  gnosys add "We chose PostgreSQL over MySQL for JSON support"
@@ -61,7 +62,7 @@ All tools are exposed over stdio and HTTP transports. Many tools accept an optio
61
62
 
62
63
  This package installs two binaries:
63
64
 
64
- - **`gnosys`** — the CLI. `gnosys serve` starts the MCP server (stdio by default, `--transport http` for the central-server topology). `gnosys init <ide>` wires this into your IDE/agent automatically.
65
+ - **`gnosys`** — the CLI. `gnosys serve` starts the MCP server (stdio by default, `--transport http` for the central-server topology). `gnosys setup ides` wires `gnosys-mcp` into your IDE/agent configs.
65
66
  - **`gnosys-mcp`** — a direct alias for the MCP stdio server entry, for MCP clients that prefer to spawn the server binary directly (e.g. `npx -y gnosys-mcp`). Equivalent to `gnosys serve`.
66
67
 
67
68
  | Tool | Description |
package/dist/cli.js CHANGED
@@ -26,6 +26,7 @@ import { loadConfig, generateConfigTemplate, DEFAULT_CONFIG, writeConfig, resolv
26
26
  import { getLLMProvider, isProviderAvailable } from "./lib/llm.js";
27
27
  import { GnosysDB } from "./lib/db.js";
28
28
  import { logError } from "./lib/log.js";
29
+ import { getSecureStorageSetupHint } from "./lib/platform.js";
29
30
  import { createProjectIdentity, readProjectIdentity, findProjectIdentity, migrateProject } from "./lib/projectIdentity.js";
30
31
  import { setPreference, getPreference, getAllPreferences, deletePreference, KNOWN_PREFERENCE_KEYS, suggestPreferenceKey } from "./lib/preferences.js";
31
32
  import { syncToTarget } from "./lib/rulesGen.js";
@@ -972,7 +973,7 @@ setupCmd
972
973
  // `gnosys setup ides` — configure IDE / MCP integrations standalone
973
974
  setupCmd
974
975
  .command("ides")
975
- .description("Configure IDE integrations (Claude Code/Desktop, Cursor, Codex, Gemini CLI, Antigravity)")
976
+ .description("Configure IDE MCP integrations (Claude Code/Desktop, Cursor, Codex, Grok Build, Gemini CLI, Antigravity)")
976
977
  .action(async () => {
977
978
  const readline = await import("readline/promises");
978
979
  const { runIdesSetup } = await import("./lib/setup/sections/ides.js");
@@ -1020,11 +1021,11 @@ setupCmd
1020
1021
  // we need to revive a top-level shortcut later.
1021
1022
  // ─── gnosys init ─────────────────────────────────────────────────────────
1022
1023
  program
1023
- .command("init [ide]")
1024
- .description("Initialize Gnosys in the current directory. Optionally specify IDE: cursor, claude, claude-desktop, codex, gemini-cli, or antigravity to force IDE setup.")
1024
+ .command("init")
1025
+ .description("Initialize Gnosys in the current directory (project store, identity, central DB). Wire IDE MCP servers with: gnosys setup ides")
1025
1026
  .option("-d, --directory <dir>", "Target directory (default: cwd)")
1026
1027
  .option("-n, --name <name>", "Project name (default: directory basename)")
1027
- .action(async (ide, opts) => {
1028
+ .action(async (opts) => {
1028
1029
  const targetDir = opts.directory
1029
1030
  ? path.resolve(opts.directory)
1030
1031
  : process.cwd();
@@ -1126,62 +1127,8 @@ program
1126
1127
  else {
1127
1128
  console.log(`\nIDE hooks: ${hookResult.details}`);
1128
1129
  }
1129
- // If a specific IDE was requested, force-create its config
1130
- if (ide) {
1131
- const validIdes = ["cursor", "claude", "claude-desktop", "codex", "gemini-cli", "antigravity"];
1132
- const normalizedIde = ide.toLowerCase();
1133
- if (!validIdes.includes(normalizedIde)) {
1134
- console.log(`\nUnknown IDE: "${ide}". Valid options: ${validIdes.join(", ")}`);
1135
- }
1136
- else {
1137
- const { configureCursor, configureClaudeCode, configureCodex } = await import("./lib/projectIdentity.js");
1138
- // Cursor/Claude/Codex have IDE-specific session hooks. Gemini CLI and
1139
- // Antigravity don't yet, so we skip the hook step for them.
1140
- let result;
1141
- switch (normalizedIde) {
1142
- case "cursor":
1143
- result = await configureCursor(targetDir);
1144
- break;
1145
- case "claude":
1146
- result = await configureClaudeCode(targetDir);
1147
- break;
1148
- case "codex":
1149
- result = await configureCodex(targetDir);
1150
- break;
1151
- }
1152
- if (result?.configured) {
1153
- console.log(`\nIDE setup (${result.ide}):`);
1154
- console.log(` ${result.details}`);
1155
- console.log(` File: ${result.filePath}`);
1156
- }
1157
- // Set up MCP config for the IDE
1158
- const { setupIDE } = await import("./lib/setup.js");
1159
- const mcp = await setupIDE(normalizedIde, targetDir);
1160
- if (mcp.success) {
1161
- console.log(` MCP: ${mcp.message}`);
1162
- }
1163
- // Update agentRulesTarget in gnosys.json (only for IDEs with rules files)
1164
- const targetMap = {
1165
- cursor: ".cursor/rules/gnosys.mdc",
1166
- claude: "CLAUDE.md",
1167
- codex: "CODEX.md",
1168
- };
1169
- if (targetMap[normalizedIde]) {
1170
- const identityPath = path.join(storePath, "gnosys.json");
1171
- try {
1172
- const identityContent = await fs.readFile(identityPath, "utf-8");
1173
- const identity = JSON.parse(identityContent);
1174
- identity.agentRulesTarget = targetMap[normalizedIde];
1175
- await fs.writeFile(identityPath, JSON.stringify(identity, null, 2) + "\n", "utf-8");
1176
- console.log(` Config: agentRulesTarget → ${identity.agentRulesTarget}`);
1177
- }
1178
- catch {
1179
- // Non-critical
1180
- }
1181
- }
1182
- }
1183
- }
1184
- console.log(`\nStart adding memories with: gnosys add "your knowledge here"`);
1130
+ console.log(`\nWire IDE MCP servers: gnosys setup ides`);
1131
+ console.log(`Start adding memories with: gnosys add "your knowledge here"`);
1185
1132
  });
1186
1133
  // ─── gnosys migrate ─────────────────────────────────────────────────────
1187
1134
  program
@@ -2836,7 +2783,7 @@ program
2836
2783
  const envVar = envVarMap[providerName];
2837
2784
  if (envVar) {
2838
2785
  console.error(`No LLM provider available. Configured default is "${providerName}" but its key wasn't found. ` +
2839
- `Set ${envVar}, run 'gnosys setup' to store one in the macOS Keychain, or add llm.${providerName}.apiKey to gnosys.json.`);
2786
+ `Set ${envVar}, run 'gnosys setup' to store one in ${getSecureStorageSetupHint()}, or add llm.${providerName}.apiKey to gnosys.json.`);
2840
2787
  }
2841
2788
  else {
2842
2789
  console.error(`No LLM provider available. Provider "${providerName}" is not reachable. Run 'gnosys setup' to configure one.`);
@@ -4421,7 +4368,7 @@ exportCmd
4421
4368
  // ─── gnosys serve ────────────────────────────────────────────────────────
4422
4369
  program
4423
4370
  .command("serve")
4424
- .description("Start the MCP server (stdio mode). Used by IDE integrations — Claude Code/Desktop, Cursor, Codex, etc. spawn this command in the background to talk to gnosys via the Model Context Protocol. You don't normally invoke this yourself; `gnosys init <ide>` wires it into the IDE config.")
4371
+ .description("Start the MCP server (stdio mode). Used by IDE integrations — Claude Code/Desktop, Cursor, Codex, etc. spawn this command in the background to talk to gnosys via the Model Context Protocol. You don't normally invoke this yourself; `gnosys setup ides` wires gnosys-mcp into your IDE configs.")
4425
4372
  .option("--with-maintenance", "Run maintenance every 6 hours in background")
4426
4373
  .option("--transport <mode>", "Transport: 'stdio' (default) or 'http' (central-server topology)", "stdio")
4427
4374
  .option("--host <addr>", "HTTP bind address — http transport (default 127.0.0.1; use a tailnet addr to share)", "127.0.0.1")
@@ -4460,7 +4407,8 @@ program
4460
4407
  setInterval(runMaintenance, SIX_HOURS);
4461
4408
  console.error("[maintenance] Background maintenance enabled (every 6 hours)");
4462
4409
  }
4463
- await import("./index.js");
4410
+ const { startMcpServer } = await import("./index.js");
4411
+ await startMcpServer();
4464
4412
  });
4465
4413
  // ─── gnosys recall ───────────────────────────────────────────────────────
4466
4414
  program
package/dist/index.d.ts CHANGED
@@ -13,3 +13,5 @@ export declare function registerCapabilities(s: McpServer): void;
13
13
  * need any of these should `await ensureHeavyDeps()` first.
14
14
  */
15
15
  export declare function ensureHeavyDeps(): Promise<void>;
16
+ /** Start the MCP server (stdio or http). Called by `gnosys serve` and when invoked as `gnosys-mcp`. */
17
+ export declare function startMcpServer(): Promise<void>;
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  // MCP stdio JSON protocol. parse() is a pure function with no side effects.
11
11
  import dotenv from "dotenv";
12
12
  import path from "path";
13
- import { readFileSync } from "fs";
13
+ import { readFileSync, realpathSync } from "fs";
14
14
  import { fileURLToPath } from "url";
15
15
  const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
16
16
  try {
@@ -2955,7 +2955,8 @@ async function initHeavyDeps() {
2955
2955
  heavyDepsReadyResolve?.();
2956
2956
  }
2957
2957
  // ─── Start the server ────────────────────────────────────────────────────
2958
- async function main() {
2958
+ /** Start the MCP server (stdio or http). Called by `gnosys serve` and when invoked as `gnosys-mcp`. */
2959
+ export async function startMcpServer() {
2959
2960
  // v5.7.1 (#15): start the upgrade-marker watcher BEFORE anything else.
2960
2961
  // If `gnosys upgrade` was run on this machine while the MCP was idle,
2961
2962
  // pick that up immediately instead of serving stale tool handlers.
@@ -3091,9 +3092,23 @@ async function main() {
3091
3092
  // Notification handler setup failed — non-critical
3092
3093
  }
3093
3094
  }
3094
- const invokedAsScript = !!process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
3095
- if (invokedAsScript) {
3096
- main().catch((err) => {
3095
+ /** True when this file is the process entry (direct path or npm `gnosys-mcp` bin symlink). */
3096
+ function isMcpEntryPoint() {
3097
+ if (!process.argv[1])
3098
+ return false;
3099
+ const entry = path.resolve(process.argv[1]);
3100
+ const self = fileURLToPath(import.meta.url);
3101
+ if (entry === self)
3102
+ return true;
3103
+ try {
3104
+ return realpathSync(entry) === realpathSync(self);
3105
+ }
3106
+ catch {
3107
+ return false;
3108
+ }
3109
+ }
3110
+ if (isMcpEntryPoint()) {
3111
+ startMcpServer().catch((err) => {
3097
3112
  console.error("Fatal error:", err);
3098
3113
  process.exit(1);
3099
3114
  });
package/dist/lib/ask.js CHANGED
@@ -13,6 +13,7 @@ import { getLLMProvider } from "./llm.js";
13
13
  import { GnosysArchive } from "./archive.js";
14
14
  import { GnosysMaintenanceEngine } from "./maintenance.js";
15
15
  import { auditLog } from "./audit.js";
16
+ import { getSecureStorageSetupHint } from "./platform.js";
16
17
  const __filename = fileURLToPath(import.meta.url);
17
18
  const __dirname = path.dirname(__filename);
18
19
  /**
@@ -132,7 +133,7 @@ export class GnosysAsk {
132
133
  }
133
134
  if (envVar) {
134
135
  throw new Error(`gnosys_ask requires an LLM. Configured default provider "${providerName}" has no key. ` +
135
- `Set ${envVar} in your shell, run 'gnosys setup' to store one in the macOS Keychain, or add llm.${providerName}.apiKey to gnosys.json.`);
136
+ `Set ${envVar} in your shell, run 'gnosys setup' to store one in ${getSecureStorageSetupHint()}, or add llm.${providerName}.apiKey to gnosys.json.`);
136
137
  }
137
138
  throw new Error(`gnosys_ask requires an LLM. Provider "${providerName}" is not available. Run 'gnosys setup' to configure one.`);
138
139
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { DEFAULT_CONFIG } from "./config.js";
7
7
  import { getLLMProvider } from "./llm.js";
8
+ import { getSetupStorageBullet } from "./platform.js";
8
9
  export class GnosysIngestion {
9
10
  provider = null;
10
11
  tagRegistry;
@@ -73,7 +74,7 @@ export class GnosysIngestion {
73
74
  lines.push(`Make sure ${providerName} is running locally (gnosys status --system will probe it).`, `Or use gnosys_add_structured for direct memory writes (no LLM needed).`);
74
75
  }
75
76
  else if (envVar) {
76
- lines.push(`Configure a key for ${providerName} via one of these methods:`, ` • gnosys setup — interactive (recommended; stores in macOS Keychain)`, ` • Set ${envVar} in your shell profile`, ` • Edit llm.${providerName}.apiKey in gnosys.json`, "", `Or use gnosys_add_structured for direct memory writes (no LLM needed).`);
77
+ lines.push(`Configure a key for ${providerName} via one of these methods:`, getSetupStorageBullet(), ` • Set ${envVar} in your shell profile`, ` • Edit llm.${providerName}.apiKey in gnosys.json`, "", `Or use gnosys_add_structured for direct memory writes (no LLM needed).`);
77
78
  }
78
79
  else {
79
80
  lines.push(`Switch to a different default provider with: gnosys config set provider <name>`, `Or use gnosys_add_structured for direct memory writes (no LLM needed).`);
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import fs from "fs/promises";
10
10
  import path from "path";
11
- import os from "os";
11
+ import { getClaudeDesktopConfigPath } from "./platform.js";
12
12
  /** The MCP server entry for a remote (HTTP/URL) gnosys server. */
13
13
  export function remoteMcpEntry(opts) {
14
14
  return {
@@ -16,18 +16,6 @@ export function remoteMcpEntry(opts) {
16
16
  ...(opts.token ? { headers: { Authorization: `Bearer ${opts.token}` } } : {}),
17
17
  };
18
18
  }
19
- /** Platform-specific Claude Desktop config path (mirrors setup.ts). */
20
- function claudeDesktopConfigPath() {
21
- const home = os.homedir();
22
- if (process.platform === "darwin") {
23
- return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
24
- }
25
- if (process.platform === "win32") {
26
- const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
27
- return path.join(appData, "Claude", "claude_desktop_config.json");
28
- }
29
- return path.join(home, ".config", "Claude", "claude_desktop_config.json");
30
- }
31
19
  /** Merge a `gnosys` entry into a JSON file's `mcpServers` map (create if absent). */
32
20
  export async function mergeJsonMcpServer(file, entry) {
33
21
  let config = {};
@@ -51,7 +39,7 @@ export async function writeCursorRemote(projectDir, opts) {
51
39
  }
52
40
  /** Write the remote entry into the Claude Desktop config. Returns the path. */
53
41
  async function writeClaudeDesktopRemote(opts) {
54
- const file = claudeDesktopConfigPath();
42
+ const file = getClaudeDesktopConfigPath();
55
43
  await mergeJsonMcpServer(file, remoteMcpEntry(opts));
56
44
  return file;
57
45
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Cross-platform paths and user-facing hints (macOS, Linux, Windows).
3
+ */
4
+ export type OsFamily = "macos" | "linux" | "windows";
5
+ /** Current OS family for CLI messages and help text. */
6
+ export declare function getOsFamily(): OsFamily;
7
+ /** Primary secure credential store name on this machine. */
8
+ export declare function getSecureStorageLabel(): string;
9
+ /** Short phrase for error messages (setup may still be required on Windows). */
10
+ export declare function getSecureStorageSetupHint(): string;
11
+ /** Order of API key resolution for user-facing help on the current OS. */
12
+ export declare function getApiKeyResolutionOrderText(): string;
13
+ /** Claude Desktop MCP config file path for the current platform. */
14
+ export declare function getClaudeDesktopConfigPath(): string;
15
+ /** Display path with ~ for home (for logs and help). */
16
+ export declare function displayClaudeDesktopConfigPath(): string;
17
+ /** Shell profile file(s) suggested for env vars on this OS. */
18
+ export declare function getShellProfileHint(): string;
19
+ /** Lines shown when user skips API key setup in gnosys setup. */
20
+ export declare function getApiKeySkipHints(envVarName: string, provider: string): string[];
21
+ /** ffmpeg install instructions (all platforms, for errors). */
22
+ export declare function formatFfmpegInstallHint(): string;
23
+ /** Ingest / LLM missing-key helper bullet for gnosys setup. */
24
+ export declare function getSetupStorageBullet(): string;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Cross-platform paths and user-facing hints (macOS, Linux, Windows).
3
+ */
4
+ import os from "os";
5
+ import path from "path";
6
+ /** Current OS family for CLI messages and help text. */
7
+ export function getOsFamily() {
8
+ if (process.platform === "darwin")
9
+ return "macos";
10
+ if (process.platform === "win32")
11
+ return "windows";
12
+ return "linux";
13
+ }
14
+ /** Primary secure credential store name on this machine. */
15
+ export function getSecureStorageLabel() {
16
+ switch (getOsFamily()) {
17
+ case "macos":
18
+ return "macOS Keychain";
19
+ case "linux":
20
+ return "GNOME Keyring";
21
+ case "windows":
22
+ return "Windows Credential Manager";
23
+ }
24
+ }
25
+ /** Short phrase for error messages (setup may still be required on Windows). */
26
+ export function getSecureStorageSetupHint() {
27
+ switch (getOsFamily()) {
28
+ case "macos":
29
+ return "the macOS Keychain (via gnosys setup)";
30
+ case "linux":
31
+ return "GNOME Keyring (via gnosys setup, when secret-tool is available)";
32
+ case "windows":
33
+ return "your user environment or ~/.config/gnosys/.env (via gnosys setup)";
34
+ }
35
+ }
36
+ /** Order of API key resolution for user-facing help on the current OS. */
37
+ export function getApiKeyResolutionOrderText() {
38
+ switch (getOsFamily()) {
39
+ case "macos":
40
+ return "macOS Keychain, environment variable, then ~/.config/gnosys/.env";
41
+ case "linux":
42
+ return "GNOME Keyring (when available), environment variable, then ~/.config/gnosys/.env";
43
+ case "windows":
44
+ return "environment variable, then ~/.config/gnosys/.env";
45
+ }
46
+ }
47
+ /** Claude Desktop MCP config file path for the current platform. */
48
+ export function getClaudeDesktopConfigPath() {
49
+ const home = os.homedir();
50
+ if (process.platform === "darwin") {
51
+ return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
52
+ }
53
+ if (process.platform === "win32") {
54
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
55
+ return path.join(appData, "Claude", "claude_desktop_config.json");
56
+ }
57
+ return path.join(home, ".config", "Claude", "claude_desktop_config.json");
58
+ }
59
+ /** Display path with ~ for home (for logs and help). */
60
+ export function displayClaudeDesktopConfigPath() {
61
+ const home = os.homedir();
62
+ const p = getClaudeDesktopConfigPath();
63
+ return p.startsWith(home) ? "~" + p.slice(home.length) : p;
64
+ }
65
+ /** Shell profile file(s) suggested for env vars on this OS. */
66
+ export function getShellProfileHint() {
67
+ switch (getOsFamily()) {
68
+ case "macos": {
69
+ const shell = path.basename(process.env.SHELL ?? "zsh");
70
+ return shell === "bash" ? "~/.bash_profile or ~/.bashrc" : "~/.zshrc";
71
+ }
72
+ case "linux": {
73
+ const shell = path.basename(process.env.SHELL ?? "bash");
74
+ return shell === "zsh" ? "~/.zshrc" : "~/.bashrc";
75
+ }
76
+ case "windows":
77
+ return "%USERPROFILE%\\Documents\\PowerShell\\Microsoft.PowerShell_profile.ps1 (or System Properties → Environment Variables)";
78
+ }
79
+ }
80
+ /** Lines shown when user skips API key setup in gnosys setup. */
81
+ export function getApiKeySkipHints(envVarName, provider) {
82
+ const hints = [];
83
+ const profile = getShellProfileHint();
84
+ if (getOsFamily() === "macos") {
85
+ hints.push(`macOS Keychain: security add-generic-password -a "$USER" -s "${envVarName}" -w "key" -U`);
86
+ }
87
+ if (getOsFamily() === "linux") {
88
+ hints.push(`GNOME Keyring: printf '%s' 'key' | secret-tool store --label="Gnosys ${provider}" service gnosys account ${envVarName}`);
89
+ }
90
+ if (getOsFamily() === "windows") {
91
+ hints.push(`PowerShell profile: [Environment]::SetEnvironmentVariable("${envVarName}", "key", "User")`);
92
+ }
93
+ hints.push(`Shell profile: echo 'export ${envVarName}=key' >> ${profile}`);
94
+ hints.push(`Dotenv file: echo '${envVarName}=key' >> ~/.config/gnosys/.env`);
95
+ return hints;
96
+ }
97
+ /** ffmpeg install instructions (all platforms, for errors). */
98
+ export function formatFfmpegInstallHint() {
99
+ return ("Install it with:\n" +
100
+ " macOS: brew install ffmpeg\n" +
101
+ " Linux: sudo apt install ffmpeg (Debian/Ubuntu) or your distro package manager\n" +
102
+ " Windows: winget install FFmpeg (or choco install ffmpeg)");
103
+ }
104
+ /** Ingest / LLM missing-key helper bullet for gnosys setup. */
105
+ export function getSetupStorageBullet() {
106
+ switch (getOsFamily()) {
107
+ case "macos":
108
+ return " • gnosys setup — interactive (recommended; stores in macOS Keychain)";
109
+ case "linux":
110
+ return " • gnosys setup — interactive (recommended; stores in GNOME Keyring when available)";
111
+ case "windows":
112
+ return " • gnosys setup — interactive (recommended; env var or ~/.config/gnosys/.env)";
113
+ }
114
+ }
@@ -11,6 +11,10 @@
11
11
  */
12
12
  import { GnosysDB, type DbMemory } from "./db.js";
13
13
  import type { ProgressCallback } from "./progress.js";
14
+ /** Resolve the configured remote directory for this machine (machine.json, then legacy meta). */
15
+ export declare function getConfiguredRemotePath(localDb: GnosysDB): string | null;
16
+ /** Stop using a remote on this machine; does not delete the remote database files. */
17
+ export declare function clearRemoteSyncConfig(localDb: GnosysDB): void;
14
18
  interface ConflictInfo {
15
19
  memoryId: string;
16
20
  title: string;
@@ -13,7 +13,27 @@ import { existsSync, statSync, mkdirSync, writeFileSync, unlinkSync } from "fs";
13
13
  import os from "os";
14
14
  import * as path from "path";
15
15
  import { GnosysDB } from "./db.js";
16
- import { readMachineConfig } from "./machineConfig.js";
16
+ import { readMachineConfig, writeMachineConfig } from "./machineConfig.js";
17
+ const META_REMOTE_PATH = "remote_path";
18
+ const META_REMOTE_MODE = "remote_mode";
19
+ /** Resolve the configured remote directory for this machine (machine.json, then legacy meta). */
20
+ export function getConfiguredRemotePath(localDb) {
21
+ const mc = readMachineConfig();
22
+ if (mc?.remote?.enabled && mc.remote.path)
23
+ return mc.remote.path;
24
+ const fromMeta = localDb.getMeta(META_REMOTE_PATH);
25
+ return fromMeta || null;
26
+ }
27
+ /** Stop using a remote on this machine; does not delete the remote database files. */
28
+ export function clearRemoteSyncConfig(localDb) {
29
+ localDb.setMeta(META_REMOTE_PATH, "");
30
+ localDb.deleteMeta(META_REMOTE_MODE);
31
+ const mc = readMachineConfig();
32
+ if (mc) {
33
+ mc.remote = { enabled: false };
34
+ writeMachineConfig(mc);
35
+ }
36
+ }
17
37
  // ─── Validation ─────────────────────────────────────────────────────────
18
38
  /**
19
39
  * Validate that a directory is suitable for hosting the remote gnosys.db.
@@ -7,7 +7,7 @@
7
7
  * 3. Join existing — second machine joining a remote that already has data
8
8
  */
9
9
  import { type Interface } from "readline/promises";
10
- import type { GnosysDB } from "./db.js";
10
+ import { GnosysDB } from "./db.js";
11
11
  export declare function runConfigureWizard(centralDb: GnosysDB, externalRl?: Interface): Promise<boolean>;
12
12
  export declare function configureFromPath(centralDb: GnosysDB, remotePath: string, opts?: {
13
13
  migrate?: boolean;
@@ -9,7 +9,8 @@
9
9
  import { readdirSync, statSync } from "fs";
10
10
  import * as path from "path";
11
11
  import { createInterface } from "readline/promises";
12
- import { RemoteSync, validateLocation } from "./remote.js";
12
+ import { GnosysDB } from "./db.js";
13
+ import { RemoteSync, validateLocation, getConfiguredRemotePath, clearRemoteSyncConfig, } from "./remote.js";
13
14
  import { safeQuestion } from "./setup/ui/safePrompt.js";
14
15
  import { Spinner } from "./setup/ui/spinner.js";
15
16
  import { printStatus } from "./setup/ui/status.js";
@@ -148,7 +149,7 @@ export async function runConfigureWizard(centralDb, externalRl) {
148
149
  const choice = await askChoice(rl, "What would you like to do?", [
149
150
  { key: "1", label: "Change remote location" },
150
151
  { key: "2", label: "Re-validate current remote" },
151
- { key: "3", label: "Disconnect remote (back to local-only)" },
152
+ { key: "3", label: "Disconnect remote (local-only — warns if sync is needed)" },
152
153
  { key: "4", label: "Cancel" },
153
154
  ], "4");
154
155
  if (choice === "4")
@@ -342,14 +343,131 @@ async function setupRemoteFlow(rl, centralDb, localActiveCount) {
342
343
  return true;
343
344
  }
344
345
  // ─── Reconfigure helpers ────────────────────────────────────────────────
345
- async function disconnectRemote(rl, centralDb) {
346
- const confirm = await askConfirm(rl, "Disconnect the remote? Your local DB will remain. The remote DB itself is not deleted.", false);
346
+ async function disconnectRemote(rl, localDb) {
347
+ const remotePath = getConfiguredRemotePath(localDb);
348
+ if (!remotePath) {
349
+ printStatus("warn", "remote is not configured");
350
+ return false;
351
+ }
352
+ const localCounts = localDb.getMemoryCount();
353
+ let remoteCounts = null;
354
+ let syncStatus = null;
355
+ const validation = await validateLocation(remotePath);
356
+ const sync = new RemoteSync(localDb, remotePath);
357
+ try {
358
+ syncStatus = await sync.getStatus();
359
+ const remoteDb = new GnosysDB(remotePath);
360
+ if (remoteDb.isAvailable()) {
361
+ remoteCounts = remoteDb.getMemoryCount();
362
+ remoteDb.close();
363
+ }
364
+ }
365
+ catch {
366
+ // Remote unreadable — still show warning with validation hints below.
367
+ }
368
+ finally {
369
+ sync.closeRemote();
370
+ }
371
+ const remoteReachable = Boolean(syncStatus?.reachable && validation.ok);
372
+ const remoteActive = remoteCounts?.active ?? validation.checks.existingDb.memoryCount ?? null;
373
+ console.log("");
374
+ printStatus("warn", "disconnecting makes this machine local-only");
375
+ console.log(` local ~/.gnosys/gnosys.db — ${localCounts.active} active memories`);
376
+ if (remoteReachable && remoteActive !== null) {
377
+ console.log(` remote ${remotePath} — ${remoteActive} active memories`);
378
+ }
379
+ else {
380
+ printStatus("warn", "remote is not reachable", remotePath);
381
+ }
382
+ console.log("");
383
+ console.log(" The remote folder and gnosys.db are not deleted.");
384
+ console.log(" After disconnect, this Mac will not read or write that remote,");
385
+ console.log(" even when the volume is mounted.");
386
+ const pendingPull = syncStatus?.pendingPull ?? 0;
387
+ const pendingPush = syncStatus?.pendingPush ?? 0;
388
+ const conflicts = syncStatus?.conflicts.length ?? 0;
389
+ const countGap = remoteActive !== null && remoteActive > localCounts.active
390
+ ? remoteActive - localCounts.active
391
+ : 0;
392
+ const shouldRecommendSync = remoteReachable &&
393
+ (pendingPull > 0 || pendingPush > 0 || conflicts > 0 || countGap > 0);
394
+ if (shouldRecommendSync) {
395
+ console.log("");
396
+ printStatus("warn", "your local cache may be behind the shared remote brain");
397
+ if (countGap > 0) {
398
+ console.log(` Remote has about ${countGap} more active memor${countGap === 1 ? "y" : "ies"} than local.`);
399
+ }
400
+ if (pendingPull > 0 || pendingPush > 0) {
401
+ const parts = [];
402
+ if (pendingPull > 0)
403
+ parts.push(`${pendingPull} to pull into local`);
404
+ if (pendingPush > 0)
405
+ parts.push(`${pendingPush} to push to remote`);
406
+ console.log(` Pending sync: ${parts.join(", ")}.`);
407
+ }
408
+ if (conflicts > 0) {
409
+ console.log(` ${conflicts} unresolved conflict${conflicts === 1 ? "" : "s"} — resolve before or during sync.`);
410
+ }
411
+ printStatus("progress", "recommended", "gnosys setup remote sync");
412
+ }
413
+ const choice = await askChoice(rl, shouldRecommendSync
414
+ ? "Sync first so you do not lose access to remote-only memories on this Mac:"
415
+ : "How do you want to proceed?", remoteReachable
416
+ ? [
417
+ { key: "1", label: "Run gnosys setup remote sync, then disconnect (recommended)" },
418
+ { key: "2", label: "Disconnect now without syncing (keep current local DB only)" },
419
+ { key: "3", label: "Cancel" },
420
+ ]
421
+ : [
422
+ { key: "2", label: "Disconnect anyway (local DB only; remote not reachable to sync)" },
423
+ { key: "3", label: "Cancel" },
424
+ ], "3");
425
+ if (choice === "3") {
426
+ console.log("Cancelled.");
427
+ return false;
428
+ }
429
+ if (choice === "1" && remoteReachable) {
430
+ const spin = Spinner("syncing local and remote…");
431
+ const syncRun = new RemoteSync(localDb, remotePath);
432
+ try {
433
+ const result = await syncRun.sync();
434
+ if (result.errors.length > 0) {
435
+ spin.fail("sync had errors");
436
+ for (const e of result.errors)
437
+ printStatus("fail", e);
438
+ const proceed = await askConfirm(rl, "Disconnect anyway without a clean sync?", false);
439
+ if (!proceed)
440
+ return false;
441
+ }
442
+ else {
443
+ spin.ok("sync complete", `pushed ${result.pushed} · pulled ${result.pulled} · conflicts ${result.conflicts.length}`);
444
+ if (result.conflicts.length > 0) {
445
+ printStatus("warn", "conflicts still open", "gnosys setup remote resolve <id> --keep <local|remote>");
446
+ }
447
+ }
448
+ }
449
+ finally {
450
+ syncRun.closeRemote();
451
+ }
452
+ }
453
+ else if (choice === "1" && !remoteReachable) {
454
+ printStatus("fail", "cannot sync — mount the remote or cancel");
455
+ return false;
456
+ }
457
+ if (choice === "2" && shouldRecommendSync) {
458
+ const risky = await askConfirm(rl, "Disconnect without syncing? You may only see local memories on this Mac until you reconnect.", false);
459
+ if (!risky) {
460
+ console.log("Cancelled.");
461
+ return false;
462
+ }
463
+ }
464
+ const confirm = await askConfirm(rl, "Disconnect now? Remote files stay on disk; this machine uses ~/.gnosys/gnosys.db only.", false);
347
465
  if (!confirm) {
348
466
  console.log("Cancelled.");
349
467
  return false;
350
468
  }
351
- centralDb.setMeta(REMOTE_PATH_KEY, "");
352
- console.log("✓ Remote disconnected. Gnosys is now local-only.");
469
+ clearRemoteSyncConfig(localDb);
470
+ console.log("✓ Remote disconnected. Gnosys is now local-only on this machine.");
353
471
  return true;
354
472
  }
355
473
  async function revalidateRemote(_rl, _centralDb, currentRemote) {
package/dist/lib/setup.js CHANGED
@@ -18,6 +18,7 @@ import { loadConfig, updateConfig, getProviderModel, } from "./config.js";
18
18
  import { validateModel } from "./modelValidation.js";
19
19
  import { resolveActiveStorePath, ensureActiveStorePath } from "./setup/storePath.js";
20
20
  import { safeQuestion } from "./setup/ui/safePrompt.js";
21
+ import { getClaudeDesktopConfigPath, getApiKeySkipHints } from "./platform.js";
21
22
  // ─── ANSI Colors ────────────────────────────────────────────────────────────
22
23
  const BOLD = "\x1b[1m";
23
24
  const DIM = "\x1b[2m";
@@ -538,7 +539,7 @@ export async function detectIDEs(projectDir) {
538
539
  // Check for Claude Desktop — distinct from Claude Code CLI. Detected via the
539
540
  // app bundle on macOS or the platform-specific config dir.
540
541
  try {
541
- const cfg = claudeDesktopConfigPath();
542
+ const cfg = getClaudeDesktopConfigPath();
542
543
  const cfgDir = path.dirname(cfg);
543
544
  const stat = await fs.stat(cfgDir);
544
545
  if (stat.isDirectory())
@@ -566,23 +567,6 @@ export async function detectIDEs(projectDir) {
566
567
  }
567
568
  return detected;
568
569
  }
569
- /**
570
- * Resolve the platform-specific Claude Desktop config file path.
571
- * macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
572
- * Windows: %APPDATA%/Claude/claude_desktop_config.json
573
- * Linux: ~/.config/Claude/claude_desktop_config.json (no official build yet)
574
- */
575
- function claudeDesktopConfigPath() {
576
- const home = os.homedir();
577
- if (process.platform === "darwin") {
578
- return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
579
- }
580
- if (process.platform === "win32") {
581
- const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
582
- return path.join(appData, "Claude", "claude_desktop_config.json");
583
- }
584
- return path.join(home, ".config", "Claude", "claude_desktop_config.json");
585
- }
586
570
  /**
587
571
  * Replace (or append) a `[mcp.<name>]` block inside the TOML text for
588
572
  * Grok Build's config file. Preserves every line outside that block —
@@ -634,6 +618,25 @@ export function upsertGrokMcpBlock(existing, name, entry) {
634
618
  const afterBlock = afterLines.join("\n");
635
619
  return `${head}${sectionHeader}\n${blockBody}${gap}${afterBlock}`;
636
620
  }
621
+ /**
622
+ * Absolute path to the `gnosys-mcp` stdio entry (dist/index.js).
623
+ * Prefer this over `gnosys serve` — v5.11.0 `gnosys serve` imported index.js but
624
+ * did not call startMcpServer(), so MCP hosts saw "connection closed" on init.
625
+ */
626
+ function resolveGnosysMcpCommand() {
627
+ try {
628
+ const p = execSync("command -v gnosys-mcp", { encoding: "utf-8" }).trim();
629
+ if (p)
630
+ return p;
631
+ }
632
+ catch {
633
+ // Fall back to bare name on PATH.
634
+ }
635
+ return "gnosys-mcp";
636
+ }
637
+ function gnosysMcpServerEntry() {
638
+ return { command: resolveGnosysMcpCommand(), args: [] };
639
+ }
637
640
  function renderGrokMcpBlock(entry) {
638
641
  const argsStr = `[${entry.args.map((a) => JSON.stringify(a)).join(", ")}]`;
639
642
  const lines = [
@@ -652,8 +655,15 @@ export async function setupIDE(ide, projectDir) {
652
655
  try {
653
656
  switch (ide) {
654
657
  case "claude": {
658
+ const mcpCmd = resolveGnosysMcpCommand();
655
659
  try {
656
- execSync("claude mcp add -s user gnosys -- gnosys serve", {
660
+ try {
661
+ execSync("claude mcp remove gnosys", { stdio: "pipe" });
662
+ }
663
+ catch {
664
+ // Not registered yet — fine.
665
+ }
666
+ execSync(`claude mcp add -s user gnosys -- ${mcpCmd}`, {
657
667
  stdio: "pipe",
658
668
  });
659
669
  }
@@ -664,7 +674,7 @@ export async function setupIDE(ide, projectDir) {
664
674
  }
665
675
  throw e;
666
676
  }
667
- return { success: true, message: "Claude Code MCP server registered" };
677
+ return { success: true, message: `Claude Code MCP server registered (${mcpCmd})` };
668
678
  }
669
679
  case "cursor": {
670
680
  const cursorDir = path.join(projectDir, ".cursor");
@@ -680,7 +690,7 @@ export async function setupIDE(ide, projectDir) {
680
690
  }
681
691
  // Merge gnosys entry
682
692
  const servers = (config.mcpServers ?? {});
683
- servers.gnosys = { command: "gnosys", args: ["serve"] };
693
+ servers.gnosys = gnosysMcpServerEntry();
684
694
  config.mcpServers = servers;
685
695
  await fs.writeFile(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
686
696
  return { success: true, message: "Cursor MCP config updated (.cursor/mcp.json)" };
@@ -713,15 +723,8 @@ export async function setupIDE(ide, projectDir) {
713
723
  catch {
714
724
  // No user-level config.toml to clean — fine.
715
725
  }
716
- // 2. Find the absolute path to `gnosys` so Codex can launch it even
717
- // if shell PATH differs from what it has at run time.
718
- let gnosysCmd = "gnosys";
719
- try {
720
- gnosysCmd = execSync("command -v gnosys", { encoding: "utf-8" }).trim() || "gnosys";
721
- }
722
- catch {
723
- // Fall back to `gnosys` on PATH if `command -v` fails.
724
- }
726
+ // 2. Absolute path to `gnosys-mcp` (stdio entry) for Codex spawn.
727
+ const gnosysCmd = resolveGnosysMcpCommand();
725
728
  // 3. Check whether gnosys is already registered. If yes and the
726
729
  // command matches, leave it alone (idempotent). If it differs,
727
730
  // remove and re-add.
@@ -730,7 +733,7 @@ export async function setupIDE(ide, projectDir) {
730
733
  const existing = execSync("codex mcp get gnosys 2>/dev/null", {
731
734
  encoding: "utf-8",
732
735
  });
733
- if (existing && existing.includes(gnosysCmd) && existing.includes("serve")) {
736
+ if (existing && existing.includes(gnosysCmd) && !existing.includes(" serve")) {
734
737
  alreadyCorrect = true;
735
738
  }
736
739
  else if (existing) {
@@ -755,7 +758,7 @@ export async function setupIDE(ide, projectDir) {
755
758
  }
756
759
  // 4. Register via the canonical Codex CLI command.
757
760
  try {
758
- execSync(`codex mcp add gnosys -- ${gnosysCmd} serve`, { stdio: "pipe" });
761
+ execSync(`codex mcp add gnosys -- ${gnosysCmd}`, { stdio: "pipe" });
759
762
  }
760
763
  catch (err) {
761
764
  const msg = err instanceof Error ? err.message : String(err);
@@ -785,7 +788,7 @@ export async function setupIDE(ide, projectDir) {
785
788
  // File doesn't exist or is invalid — start fresh
786
789
  }
787
790
  const servers = (config.mcpServers ?? {});
788
- servers.gnosys = { command: "gnosys", args: ["serve"] };
791
+ servers.gnosys = gnosysMcpServerEntry();
789
792
  config.mcpServers = servers;
790
793
  await fs.writeFile(settingsPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
791
794
  return { success: true, message: "Gemini CLI MCP config updated (~/.gemini/settings.json)" };
@@ -805,7 +808,7 @@ export async function setupIDE(ide, projectDir) {
805
808
  // File doesn't exist or is invalid — start fresh
806
809
  }
807
810
  const servers = (config.mcpServers ?? {});
808
- servers.gnosys = { command: "gnosys", args: ["serve"] };
811
+ servers.gnosys = gnosysMcpServerEntry();
809
812
  config.mcpServers = servers;
810
813
  await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
811
814
  return { success: true, message: "Antigravity MCP config updated (~/.gemini/antigravity/mcp_config.json)" };
@@ -826,8 +829,7 @@ export async function setupIDE(ide, projectDir) {
826
829
  // File doesn't exist yet — start fresh
827
830
  }
828
831
  const updated = upsertGrokMcpBlock(existing, "gnosys", {
829
- command: "gnosys",
830
- args: ["serve"],
832
+ ...gnosysMcpServerEntry(),
831
833
  startup_timeout_sec: 90,
832
834
  });
833
835
  await fs.writeFile(configPath, updated, "utf-8");
@@ -837,7 +839,7 @@ export async function setupIDE(ide, projectDir) {
837
839
  // Claude Desktop reads MCP servers from claude_desktop_config.json
838
840
  // in a platform-specific app data directory. Distinct from Claude
839
841
  // Code CLI which uses `claude mcp add`.
840
- const configPath = claudeDesktopConfigPath();
842
+ const configPath = getClaudeDesktopConfigPath();
841
843
  const configDir = path.dirname(configPath);
842
844
  await fs.mkdir(configDir, { recursive: true });
843
845
  let config = {};
@@ -849,7 +851,7 @@ export async function setupIDE(ide, projectDir) {
849
851
  // File doesn't exist or is invalid — start fresh
850
852
  }
851
853
  const servers = (config.mcpServers ?? {});
852
- servers.gnosys = { command: "gnosys", args: ["serve"] };
854
+ servers.gnosys = gnosysMcpServerEntry();
853
855
  config.mcpServers = servers;
854
856
  await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
855
857
  // Display path with ~ prefix when inside HOME for clarity
@@ -1432,14 +1434,10 @@ export async function runSetup(opts) {
1432
1434
  else {
1433
1435
  // Skip
1434
1436
  console.log(` ${DIM}Skipped. Set your key later using one of these methods:`);
1435
- if (isMac) {
1436
- console.log(` \u2022 macOS Keychain: security add-generic-password -a "$USER" -s "${envVarName}" -w "key" -U`);
1437
- }
1438
- if (hasSecret) {
1439
- console.log(` \u2022 GNOME Keyring: printf '%s' 'key' | secret-tool store --label="Gnosys ${provider}" service gnosys account ${envVarName}`);
1437
+ for (const hint of getApiKeySkipHints(envVarName, provider)) {
1438
+ console.log(` \u2022 ${hint}`);
1440
1439
  }
1441
- console.log(` \u2022 Shell profile: echo 'export ${envVarName}=key' >> ${profileFile}`);
1442
- console.log(` \u2022 Dotenv file: echo '${envVarName}=key' >> ~/.config/gnosys/.env${RESET}`);
1440
+ console.log(`${RESET}`);
1443
1441
  }
1444
1442
  }
1445
1443
  }
@@ -30,8 +30,11 @@ export declare function getMarkerPath(): string;
30
30
  export declare function writeUpgradeMarker(version: string): void;
31
31
  export declare function readUpgradeMarker(): UpgradeMarker | null;
32
32
  /**
33
- * Returns true when the on-disk marker names a different version than the
33
+ * Returns true when the on-disk marker names a **newer** version than the
34
34
  * currently running binary. The caller (an MCP server) should exit cleanly
35
35
  * so the host respawns it against the upgraded global binary.
36
+ *
37
+ * Stale markers from an older install must not restart a newer binary (that
38
+ * caused an immediate exit loop after patch upgrades).
36
39
  */
37
40
  export declare function shouldRestartMcp(currentVersion: string): boolean;
@@ -47,14 +47,27 @@ export function readUpgradeMarker() {
47
47
  return null;
48
48
  }
49
49
  }
50
+ function compareSemver(a, b) {
51
+ const pa = a.split(".").map((x) => parseInt(x, 10) || 0);
52
+ const pb = b.split(".").map((x) => parseInt(x, 10) || 0);
53
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
54
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
55
+ if (d !== 0)
56
+ return d;
57
+ }
58
+ return 0;
59
+ }
50
60
  /**
51
- * Returns true when the on-disk marker names a different version than the
61
+ * Returns true when the on-disk marker names a **newer** version than the
52
62
  * currently running binary. The caller (an MCP server) should exit cleanly
53
63
  * so the host respawns it against the upgraded global binary.
64
+ *
65
+ * Stale markers from an older install must not restart a newer binary (that
66
+ * caused an immediate exit loop after patch upgrades).
54
67
  */
55
68
  export function shouldRestartMcp(currentVersion) {
56
69
  const marker = readUpgradeMarker();
57
70
  if (!marker)
58
71
  return false;
59
- return marker.version !== currentVersion;
72
+ return compareSemver(marker.version, currentVersion) > 0;
60
73
  }
@@ -11,6 +11,7 @@ import * as os from "os";
11
11
  import * as fs from "fs/promises";
12
12
  import { execFileSync } from "child_process";
13
13
  import { transcribeAudio, } from "./audioExtract.js";
14
+ import { formatFfmpegInstallHint } from "./platform.js";
14
15
  // ─── Helpers ────────────────────────────────────────────────────────────
15
16
  /**
16
17
  * Check that ffmpeg is installed and accessible.
@@ -22,10 +23,7 @@ function checkFfmpeg() {
22
23
  }
23
24
  catch {
24
25
  throw new Error("Video transcription requires ffmpeg to be installed.\n" +
25
- "Install it with:\n" +
26
- " macOS: brew install ffmpeg\n" +
27
- " Ubuntu: sudo apt install ffmpeg\n" +
28
- " Windows: winget install FFmpeg");
26
+ formatFfmpegInstallHint());
29
27
  }
30
28
  }
31
29
  /**
@@ -77,8 +77,9 @@ async function main() {
77
77
  out();
78
78
  out(" Get started:");
79
79
  out(" 1. gnosys setup configure LLM providers and preferences");
80
- out(" 2. gnosys init initialize gnosys in a project directory");
81
- out(" 3. gnosys status check project status");
80
+ out(" 2. gnosys setup ides wire MCP into your IDEs (once per machine)");
81
+ out(" 3. gnosys init initialize gnosys in a project directory");
82
+ out(" 4. gnosys status check project status");
82
83
  out();
83
84
  // If interactive, offer to run setup automatically
84
85
  if (isInteractive) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnosys",
3
- "version": "5.11.0",
3
+ "version": "5.11.2",
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",