gnosys 5.12.0 → 5.12.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.
Files changed (72) hide show
  1. package/dist/cli.js +48 -7
  2. package/dist/index.js +179 -10
  3. package/dist/lib/addCommand.js +0 -1
  4. package/dist/lib/archive.js +0 -2
  5. package/dist/lib/askCommand.js +1 -1
  6. package/dist/lib/attachCommand.d.ts +17 -0
  7. package/dist/lib/attachCommand.js +66 -0
  8. package/dist/lib/attachments.d.ts +43 -2
  9. package/dist/lib/attachments.js +81 -2
  10. package/dist/lib/chat/choose.js +2 -2
  11. package/dist/lib/clientReadOverlay.js +3 -0
  12. package/dist/lib/config.d.ts +1 -48
  13. package/dist/lib/configCommand.js +2 -2
  14. package/dist/lib/db.d.ts +16 -1
  15. package/dist/lib/db.js +216 -119
  16. package/dist/lib/dbWrite.d.ts +1 -1
  17. package/dist/lib/dearchiveCommand.js +1 -1
  18. package/dist/lib/docxExtract.js +1 -1
  19. package/dist/lib/dream.d.ts +8 -0
  20. package/dist/lib/dream.js +35 -1
  21. package/dist/lib/dreamLogCommand.js +1 -1
  22. package/dist/lib/dreamRunLog.d.ts +1 -1
  23. package/dist/lib/dreamRunLog.js +26 -4
  24. package/dist/lib/embeddings.js +0 -3
  25. package/dist/lib/exportProject.d.ts +3 -2
  26. package/dist/lib/exportProject.js +2 -1
  27. package/dist/lib/federated.js +1 -1
  28. package/dist/lib/hybridSearchCommand.js +1 -1
  29. package/dist/lib/importProject.js +2 -1
  30. package/dist/lib/llm.js +1 -1
  31. package/dist/lib/lock.d.ts +1 -1
  32. package/dist/lib/lock.js +5 -3
  33. package/dist/lib/migrate.js +0 -1
  34. package/dist/lib/multimodalIngest.js +1 -1
  35. package/dist/lib/platform.d.ts +0 -6
  36. package/dist/lib/platform.js +0 -28
  37. package/dist/lib/readCommand.js +11 -10
  38. package/dist/lib/remoteWizard.d.ts +1 -1
  39. package/dist/lib/remoteWizard.js +4 -4
  40. package/dist/lib/rulesGen.d.ts +8 -0
  41. package/dist/lib/rulesGen.js +16 -0
  42. package/dist/lib/search.d.ts +0 -2
  43. package/dist/lib/search.js +0 -7
  44. package/dist/lib/semanticSearchCommand.js +1 -1
  45. package/dist/lib/setup/sections/providers.js +56 -4
  46. package/dist/lib/setup/sections/routing.js +42 -5
  47. package/dist/lib/setup/sections/taskRoutingEditor.d.ts +1 -5
  48. package/dist/lib/setup/sections/taskRoutingEditor.js +0 -10
  49. package/dist/lib/setup/ui/header.js +0 -1
  50. package/dist/lib/setup/ui/status.d.ts +0 -1
  51. package/dist/lib/setup/ui/status.js +0 -2
  52. package/dist/lib/setup.d.ts +0 -15
  53. package/dist/lib/setup.js +13 -158
  54. package/dist/lib/staleCommand.js +2 -2
  55. package/dist/lib/syncClient.d.ts +0 -6
  56. package/dist/lib/syncClient.js +36 -14
  57. package/dist/lib/syncDoctorCommand.js +2 -2
  58. package/dist/lib/syncIngest.d.ts +11 -0
  59. package/dist/lib/syncIngest.js +24 -1
  60. package/dist/lib/syncIngestStartup.js +2 -2
  61. package/dist/lib/syncSnapshot.d.ts +2 -0
  62. package/dist/lib/syncSnapshot.js +4 -0
  63. package/dist/lib/syncStaging.d.ts +0 -2
  64. package/dist/lib/syncStaging.js +0 -2
  65. package/dist/lib/updateCommand.js +1 -1
  66. package/dist/lib/webBuildCommand.js +1 -1
  67. package/dist/lib/webIndex.js +0 -1
  68. package/dist/lib/webIngestCommand.js +1 -1
  69. package/dist/sandbox/client.js +1 -1
  70. package/dist/sandbox/manager.js +1 -14
  71. package/dist/sandbox/server.js +3 -5
  72. package/package.json +5 -2
package/dist/lib/dream.js CHANGED
@@ -23,7 +23,7 @@ import { createProvider } from "./llm.js";
23
23
  import { notifyDesktop } from "./desktopNotify.js";
24
24
  import { syncConfidenceToDb, auditToDb } from "./dbWrite.js";
25
25
  import { logError } from "./log.js";
26
- import { estimateCost, estimateTokens, fingerprintMemories, memoryWatermark, readDreamState, writeDreamState, } from "./dreamRunLog.js";
26
+ import { estimateCost, acquireDreamLock, estimateTokens, fingerprintMemories, memoryWatermark, readDreamState, writeDreamState, } from "./dreamRunLog.js";
27
27
  /** Layer 4 alert threshold: fire desktop notification at this many consecutive provider failures. */
28
28
  const DREAM_FAILURE_NOTIFY_THRESHOLD = 3;
29
29
  export const DEFAULT_DREAM_CONFIG = {
@@ -109,6 +109,30 @@ export class GnosysDreamEngine {
109
109
  phase.durationMs = Date.now() - startedAtMs;
110
110
  phase.memoryIdsTouched = Array.from(new Set(phase.memoryIdsTouched));
111
111
  phase.estimatedCostUsd = Math.round(phase.estimatedCostUsd * 1_000_000) / 1_000_000;
112
+ this.checkpointFingerprints();
113
+ }
114
+ /**
115
+ * v5.12.1 crash safety: persist analyzed fingerprints at every phase
116
+ * boundary, not only in finalize(). A crash mid-run previously lost all
117
+ * pendingFingerprints, so the next run re-analyzed (and re-paid for) the
118
+ * same memory sets and could double-create summaries. Checkpointing only
119
+ * merges fingerprints — lastRunAt / watermarks remain finalize()'s job.
120
+ */
121
+ checkpointFingerprints() {
122
+ if (Object.keys(this.pendingFingerprints).length === 0)
123
+ return;
124
+ try {
125
+ writeDreamState({
126
+ ...this.dreamState,
127
+ analyzedFingerprints: {
128
+ ...this.dreamState.analyzedFingerprints,
129
+ ...this.pendingFingerprints,
130
+ },
131
+ });
132
+ }
133
+ catch {
134
+ // Best-effort: a failed checkpoint only costs re-analysis on resume.
135
+ }
112
136
  }
113
137
  addTouched(phase, memoryIds) {
114
138
  for (const id of memoryIds) {
@@ -986,6 +1010,15 @@ export class DreamScheduler {
986
1010
  const idleMs = Date.now() - this.lastActivity;
987
1011
  const idleMinutes = idleMs / 60_000;
988
1012
  if (idleMinutes >= this.config.idleMinutes) {
1013
+ // v5.12.1: the in-memory `running` flag does not survive a sandbox
1014
+ // restart and cannot see a concurrent manual `gnosys dream`. Tie the
1015
+ // scheduler to the same cross-process file lock the CLI uses.
1016
+ const lock = acquireDreamLock();
1017
+ if (!lock.acquired) {
1018
+ console.error(`[dream] scheduler skipped: ${lock.reason}`);
1019
+ this.lastActivity = Date.now(); // back off a full idle window
1020
+ return;
1021
+ }
989
1022
  this.running = true;
990
1023
  try {
991
1024
  this.currentDream = this.engine.dream((phase, detail) => {
@@ -1002,6 +1035,7 @@ export class DreamScheduler {
1002
1035
  this.running = false;
1003
1036
  this.currentDream = null;
1004
1037
  this.lastActivity = Date.now(); // Reset idle timer after dream
1038
+ lock.release();
1005
1039
  }
1006
1040
  }
1007
1041
  }
@@ -8,7 +8,7 @@ export async function runDreamLogCommand(opts, context = {}) {
8
8
  process.exitCode = 1;
9
9
  return;
10
10
  }
11
- const limit = Math.max(1, parseInt(opts.last) || 20);
11
+ const limit = Math.max(1, parseInt(opts.last, 10) || 20);
12
12
  const sinceIso = opts.since ? `${opts.since}T00:00:00Z` : undefined;
13
13
  const runs = centralDb.getRecentDreamRuns(limit, {
14
14
  failuresOnly: !!opts.failuresOnly,
@@ -88,7 +88,7 @@ export interface DreamReadOptions {
88
88
  export declare function getDreamRunsPath(): string;
89
89
  export declare function getDreamStatePath(): string;
90
90
  export declare function getDreamLockPath(): string;
91
- export declare function acquireDreamLock(): {
91
+ export declare function acquireDreamLock(depth?: number): {
92
92
  acquired: true;
93
93
  release: () => void;
94
94
  } | {
@@ -26,7 +26,7 @@ export function getDreamStatePath() {
26
26
  export function getDreamLockPath() {
27
27
  return path.join(getGnosysHome(), "dream.lock");
28
28
  }
29
- export function acquireDreamLock() {
29
+ export function acquireDreamLock(depth = 0) {
30
30
  const lockPath = getDreamLockPath();
31
31
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
32
32
  try {
@@ -46,17 +46,39 @@ export function acquireDreamLock() {
46
46
  };
47
47
  }
48
48
  catch {
49
+ // Bounded retries so stale-lock cleanup can't recurse forever under
50
+ // genuine contention (two processes racing to re-acquire).
51
+ if (depth >= 3) {
52
+ return { acquired: false, reason: "dream lock contention" };
53
+ }
54
+ let raw;
55
+ try {
56
+ raw = fs.readFileSync(lockPath, "utf8");
57
+ }
58
+ catch {
59
+ // Lock vanished between our create attempt and this read — retry.
60
+ return acquireDreamLock(depth + 1);
61
+ }
49
62
  try {
50
- const raw = fs.readFileSync(lockPath, "utf8");
51
63
  const parsed = JSON.parse(raw);
52
64
  if (parsed.pid && !isProcessRunning(parsed.pid)) {
53
65
  fs.unlinkSync(lockPath);
54
- return acquireDreamLock();
66
+ return acquireDreamLock(depth + 1);
55
67
  }
56
68
  return { acquired: false, reason: `dream already running (pid ${parsed.pid || "unknown"})` };
57
69
  }
58
70
  catch {
59
- return { acquired: false, reason: "dream already running (lock exists)" };
71
+ // v5.12.1: unreadable/corrupt lock (crash mid-write) previously blocked
72
+ // dreaming forever until manual deletion. Treat it as stale: the lock
73
+ // payload is a single tiny write, so a live owner with a corrupt lock
74
+ // is implausible.
75
+ try {
76
+ fs.unlinkSync(lockPath);
77
+ }
78
+ catch {
79
+ // ignore — next acquire retries
80
+ }
81
+ return acquireDreamLock(depth + 1);
60
82
  }
61
83
  }
62
84
  }
@@ -5,7 +5,6 @@
5
5
  * Embeddings are stored in SQLite as regeneratable sidecar data.
6
6
  */
7
7
  // Dynamic import — gracefully handles missing native module (dlopen failures)
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
8
  let Database = null;
10
9
  try {
11
10
  Database = (await import("better-sqlite3")).default;
@@ -17,10 +16,8 @@ import path from "path";
17
16
  import fs from "fs/promises";
18
17
  import { enableWAL } from "./lock.js";
19
18
  const MODEL_NAME = "Xenova/all-MiniLM-L6-v2";
20
- const EMBEDDING_DIM = 384;
21
19
  export class GnosysEmbeddings {
22
20
  pipeline = null;
23
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
21
  db = null;
25
22
  storePath;
26
23
  modelReady = false;
@@ -14,9 +14,10 @@ interface BundleManifest {
14
14
  source_user: string;
15
15
  gnosys_version: string;
16
16
  }
17
- /** A memory row with its embedding base64-encoded for JSON transport. */
18
- export interface PortableMemory extends Omit<DbMemory, "embedding"> {
17
+ /** A memory row with its binary columns base64-encoded for JSON transport. */
18
+ export interface PortableMemory extends Omit<DbMemory, "embedding" | "attachment_data"> {
19
19
  embedding_b64: string | null;
20
+ attachment_data_b64: string | null;
20
21
  }
21
22
  export interface ProjectBundle {
22
23
  manifest: BundleManifest;
@@ -34,10 +34,11 @@ export function exportProject(db, opts) {
34
34
  const totalIncludingArchived = db.getMemoriesByProject(opts.projectId, true).length;
35
35
  const archivedExcluded = opts.includeArchived ? 0 : totalIncludingArchived - rawMemories.length;
36
36
  const memories = rawMemories.map((m) => {
37
- const { embedding: _embedding, ...rest } = m;
37
+ const { embedding: _embedding, attachment_data: _attachment, ...rest } = m;
38
38
  return {
39
39
  ...rest,
40
40
  embedding_b64: m.embedding ? Buffer.from(m.embedding).toString("base64") : null,
41
+ attachment_data_b64: m.attachment_data ? Buffer.from(m.attachment_data).toString("base64") : null,
41
42
  };
42
43
  });
43
44
  const memoryIds = rawMemories.map((m) => m.id);
@@ -132,7 +132,7 @@ export function detectAmbiguity(db, query, opts) {
132
132
  const projectHits = new Map();
133
133
  for (const r of results) {
134
134
  const mem = db.getMemory(r.id);
135
- if (!mem || !mem.project_id)
135
+ if (!mem?.project_id)
136
136
  continue;
137
137
  projectHits.set(mem.project_id, (projectHits.get(mem.project_id) || 0) + 1);
138
138
  }
@@ -67,7 +67,7 @@ export async function runHybridSearchCommand(getResolver, query, opts) {
67
67
  const embeddings = new GnosysEmbeddings(storePath);
68
68
  const hybridSearch = new GnosysHybridSearch(search, embeddings, resolver, storePath);
69
69
  const mode = opts.mode;
70
- const results = await hybridSearch.hybridSearch(query, parseInt(opts.limit), mode);
70
+ const results = await hybridSearch.hybridSearch(query, parseInt(opts.limit, 10), mode);
71
71
  if (results.length === 0) {
72
72
  outputResult(!!opts.json, { query, mode, results: [] }, () => {
73
73
  console.log(`No results for "${query}". Try gnosys reindex to build embeddings.`);
@@ -20,10 +20,11 @@ export function readBundle(bundlePath) {
20
20
  return bundle;
21
21
  }
22
22
  function portableToDbMemory(p) {
23
- const { embedding_b64, ...rest } = p;
23
+ const { embedding_b64, attachment_data_b64, ...rest } = p;
24
24
  return {
25
25
  ...rest,
26
26
  embedding: embedding_b64 ? Buffer.from(embedding_b64, "base64") : null,
27
+ attachment_data: attachment_data_b64 ? Buffer.from(attachment_data_b64, "base64") : null,
27
28
  };
28
29
  }
29
30
  /** Restore a bundle into the central DB. Returns counts and the final project ID. */
package/dist/lib/llm.js CHANGED
@@ -487,7 +487,7 @@ export function createProvider(provider, model, config, task) {
487
487
  }
488
488
  case "custom": {
489
489
  const customConfig = config.llm.custom;
490
- if (!customConfig || !customConfig.baseUrl || !customConfig.model) {
490
+ if (!customConfig?.baseUrl || !customConfig.model) {
491
491
  throw new Error("Custom provider not configured. Set llm.custom.baseUrl and llm.custom.model in gnosys.json, or use: gnosys config set provider custom");
492
492
  }
493
493
  const apiKey = getCustomApiKey(config);
@@ -29,4 +29,4 @@ export declare function acquireWriteLock(storePath: string, operation?: string):
29
29
  * unboundedly until something triggers a manual checkpoint — we observed
30
30
  * 4MB+ WAL files in the wild with no checkpoint cadence in v5.4.0.
31
31
  */
32
- export declare function enableWAL(db: any): void;
32
+ export declare function enableWAL(db: any, busyTimeoutMs?: number): void;
package/dist/lib/lock.js CHANGED
@@ -134,11 +134,13 @@ function isLockStale(lock) {
134
134
  * unboundedly until something triggers a manual checkpoint — we observed
135
135
  * 4MB+ WAL files in the wild with no checkpoint cadence in v5.4.0.
136
136
  */
137
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
- export function enableWAL(db) {
137
+ export function enableWAL(db, busyTimeoutMs = 5000) {
139
138
  try {
140
139
  db.pragma("journal_mode = WAL");
141
- db.pragma("busy_timeout = 5000"); // Wait up to 5s if DB is busy
140
+ // Single source of truth for the connection's busy timeout callers on
141
+ // network shares (central DB) pass a longer value (10s) instead of
142
+ // overriding with a second pragma afterwards.
143
+ db.pragma(`busy_timeout = ${Math.max(0, Math.floor(busyTimeoutMs))}`);
142
144
  // Auto-checkpoint after every 1000 frames written to WAL. Default is
143
145
  // 1000 anyway in newer SQLite, but set explicitly so behavior is
144
146
  // predictable across SQLite versions.
@@ -10,7 +10,6 @@ import { GnosysDB, fnv1a } from "./db.js";
10
10
  import { GnosysStore } from "./store.js";
11
11
  import { GnosysArchive } from "./archive.js";
12
12
  // Dynamic import for embeddings DB
13
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
13
  let Database = null;
15
14
  try {
16
15
  Database = (await import("better-sqlite3")).default;
@@ -143,7 +143,7 @@ function buildTranscriptChunks(segments, targetSize) {
143
143
  */
144
144
  export async function ingestFile(options) {
145
145
  const startTime = Date.now();
146
- const { filePath, storePath, mode = "llm", author = "human", authority = "imported", dryRun = false, projectRoot, onProgress, } = options;
146
+ const { filePath, storePath, mode = "llm", author = "human", authority = "imported", dryRun = false, onProgress, } = options;
147
147
  // Step 1: Detect file type
148
148
  const fileInfo = await detectFileType(filePath);
149
149
  // Reject unsupported types early
@@ -4,16 +4,10 @@
4
4
  export type OsFamily = "macos" | "linux" | "windows";
5
5
  /** Current OS family for CLI messages and help text. */
6
6
  export declare function getOsFamily(): OsFamily;
7
- /** Primary secure credential store name on this machine. */
8
- export declare function getSecureStorageLabel(): string;
9
7
  /** Short phrase for error messages (setup may still be required on Windows). */
10
8
  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
9
  /** Claude Desktop MCP config file path for the current platform. */
14
10
  export declare function getClaudeDesktopConfigPath(): string;
15
- /** Display path with ~ for home (for logs and help). */
16
- export declare function displayClaudeDesktopConfigPath(): string;
17
11
  /** Shell profile file(s) suggested for env vars on this OS. */
18
12
  export declare function getShellProfileHint(): string;
19
13
  /** Lines shown when user skips API key setup in gnosys setup. */
@@ -11,17 +11,6 @@ export function getOsFamily() {
11
11
  return "windows";
12
12
  return "linux";
13
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
14
  /** Short phrase for error messages (setup may still be required on Windows). */
26
15
  export function getSecureStorageSetupHint() {
27
16
  switch (getOsFamily()) {
@@ -33,17 +22,6 @@ export function getSecureStorageSetupHint() {
33
22
  return "your user environment or ~/.config/gnosys/.env (via gnosys setup)";
34
23
  }
35
24
  }
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
25
  /** Claude Desktop MCP config file path for the current platform. */
48
26
  export function getClaudeDesktopConfigPath() {
49
27
  const home = os.homedir();
@@ -56,12 +34,6 @@ export function getClaudeDesktopConfigPath() {
56
34
  }
57
35
  return path.join(home, ".config", "Claude", "claude_desktop_config.json");
58
36
  }
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
37
  /** Shell profile file(s) suggested for env vars on this OS. */
66
38
  export function getShellProfileHint() {
67
39
  switch (getOsFamily()) {
@@ -1,5 +1,4 @@
1
1
  import fs from "fs/promises";
2
- import { GnosysDB } from "./db.js";
3
2
  function outputResult(json, data, humanFn) {
4
3
  if (json) {
5
4
  console.log(JSON.stringify(data, null, 2));
@@ -9,11 +8,14 @@ function outputResult(json, data, humanFn) {
9
8
  }
10
9
  }
11
10
  export async function runReadCommand(getResolver, memoryPath, opts) {
12
- const centralDb = GnosysDB.openCentral();
13
- if (centralDb.isAvailable()) {
14
- const dbMem = centralDb.getMemory(memoryPath);
15
- if (dbMem) {
16
- try {
11
+ // v13 client read path: master/snapshot routing + pending-offline-adds overlay,
12
+ // so `gnosys read <id>` sees the same memories as list/discover/search.
13
+ const { resolveClientRead, getMemoryWithOverlay } = await import("./clientReadResolve.js");
14
+ const resolved = resolveClientRead();
15
+ if (resolved) {
16
+ try {
17
+ const dbMem = getMemoryWithOverlay(resolved, memoryPath);
18
+ if (dbMem) {
17
19
  const tags = dbMem.tags || "[]";
18
20
  const headerLines = [
19
21
  `---`,
@@ -42,12 +44,11 @@ export async function runReadCommand(getResolver, memoryPath, opts) {
42
44
  });
43
45
  return;
44
46
  }
45
- finally {
46
- centralDb.close();
47
- }
47
+ }
48
+ finally {
49
+ resolved.release();
48
50
  }
49
51
  }
50
- centralDb.close();
51
52
  const resolver = await getResolver();
52
53
  const memory = await resolver.readMemory(memoryPath);
53
54
  if (!memory) {
@@ -24,7 +24,7 @@ export declare function configureFromPath(centralDb: GnosysDB, remotePath: strin
24
24
  migrate?: boolean;
25
25
  role?: MultiMachineRole;
26
26
  }): Promise<boolean>;
27
- export { stagingDirForMachine, clientPresencePath } from "./syncStaging.js";
27
+ export { machineStagingDir as stagingDirForMachine, clientPresencePath } from "./syncStaging.js";
28
28
  export declare const __test: {
29
29
  matchesTypedPhrase: typeof matchesTypedPhrase;
30
30
  detectClonedStagingPresence: typeof detectClonedStagingPresence;
@@ -6,7 +6,7 @@
6
6
  * - Reconfigure: change path, re-validate, or disconnect (when already configured)
7
7
  */
8
8
  import { randomUUID } from "crypto";
9
- import { existsSync, mkdirSync, renameSync } from "fs";
9
+ import { existsSync, mkdirSync, renameSync, } from "fs";
10
10
  import * as path from "path";
11
11
  import { createInterface } from "readline/promises";
12
12
  import { GnosysDB } from "./db.js";
@@ -16,7 +16,7 @@ import { getGnosysHome } from "./paths.js";
16
16
  import { atomicWriteFileSync } from "./atomicWrite.js";
17
17
  import { readMasterMarker, writeMasterMarker } from "./masterLease.js";
18
18
  import { checkMasterPathLocalDisk, LOCAL_DISK_ACK_PHRASE, } from "./localDiskCheck.js";
19
- import { stagingDirForMachine, clientPresencePath, machineStagingDir } from "./syncStaging.js";
19
+ import { clientPresencePath, machineStagingDir } from "./syncStaging.js";
20
20
  import { safeQuestion } from "./setup/ui/safePrompt.js";
21
21
  import { Spinner } from "./setup/ui/spinner.js";
22
22
  import { printStatus } from "./setup/ui/status.js";
@@ -500,10 +500,10 @@ export async function configureFromPath(centralDb, remotePath, opts = {}) {
500
500
  }
501
501
  return true;
502
502
  }
503
- export { stagingDirForMachine, clientPresencePath } from "./syncStaging.js";
503
+ export { machineStagingDir as stagingDirForMachine, clientPresencePath } from "./syncStaging.js";
504
504
  export const __test = {
505
505
  matchesTypedPhrase,
506
506
  detectClonedStagingPresence,
507
- stagingDirForMachine,
507
+ stagingDirForMachine: machineStagingDir,
508
508
  clientPresencePath,
509
509
  };
@@ -47,6 +47,14 @@ export declare function syncRules(centralDb: GnosysDB, projectDir: string, agent
47
47
  * If target is "global", syncs to ~/.claude/CLAUDE.md.
48
48
  */
49
49
  export declare function syncToTarget(centralDb: GnosysDB, projectDir: string, target: string, projectId: string | null): Promise<RulesGenResult[]>;
50
+ /**
51
+ * Remove the GNOSYS block from every known agent rules file in a project —
52
+ * the uninstall counterpart of syncToTarget (v5.12.1: wired into
53
+ * `gnosys cleanup --rules`). Checks every known target rather than only
54
+ * detected ones, so leftovers survive even if the agent dir was removed.
55
+ * Returns the relative paths actually cleaned.
56
+ */
57
+ export declare function removeRulesFromProject(projectDir: string): Promise<string[]>;
50
58
  /**
51
59
  * Remove the GNOSYS block from a rules file (cleanup).
52
60
  */
@@ -254,6 +254,22 @@ export async function syncToTarget(centralDb, projectDir, target, projectId) {
254
254
  }
255
255
  return results;
256
256
  }
257
+ /**
258
+ * Remove the GNOSYS block from every known agent rules file in a project —
259
+ * the uninstall counterpart of syncToTarget (v5.12.1: wired into
260
+ * `gnosys cleanup --rules`). Checks every known target rather than only
261
+ * detected ones, so leftovers survive even if the agent dir was removed.
262
+ * Returns the relative paths actually cleaned.
263
+ */
264
+ export async function removeRulesFromProject(projectDir) {
265
+ const cleaned = [];
266
+ for (const relPath of Object.values(TARGET_PATHS)) {
267
+ const filePath = path.join(projectDir, relPath);
268
+ if (await removeRulesBlock(filePath))
269
+ cleaned.push(relPath);
270
+ }
271
+ return cleaned;
272
+ }
257
273
  /**
258
274
  * Remove the GNOSYS block from a rules file (cleanup).
259
275
  */
@@ -17,8 +17,6 @@ export interface DiscoverResult {
17
17
  }
18
18
  export declare class GnosysSearch {
19
19
  private db;
20
- private storePath;
21
- private available;
22
20
  constructor(storePath: string);
23
21
  private initSchema;
24
22
  /**
@@ -3,7 +3,6 @@
3
3
  * FTS5-based search and discovery across all Gnosys stores.
4
4
  */
5
5
  // Dynamic import — gracefully handles missing native module (dlopen failures)
6
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
6
  let Database = null;
8
7
  try {
9
8
  Database = (await import("better-sqlite3")).default;
@@ -13,12 +12,8 @@ catch {
13
12
  }
14
13
  import path from "path";
15
14
  export class GnosysSearch {
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
15
  db = null;
18
- storePath;
19
- available = false;
20
16
  constructor(storePath) {
21
- this.storePath = storePath;
22
17
  if (!Database) {
23
18
  // Native module not available — search features disabled
24
19
  return;
@@ -34,7 +29,6 @@ export class GnosysSearch {
34
29
  this.initSchema();
35
30
  // Smoke-test: insert + delete to confirm journal ops work
36
31
  this.db.exec("CREATE TABLE IF NOT EXISTS _write_test (v INTEGER); INSERT INTO _write_test VALUES (1); DELETE FROM _write_test; DROP TABLE _write_test;");
37
- this.available = true;
38
32
  }
39
33
  catch {
40
34
  // Fallback to in-memory (works everywhere, rebuilt on each start)
@@ -44,7 +38,6 @@ export class GnosysSearch {
44
38
  catch { /* ignore */ }
45
39
  this.db = new Database(":memory:");
46
40
  this.initSchema();
47
- this.available = true;
48
41
  }
49
42
  }
50
43
  initSchema() {
@@ -24,7 +24,7 @@ export async function runSemanticSearchCommand(getResolver, query, opts) {
24
24
  const { GnosysHybridSearch } = await import("./hybridSearch.js");
25
25
  const embeddings = new GnosysEmbeddings(storePath);
26
26
  const hybridSearch = new GnosysHybridSearch(search, embeddings, resolver, storePath);
27
- const results = await hybridSearch.hybridSearch(query, parseInt(opts.limit), "semantic");
27
+ const results = await hybridSearch.hybridSearch(query, parseInt(opts.limit, 10), "semantic");
28
28
  outputResult(!!opts.json, {
29
29
  query,
30
30
  count: results.length,
@@ -129,14 +129,67 @@ async function manageProviderKeys(rl, storePath, cfg, provider) {
129
129
  }
130
130
  console.log("");
131
131
  }
132
+ const defaultAction = slots.length > 0 ? 2 : 0; // when they have a key, default to "make default + model"
132
133
  const choice = await askChoice(rl, "Actions", [
133
134
  slots.length === 0 ? "Add API key (global)" : "Rotate global API key",
134
135
  "Delete one stored key…",
136
+ "Use this provider as default for everything + pick model",
135
137
  "Back",
136
- ], 2);
137
- if (choice === 2) {
138
+ ], defaultAction);
139
+ if (choice === 3) {
138
140
  return changed;
139
141
  }
142
+ if (choice === 2) {
143
+ // "Use this provider as default for everything + pick model"
144
+ // Set it as the global default provider
145
+ const current = await loadConfig(storePath);
146
+ await updateConfig(storePath, {
147
+ llm: {
148
+ ...current.llm,
149
+ defaultProvider: provider,
150
+ },
151
+ // Clear per-task overrides so this truly becomes the single default for everything
152
+ taskModels: {},
153
+ });
154
+ printStatus("ok", `default provider set to ${provider} (all tasks now default to it)`);
155
+ // Immediately let them pick a model for it (using the same picker the main flow uses)
156
+ try {
157
+ const { fetchDynamicModels } = await import("../../setup.js");
158
+ const { pickModel } = await import("../../setup.js");
159
+ const dynamicModels = await fetchDynamicModels();
160
+ const tiers = dynamicModels[provider] ?? [];
161
+ let chosenModel;
162
+ if (tiers.length > 0) {
163
+ chosenModel = await pickModel(rl, provider, dynamicModels, `Default model for ${provider} (used for all tasks)`, current.llm[provider]?.model);
164
+ }
165
+ else {
166
+ chosenModel = await ask(rl, `Enter default model name for ${provider}: `);
167
+ }
168
+ if (chosenModel) {
169
+ const after = await loadConfig(storePath);
170
+ await updateConfig(storePath, {
171
+ llm: {
172
+ ...after.llm,
173
+ [provider]: {
174
+ ...(after.llm[provider] || {}),
175
+ model: chosenModel,
176
+ },
177
+ },
178
+ });
179
+ printStatus("ok", `default model set · ${provider} / ${chosenModel}`);
180
+ printStatus("progress", "all tasks will now default to this provider + model", "use task routing for per-task overrides");
181
+ changed = true;
182
+ }
183
+ else {
184
+ printStatus("progress", "model left unchanged — you can set it from task routing");
185
+ }
186
+ }
187
+ catch (_err) {
188
+ printStatus("warn", "could not pick model right now", "you can set a default model from the main setup → task routing");
189
+ }
190
+ cfg = await loadConfig(storePath);
191
+ continue;
192
+ }
140
193
  if (choice === 0) {
141
194
  const service = apiKeyServiceName(provider, "global");
142
195
  console.log("");
@@ -188,7 +241,6 @@ async function manageProviderKeys(rl, storePath, cfg, provider) {
188
241
  printStatus("fail", "delete failed or entry not found");
189
242
  }
190
243
  }
191
- continue;
192
244
  }
193
245
  }
194
246
  }
@@ -207,7 +259,7 @@ export async function runProvidersSetup(opts) {
207
259
  console.log("");
208
260
  console.log(Header(["gnosys", "setup", "providers"]));
209
261
  console.log("");
210
- console.log(Title("Providers", "API keys live here · pick task routing separately for models per task"));
262
+ console.log(Title("Providers", "API keys live here. Set a global default (or per-task models) from the main setup menu."));
211
263
  console.log("");
212
264
  for (let i = 0; i < menuProviders.length; i++) {
213
265
  const p = menuProviders[i];