gnosys 5.15.3 → 5.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,7 +51,7 @@ That's the 60-second tour. **Everything else lives on [gnosys.ai](https://gnosys
51
51
  - **Central brain** — one `~/.gnosys/gnosys.db` unifies every project (project / user / global scopes). Sub-10ms reads, SQLite as the sole source of truth.
52
52
  - **Federated search** — tier-boosted hybrid (FTS5 keyword + semantic) search across scopes, with recency and reinforcement.
53
53
  - **MCP server** — `gnosys serve` exposes 50+ memory tools to any MCP client. Sandbox-first runtime keeps context cost near zero.
54
- - **Web Knowledge Base** — `gnosys web build` turns any site into a searchable index for serverless chatbots. Zero runtime deps.
54
+ - **Web Knowledge Base** — `gnosys web build` turns any site into a searchable index for serverless chatbots, with optional build-time vectors for serverless-safe hybrid semantic search. Zero runtime deps.
55
55
  - **Dream Mode** — idle-time consolidation: confidence decay, summaries, relationship discovery. Never deletes — only suggests.
56
56
  - **Multi-machine sync** — share your brain across machines; conflict detection with skip-and-flag resolution.
57
57
  - **Obsidian export** — `gnosys export` regenerates a full vault with frontmatter, `[[wikilinks]]`, and graph data.
package/dist/cli.js CHANGED
@@ -1788,6 +1788,9 @@ webCmd
1788
1788
  .option("--input <dir>", "Override knowledge directory")
1789
1789
  .option("--output <path>", "Override output file path")
1790
1790
  .option("--no-stop-words", "Disable stop-word filtering")
1791
+ .option("--embeddings <provider>", "Also build gnosys-vectors.json (openai|voyage|local)")
1792
+ .option("--embed-model <id>", "Override the embedding model")
1793
+ .option("--no-expansions", "Skip LLM concept-expansion generation")
1791
1794
  .option("--json", "Output index stats as JSON")
1792
1795
  .action(async (opts) => {
1793
1796
  const { runWebBuildIndexCommand } = await import("./lib/webBuildIndexCommand.js");
@@ -1801,6 +1804,9 @@ webCmd
1801
1804
  .option("--no-llm", "Force structured mode (no LLM)")
1802
1805
  .option("--concurrency <n>", "Parallel processing limit", "3")
1803
1806
  .option("--dry-run", "Show what would change without writing files")
1807
+ .option("--embeddings <provider>", "Also build gnosys-vectors.json (openai|voyage|local)")
1808
+ .option("--embed-model <id>", "Override the embedding model")
1809
+ .option("--no-expansions", "Skip LLM concept-expansion generation")
1804
1810
  .option("--json", "Output results as JSON")
1805
1811
  .action(async (opts) => {
1806
1812
  const { runWebBuildCommand } = await import("./lib/webBuildCommand.js");
package/dist/index.js CHANGED
@@ -624,7 +624,7 @@ regTool("gnosys_list", "List memories across all stores, optionally filtered by
624
624
  }
625
625
  });
626
626
  // ─── Tool: gnosys_add ────────────────────────────────────────────────────
627
- regTool("gnosys_add", "Add a new memory from raw text the server's LLM structures it into an atomic memory. For non-agent callers (scripts, cron); if you are an LLM agent, use gnosys_add_structured instead to avoid a redundant server-side LLM call. Writes to the project store by default. Use store='personal' for cross-project knowledge, or store='global' to explicitly write to shared org knowledge.", {
627
+ regTool("gnosys_add", "DO NOT USE if you are an LLM agent call gnosys_add_structured instead (you structure the memory yourself; no redundant server-side LLM call). This freeform tool is REJECTED by default over MCP and only works when the operator sets GNOSYS_ALLOW_FREEFORM_ADD=1 for genuine non-agent callers (scripts, cron). Writes to the project store by default. Use store='personal' for cross-project knowledge, or store='global' to explicitly write to shared org knowledge.", {
628
628
  input: z
629
629
  .string()
630
630
  .describe("Raw text input. Can be a decision, concept, fact, observation, or any knowledge."),
@@ -642,6 +642,29 @@ regTool("gnosys_add", "Add a new memory from raw text — the server's LLM struc
642
642
  .describe("Epistemic trust level"),
643
643
  projectRoot: projectRootParam,
644
644
  }, async ({ input, store: targetStore, author, authority, projectRoot }) => {
645
+ // Hard gate (2026-07-04 decision): every MCP caller in practice is an
646
+ // LLM agent, and agents must use gnosys_add_structured — the freeform
647
+ // path burns a second server-side LLM call and silently fails without
648
+ // a provider key. Reject by default with a redirect the agent can act
649
+ // on immediately. Genuine non-agent MCP callers opt in via env.
650
+ if (process.env.GNOSYS_ALLOW_FREEFORM_ADD !== "1") {
651
+ return {
652
+ content: [
653
+ {
654
+ type: "text",
655
+ text: "gnosys_add is disabled for LLM agents. Call gnosys_add_structured instead — " +
656
+ "structure the memory yourself and pass: title (string), category " +
657
+ "(architecture|decisions|requirements|concepts|roadmap|landscape|open-questions), " +
658
+ "tags (object of string arrays, e.g. { domain: ['auth'], type: ['decision'] }), " +
659
+ "content (markdown body), relevance (space-separated keyword cloud for discovery), " +
660
+ "plus the same store/author/authority/projectRoot values you passed here. " +
661
+ "Retry now with gnosys_add_structured using the same input. " +
662
+ "(Non-agent scripts can re-enable this tool with GNOSYS_ALLOW_FREEFORM_ADD=1.)",
663
+ },
664
+ ],
665
+ isError: true,
666
+ };
667
+ }
645
668
  const ctx = await resolveToolContext(projectRoot);
646
669
  const writeTarget = ctx.resolver.getWriteTarget(targetStore || undefined);
647
670
  if (!writeTarget) {
@@ -972,7 +995,7 @@ regTool("gnosys_init", "Initialize Gnosys in a project directory. Creates .gnosy
972
995
  content: [
973
996
  {
974
997
  type: "text",
975
- text: `Gnosys store ${action} at ${storePath}\n\nProject Identity:\n- ID: ${identity.projectId}\n- Name: ${identity.projectName}\n- Directory: ${identity.workingDirectory}\n- Agent rules target: ${identity.agentRulesTarget || "none detected"}\n- Central DB: ${centralDb?.isAvailable() ? "registered ✓" : "not available"}\n\n${isResync ? "Identity re-synced." : "Created:\n- gnosys.json (project identity)\n- .config/ (internal config)\n- tags.json (tag registry)"}${hookInfo}\n\nThe store is ready. Use gnosys_discover to find existing memories or gnosys_add to create new ones.`,
998
+ text: `Gnosys store ${action} at ${storePath}\n\nProject Identity:\n- ID: ${identity.projectId}\n- Name: ${identity.projectName}\n- Directory: ${identity.workingDirectory}\n- Agent rules target: ${identity.agentRulesTarget || "none detected"}\n- Central DB: ${centralDb?.isAvailable() ? "registered ✓" : "not available"}\n\n${isResync ? "Identity re-synced." : "Created:\n- gnosys.json (project identity)\n- .config/ (internal config)\n- tags.json (tag registry)"}${hookInfo}\n\nThe store is ready. Use gnosys_discover to find existing memories or gnosys_add_structured to create new ones.`,
976
999
  },
977
1000
  ],
978
1001
  };
@@ -2216,7 +2239,7 @@ regTool("gnosys_stores", "Debug tool — lists all detected Gnosys stores across
2216
2239
  // Usage hint
2217
2240
  lines.push("USAGE:");
2218
2241
  lines.push(" Pass projectRoot to any tool to target a specific project:");
2219
- lines.push(' e.g. gnosys_add({ projectRoot: "/path/to/my-project", ... })');
2242
+ lines.push(' e.g. gnosys_add_structured({ projectRoot: "/path/to/my-project", ... })');
2220
2243
  return { content: [{ type: "text", text: lines.join("\n") }] };
2221
2244
  }
2222
2245
  catch (err) {
@@ -21,28 +21,13 @@
21
21
  import fs from "fs/promises";
22
22
  import fsSync from "fs";
23
23
  import path from "path";
24
- import os from "os";
25
24
  import { createInterface } from "readline/promises";
26
25
  import { stdin, stdout } from "process";
27
- import { getProjectRegistryPath } from "./paths.js";
26
+ import { getProjectRegistryPath, isTempProjectPath } from "./paths.js";
28
27
  import { safeQuestion } from "./setup/ui/safePrompt.js";
29
28
  import { Header } from "./setup/ui/header.js";
30
29
  import { Status } from "./setup/ui/status.js";
31
30
  import { c, color, glyph } from "./setup/ui/tokens.js";
32
- const TEMP_PREFIXES = [
33
- "/tmp/",
34
- "/private/tmp/",
35
- "/var/folders/",
36
- "/private/var/folders/",
37
- ];
38
- function isTempPath(p) {
39
- const resolved = path.resolve(p);
40
- if (TEMP_PREFIXES.some((prefix) => resolved.startsWith(prefix)))
41
- return true;
42
- // Honor the system tmpdir too (covers $TMPDIR overrides on CI etc.).
43
- const sysTmp = path.resolve(os.tmpdir()) + path.sep;
44
- return resolved.startsWith(sysTmp);
45
- }
46
31
  function isAlive(p) {
47
32
  try {
48
33
  const stat = fsSync.statSync(path.join(p, ".gnosys"));
@@ -74,7 +59,7 @@ export async function classifyRegistryEntries() {
74
59
  const dead = [];
75
60
  const temp = [];
76
61
  for (const e of entries) {
77
- if (isTempPath(e)) {
62
+ if (isTempProjectPath(e)) {
78
63
  temp.push(e);
79
64
  }
80
65
  else if (isAlive(e)) {
@@ -40,3 +40,4 @@ export declare function getProjectRegistryPath(): string;
40
40
  * machine-local counterpart. See machineConfig.ts.
41
41
  */
42
42
  export declare function getMachineConfigPath(): string;
43
+ export declare function isTempProjectPath(p: string): boolean;
package/dist/lib/paths.js CHANGED
@@ -23,6 +23,7 @@
23
23
  * reproduce the `path.join(os.homedir(), ".gnosys")` pattern inline.
24
24
  */
25
25
  import path from "path";
26
+ import os from "os";
26
27
  export function getGnosysHome() {
27
28
  if (process.env.GNOSYS_HOME)
28
29
  return process.env.GNOSYS_HOME;
@@ -45,6 +46,13 @@ export function getSandboxDir() {
45
46
  function getConfigDir() {
46
47
  if (process.env.GNOSYS_CONFIG_DIR)
47
48
  return process.env.GNOSYS_CONFIG_DIR;
49
+ // GNOSYS_HOME redirects "everything gnosys-owned" (see header comment).
50
+ // Before this, tests that set GNOSYS_HOME still wrote the project
51
+ // registry into the user's REAL ~/.config/gnosys/projects.json — every
52
+ // test run leaked hundreds of /var/folders/... temp entries into it.
53
+ if (process.env.GNOSYS_HOME) {
54
+ return path.join(process.env.GNOSYS_HOME, "config");
55
+ }
48
56
  const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
49
57
  return path.join(home, ".config", "gnosys");
50
58
  }
@@ -65,3 +73,24 @@ export function getProjectRegistryPath() {
65
73
  export function getMachineConfigPath() {
66
74
  return path.join(getConfigDir(), "machine.json");
67
75
  }
76
+ /**
77
+ * True when a project path lives under a system temp location (/tmp,
78
+ * /var/folders, or os.tmpdir()). Such paths are transient — test
79
+ * scaffolding, throwaway scratch dirs — and must never be treated as
80
+ * durable registered projects. Shared by cleanup, registration, and
81
+ * sync-projects so all three agree on what "temp" means.
82
+ */
83
+ const TEMP_PATH_PREFIXES = [
84
+ "/tmp/",
85
+ "/private/tmp/",
86
+ "/var/folders/",
87
+ "/private/var/folders/",
88
+ ];
89
+ export function isTempProjectPath(p) {
90
+ const resolved = path.resolve(p);
91
+ if (TEMP_PATH_PREFIXES.some((prefix) => resolved.startsWith(prefix))) {
92
+ return true;
93
+ }
94
+ const sysTmp = path.resolve(os.tmpdir()) + path.sep;
95
+ return resolved.startsWith(sysTmp);
96
+ }
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import fs from "fs/promises";
11
11
  import path from "path";
12
- import { getProjectRegistryPath } from "./paths.js";
12
+ import { getProjectRegistryPath, isTempProjectPath } from "./paths.js";
13
13
  import { GnosysStore } from "./store.js";
14
14
  /**
15
15
  * v5.9.1 (#98): read just the projectId from a project's gnosys.json
@@ -273,6 +273,16 @@ export class GnosysResolver {
273
273
  */
274
274
  async registerProject(projectDir) {
275
275
  const configPath = this.getRegistryPath();
276
+ // Never register temp-dir projects (/tmp, /var/folders, os.tmpdir())
277
+ // in the user's REAL registry — those are test scaffolding or scratch
278
+ // dirs that vanish, then linger as stale entries `gnosys setup
279
+ // sync-projects` counts forever. When GNOSYS_HOME / GNOSYS_CONFIG_DIR
280
+ // redirects the registry (tests, sandboxes), temp paths are allowed,
281
+ // since the whole registry is itself transient.
282
+ const registryIsIsolated = Boolean(process.env.GNOSYS_HOME) || Boolean(process.env.GNOSYS_CONFIG_DIR);
283
+ if (!registryIsIsolated && isTempProjectPath(projectDir)) {
284
+ return;
285
+ }
276
286
  let projects = [];
277
287
  try {
278
288
  const raw = await fs.readFile(configPath, "utf-8");
@@ -35,7 +35,7 @@ This project uses **Gnosys** for persistent memory via MCP. Gnosys uses a centra
35
35
 
36
36
  ### Write automatically
37
37
 
38
- - **Always write with \`gnosys_add_structured\`** — you are an LLM, so structure the memory yourself (title, category, tags, content, relevance) and pass the explicit fields. Do NOT use the freeform \`gnosys_add\`: it makes the Gnosys server run a *second, redundant* LLM call to structure your text and adds an external provider-key dependency that can fail silently. \`gnosys_add_structured\` makes no server-side model call. (Freeform \`gnosys_add\` exists only for non-agent callers — cron jobs, scripts that cannot structure text themselves.)
38
+ - **Always write with \`gnosys_add_structured\`** — you are an LLM, so structure the memory yourself (title, category, tags, content, relevance) and pass the explicit fields. NEVER call the freeform \`gnosys_add\`: the server **rejects it with an error** for MCP callers (it would run a second, redundant LLM call and depends on a provider key that can fail silently). \`gnosys_add_structured\` makes no server-side model call. (Freeform \`gnosys_add\` exists only for non-agent scripts/cron, which must set GNOSYS_ALLOW_FREEFORM_ADD=1.)
39
39
  - When user says "remember", "memorize", "save this", "note this down", "don't forget" — call \`gnosys_add_structured\`
40
40
  - When user states a decision or preference (even casually) — commit to \`decisions\` category
41
41
  - When user provides a spec or plan — commit BEFORE starting work
@@ -48,7 +48,7 @@ This project uses **Gnosys** for persistent memory via MCP. Gnosys uses a centra
48
48
  |--------|------|
49
49
  | Find memories | \`gnosys_discover\` (metadata) → \`gnosys_read\` (content) |
50
50
  | Search | \`gnosys_hybrid_search\` (best), \`gnosys_federated_search\` (cross-project), \`gnosys_search\` (keyword), \`gnosys_ask\` (Q&A) |
51
- | Write | \`gnosys_add_structured\` (explicit fields — **always use this as an agent**; no server-side LLM call). \`gnosys_add\` (freeform) is for non-agent callers only. |
51
+ | Write | \`gnosys_add_structured\` (explicit fields — **always use this as an agent**; no server-side LLM call). \`gnosys_add\` (freeform) is rejected for MCP callers — non-agent scripts only. |
52
52
  | Update | \`gnosys_update\`, \`gnosys_reinforce\` (useful/not_relevant/outdated) |
53
53
  | Browse | \`gnosys_list\`, \`gnosys_lens\` (filtered), \`gnosys_tags\`, \`gnosys_graph\` |
54
54
  | Maintain | \`gnosys_maintain\`, \`gnosys_stale\`, \`gnosys_history\`, \`gnosys_dashboard\` |
@@ -22,7 +22,11 @@ export async function runSetupSyncProjectsCommand(opts) {
22
22
  console.log("");
23
23
  // 1. Read registered projects from file registry AND central DB
24
24
  const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
25
- const registryPath = path.join(home, ".config", "gnosys", "projects.json");
25
+ const { getProjectRegistryPath, isTempProjectPath } = await import("./paths.js");
26
+ // Registry lives at ~/.config/gnosys/projects.json (or under
27
+ // GNOSYS_HOME/GNOSYS_CONFIG_DIR when isolated) — always resolved via
28
+ // the shared helper, never hand-rolled from HOME.
29
+ const registryPath = getProjectRegistryPath();
26
30
  let fileProjects = [];
27
31
  try {
28
32
  fileProjects = JSON.parse(await fs.readFile(registryPath, "utf-8"));
@@ -39,24 +43,46 @@ export async function runSetupSyncProjectsCommand(opts) {
39
43
  const centralDb = GnosysDB.openCentral();
40
44
  if (centralDb.isAvailable()) {
41
45
  const allProjects = centralDb.getAllProjects();
42
- dbProjects = allProjects.map((p) => p.working_directory);
43
- for (const p of allProjects)
46
+ // Purge temp-dir ghosts (leaked by test runs) from the central DB
47
+ // so they stop resurfacing in every future sync.
48
+ for (const p of allProjects) {
49
+ if (isTempProjectPath(p.working_directory)) {
50
+ try {
51
+ centralDb.deleteProject(p.id);
52
+ }
53
+ catch {
54
+ // non-critical
55
+ }
56
+ continue;
57
+ }
58
+ dbProjects.push(p.working_directory);
44
59
  titleByDir.set(p.working_directory, p.name);
60
+ }
45
61
  centralDb.close();
46
62
  }
47
63
  }
48
64
  catch {
49
65
  // non-critical
50
66
  }
51
- // Merge: deduplicate by resolved path
67
+ // Merge: deduplicate by resolved path. Temp-dir entries (/tmp,
68
+ // /var/folders — leaked by old versions' test runs) are dropped here,
69
+ // BEFORE the count and the registry write-back. Previously they were
70
+ // only skipped inside the upgrade loop, so "syncing N registered
71
+ // projects" reported hundreds of ghosts and the write-back on line
72
+ // below re-persisted them forever.
52
73
  const seen = new Set();
53
74
  const projects = [];
75
+ let droppedTemp = 0;
54
76
  for (const p of [...fileProjects, ...dbProjects]) {
55
77
  const resolved = path.resolve(p);
56
- if (!seen.has(resolved)) {
57
- seen.add(resolved);
58
- projects.push(resolved);
78
+ if (seen.has(resolved))
79
+ continue;
80
+ seen.add(resolved);
81
+ if (isTempProjectPath(resolved)) {
82
+ droppedTemp++;
83
+ continue;
59
84
  }
85
+ projects.push(resolved);
60
86
  }
61
87
  if (projects.length === 0) {
62
88
  console.log(" no registered projects found");
@@ -79,10 +105,7 @@ export async function runSetupSyncProjectsCommand(opts) {
79
105
  const skipped = [];
80
106
  const failed = [];
81
107
  for (const projectDir of projects) {
82
- // Skip test/temp directories
83
- if (projectDir.startsWith("/tmp/") || projectDir.startsWith("/private/tmp/") || projectDir.startsWith("/var/folders/") || projectDir.startsWith("/private/var/folders/")) {
84
- continue;
85
- }
108
+ // Temp/test directories were already filtered out of `projects`.
86
109
  const storePath = path.join(projectDir, ".gnosys");
87
110
  try {
88
111
  await fs.stat(storePath);
@@ -131,7 +154,8 @@ export async function runSetupSyncProjectsCommand(opts) {
131
154
  }
132
155
  // Stop the lead-in spinner now that the iteration is done. Resolved
133
156
  // before any per-section output so the cursor is on a fresh line.
134
- syncSpinner.ok(`synced ${projects.length} registered projects`, `${upgraded.length} upgraded · ${skipped.length} skipped · ${failed.length} failed`);
157
+ syncSpinner.ok(`synced ${projects.length} registered projects`, `${upgraded.length} upgraded · ${skipped.length} skipped · ${failed.length} failed` +
158
+ (droppedTemp > 0 ? ` · ${droppedTemp} temp entries pruned` : ""));
135
159
  // 3. Update global agent rules
136
160
  try {
137
161
  let centralDb = null;
@@ -13,6 +13,17 @@ export interface GnosysWebIndex {
13
13
  documentCount: number;
14
14
  documents: DocumentManifest[];
15
15
  invertedIndex: Record<string, IndexEntry[]>;
16
+ expansions?: Record<string, string[]>;
17
+ }
18
+ export interface GnosysWebVectors {
19
+ version: number;
20
+ model: string;
21
+ dims: number;
22
+ quantization: "int8";
23
+ generated: string;
24
+ scale: number;
25
+ offset: number;
26
+ vectors: Record<string, number[]>;
16
27
  }
17
28
  export interface DocumentManifest {
18
29
  id: string;
@@ -36,21 +47,40 @@ export interface SearchOptions {
36
47
  category?: string;
37
48
  tags?: string[];
38
49
  boostRecent?: boolean;
50
+ queryVector?: number[];
51
+ vectors?: GnosysWebVectors;
52
+ expectedModel?: string;
53
+ expandQuery?: boolean;
39
54
  }
40
55
  export interface SearchResult {
41
56
  document: DocumentManifest;
42
57
  score: number;
43
58
  matchedTokens: string[];
59
+ semanticScore?: number;
44
60
  }
45
61
  /**
46
62
  * Load a pre-computed Gnosys web index from a file path or raw JSON string.
47
63
  * Caches the result for repeated calls with the same source.
48
64
  */
49
65
  export declare function loadIndex(pathOrJson: string): GnosysWebIndex;
66
+ /**
67
+ * Load pre-computed Gnosys web vectors from a file path or raw JSON string.
68
+ * Caches the result for repeated calls with the same source.
69
+ */
70
+ export declare function loadVectors(pathOrJson: string): GnosysWebVectors;
50
71
  /**
51
72
  * Clear the cached index (useful for testing).
52
73
  */
53
74
  export declare function clearIndexCache(): void;
75
+ /**
76
+ * Clear the cached vectors file (useful for testing).
77
+ */
78
+ export declare function clearVectorsCache(): void;
79
+ /**
80
+ * Cosine similarity for equal-dimension vectors. Returns 0 for zero-magnitude
81
+ * vectors or dimension mismatches.
82
+ */
83
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
54
84
  /**
55
85
  * Search the pre-computed index and return ranked results.
56
86
  */