@gzoo/cortex 0.5.11 → 0.5.13

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 (62) hide show
  1. package/dist/cortex.mjs +358 -159
  2. package/package.json +1 -1
  3. package/packages/cli/dist/commands/config.d.ts.map +1 -1
  4. package/packages/cli/dist/commands/config.js +9 -1
  5. package/packages/cli/dist/commands/config.js.map +1 -1
  6. package/packages/cli/dist/commands/ingest.js +3 -1
  7. package/packages/cli/dist/commands/ingest.js.map +1 -1
  8. package/packages/cli/dist/commands/mcp.js +1 -1
  9. package/packages/cli/dist/commands/mcp.js.map +1 -1
  10. package/packages/cli/dist/commands/models.js +2 -1
  11. package/packages/cli/dist/commands/models.js.map +1 -1
  12. package/packages/cli/dist/commands/serve.js +1 -1
  13. package/packages/cli/dist/commands/serve.js.map +1 -1
  14. package/packages/cli/dist/commands/status.d.ts.map +1 -1
  15. package/packages/cli/dist/commands/status.js +15 -4
  16. package/packages/cli/dist/commands/status.js.map +1 -1
  17. package/packages/cli/dist/commands/watch.js +1 -0
  18. package/packages/cli/dist/commands/watch.js.map +1 -1
  19. package/packages/cli/dist/index.js +1 -1
  20. package/packages/cli/dist/index.js.map +1 -1
  21. package/packages/graph/dist/migrations/002-add-indexes.d.ts +4 -0
  22. package/packages/graph/dist/migrations/002-add-indexes.d.ts.map +1 -0
  23. package/packages/graph/dist/migrations/002-add-indexes.js +30 -0
  24. package/packages/graph/dist/migrations/002-add-indexes.js.map +1 -0
  25. package/packages/graph/dist/query-engine.d.ts.map +1 -1
  26. package/packages/graph/dist/query-engine.js +16 -25
  27. package/packages/graph/dist/query-engine.js.map +1 -1
  28. package/packages/graph/dist/sqlite-store.d.ts +5 -1
  29. package/packages/graph/dist/sqlite-store.d.ts.map +1 -1
  30. package/packages/graph/dist/sqlite-store.js +152 -118
  31. package/packages/graph/dist/sqlite-store.js.map +1 -1
  32. package/packages/ingest/dist/parsers/conversation.js +2 -2
  33. package/packages/ingest/dist/parsers/conversation.js.map +1 -1
  34. package/packages/ingest/dist/pipeline.d.ts +7 -0
  35. package/packages/ingest/dist/pipeline.d.ts.map +1 -1
  36. package/packages/ingest/dist/pipeline.js +43 -5
  37. package/packages/ingest/dist/pipeline.js.map +1 -1
  38. package/packages/ingest/dist/watcher.d.ts +1 -0
  39. package/packages/ingest/dist/watcher.d.ts.map +1 -1
  40. package/packages/ingest/dist/watcher.js +22 -11
  41. package/packages/ingest/dist/watcher.js.map +1 -1
  42. package/packages/llm/dist/cache.js +5 -5
  43. package/packages/llm/dist/cache.js.map +1 -1
  44. package/packages/llm/dist/providers/ollama.d.ts +1 -0
  45. package/packages/llm/dist/providers/ollama.d.ts.map +1 -1
  46. package/packages/llm/dist/providers/ollama.js +68 -17
  47. package/packages/llm/dist/providers/ollama.js.map +1 -1
  48. package/packages/llm/dist/providers/openai-compatible.d.ts.map +1 -1
  49. package/packages/llm/dist/providers/openai-compatible.js +7 -1
  50. package/packages/llm/dist/providers/openai-compatible.js.map +1 -1
  51. package/packages/llm/dist/token-tracker.d.ts +1 -0
  52. package/packages/llm/dist/token-tracker.d.ts.map +1 -1
  53. package/packages/llm/dist/token-tracker.js +19 -0
  54. package/packages/llm/dist/token-tracker.js.map +1 -1
  55. package/packages/server/dist/index.d.ts.map +1 -1
  56. package/packages/server/dist/index.js +15 -2
  57. package/packages/server/dist/index.js.map +1 -1
  58. package/packages/server/dist/middleware/auth.d.ts.map +1 -1
  59. package/packages/server/dist/middleware/auth.js +4 -0
  60. package/packages/server/dist/middleware/auth.js.map +1 -1
  61. package/packages/server/dist/routes/status.js +1 -1
  62. package/packages/server/dist/routes/status.js.map +1 -1
package/dist/cortex.mjs CHANGED
@@ -688,6 +688,43 @@ var init_initial = __esm({
688
688
  }
689
689
  });
690
690
 
691
+ // packages/graph/dist/migrations/002-add-indexes.js
692
+ function up2(db) {
693
+ const currentVersion = db.prepare("SELECT MAX(version) as v FROM schema_version").get()?.v ?? 0;
694
+ if (currentVersion >= MIGRATION_VERSION2)
695
+ return;
696
+ db.exec(`
697
+ -- Composite index for common entity queries (project + status + soft-delete filter)
698
+ CREATE INDEX IF NOT EXISTS idx_entities_project_status_deleted
699
+ ON entities(project_id, status, deleted_at);
700
+
701
+ -- Contradiction lookups by status and severity
702
+ CREATE INDEX IF NOT EXISTS idx_contradictions_status_severity
703
+ ON contradictions(status, severity);
704
+
705
+ -- Contradiction lookups by entity
706
+ CREATE INDEX IF NOT EXISTS idx_contradictions_entity_a
707
+ ON contradictions(entity_id_a);
708
+
709
+ CREATE INDEX IF NOT EXISTS idx_contradictions_entity_b
710
+ ON contradictions(entity_id_b);
711
+
712
+ -- Files by project + status (used during watch/ingest)
713
+ CREATE INDEX IF NOT EXISTS idx_files_project_status
714
+ ON files(project_id, status);
715
+
716
+ INSERT OR IGNORE INTO schema_version (version, applied_at)
717
+ VALUES (${MIGRATION_VERSION2}, datetime('now'));
718
+ `);
719
+ }
720
+ var MIGRATION_VERSION2;
721
+ var init_add_indexes = __esm({
722
+ "packages/graph/dist/migrations/002-add-indexes.js"() {
723
+ "use strict";
724
+ MIGRATION_VERSION2 = 2;
725
+ }
726
+ });
727
+
691
728
  // packages/graph/dist/sqlite-store.js
692
729
  import Database from "better-sqlite3";
693
730
  import { randomUUID } from "node:crypto";
@@ -780,17 +817,22 @@ var init_sqlite_store = __esm({
780
817
  "use strict";
781
818
  init_dist();
782
819
  init_initial();
820
+ init_add_indexes();
783
821
  SQLiteStore = class {
784
822
  db;
785
823
  dbPath;
786
824
  constructor(options = {}) {
787
825
  const { dbPath = "~/.cortex/cortex.db", walMode = true, backupOnStartup = true } = options;
788
826
  this.dbPath = resolveHomePath(dbPath);
789
- mkdirSync3(dirname(this.dbPath), { recursive: true });
827
+ mkdirSync3(dirname(this.dbPath), { recursive: true, mode: 448 });
790
828
  if (backupOnStartup) {
791
829
  this.backupSync();
792
830
  }
793
831
  this.db = new Database(this.dbPath);
832
+ try {
833
+ chmodSync(this.dbPath, 384);
834
+ } catch {
835
+ }
794
836
  if (walMode) {
795
837
  this.db.pragma("journal_mode = WAL");
796
838
  }
@@ -801,6 +843,7 @@ var init_sqlite_store = __esm({
801
843
  migrate() {
802
844
  try {
803
845
  up(this.db);
846
+ up2(this.db);
804
847
  } catch (err) {
805
848
  throw new CortexError(GRAPH_DB_ERROR, "critical", "graph", `Migration failed: ${err instanceof Error ? err.message : String(err)}`, void 0, "Delete the database and restart.");
806
849
  }
@@ -822,8 +865,17 @@ var init_sqlite_store = __esm({
822
865
  close() {
823
866
  this.db.close();
824
867
  }
868
+ transaction(fn) {
869
+ return this.db.transaction(fn)();
870
+ }
825
871
  // --- Entities ---
872
+ createEntitySync(entity) {
873
+ return this._createEntity(entity);
874
+ }
826
875
  async createEntity(entity) {
876
+ return this._createEntity(entity);
877
+ }
878
+ _createEntity(entity) {
827
879
  const id = randomUUID();
828
880
  const ts = now();
829
881
  this.db.prepare(`
@@ -903,13 +955,17 @@ var init_sqlite_store = __esm({
903
955
  }
904
956
  let sql;
905
957
  if (query.search) {
958
+ const sanitizedSearch = query.search.replace(/[^a-zA-Z0-9\s]/g, " ").trim();
959
+ if (!sanitizedSearch) {
960
+ return [];
961
+ }
906
962
  sql = `
907
963
  SELECT e.* FROM entities e
908
964
  JOIN entities_fts fts ON fts.rowid = e.rowid
909
965
  WHERE fts.entities_fts MATCH ? AND ${conditions.join(" AND ")}
910
966
  ORDER BY rank
911
967
  `;
912
- params.unshift(query.search);
968
+ params.unshift(sanitizedSearch);
913
969
  } else {
914
970
  sql = `
915
971
  SELECT * FROM entities
@@ -945,22 +1001,35 @@ var init_sqlite_store = __esm({
945
1001
  const row = this.db.prepare("SELECT * FROM relationships WHERE id = ?").get(id);
946
1002
  return row ? rowToRelationship(row) : null;
947
1003
  }
948
- async getRelationshipsForEntity(entityId, direction = "both") {
1004
+ async getRelationshipsForEntity(entityId, direction = "both", limit = 200) {
949
1005
  let sql;
950
1006
  let params;
951
1007
  if (direction === "out") {
952
- sql = "SELECT * FROM relationships WHERE source_entity_id = ?";
953
- params = [entityId];
1008
+ sql = "SELECT * FROM relationships WHERE source_entity_id = ? LIMIT ?";
1009
+ params = [entityId, limit];
954
1010
  } else if (direction === "in") {
955
- sql = "SELECT * FROM relationships WHERE target_entity_id = ?";
956
- params = [entityId];
1011
+ sql = "SELECT * FROM relationships WHERE target_entity_id = ? LIMIT ?";
1012
+ params = [entityId, limit];
957
1013
  } else {
958
- sql = "SELECT * FROM relationships WHERE source_entity_id = ? OR target_entity_id = ?";
959
- params = [entityId, entityId];
1014
+ sql = "SELECT * FROM relationships WHERE source_entity_id = ? OR target_entity_id = ? LIMIT ?";
1015
+ params = [entityId, entityId, limit];
960
1016
  }
961
1017
  const rows = this.db.prepare(sql).all(...params);
962
1018
  return rows.map(rowToRelationship);
963
1019
  }
1020
+ async getRelationshipsForEntities(entityIds) {
1021
+ if (entityIds.length === 0)
1022
+ return [];
1023
+ const placeholders = entityIds.map(() => "?").join(",");
1024
+ const sql = `
1025
+ SELECT * FROM relationships
1026
+ WHERE source_entity_id IN (${placeholders})
1027
+ OR target_entity_id IN (${placeholders})
1028
+ LIMIT 2000
1029
+ `;
1030
+ const rows = this.db.prepare(sql).all(...entityIds, ...entityIds);
1031
+ return rows.map(rowToRelationship);
1032
+ }
964
1033
  async deleteRelationship(id) {
965
1034
  this.db.prepare("DELETE FROM relationships WHERE id = ?").run(id);
966
1035
  }
@@ -1388,13 +1457,100 @@ var init_vector_store = __esm({
1388
1457
  function estimateTokens(text) {
1389
1458
  return Math.ceil(text.length / AVG_CHARS_PER_TOKEN);
1390
1459
  }
1391
- var logger2, AVG_CHARS_PER_TOKEN, QueryEngine;
1460
+ var logger2, AVG_CHARS_PER_TOKEN, FTS_STOP_WORDS, QueryEngine;
1392
1461
  var init_query_engine = __esm({
1393
1462
  "packages/graph/dist/query-engine.js"() {
1394
1463
  "use strict";
1395
1464
  init_dist();
1396
1465
  logger2 = createLogger("graph:query-engine");
1397
1466
  AVG_CHARS_PER_TOKEN = 4;
1467
+ FTS_STOP_WORDS = /* @__PURE__ */ new Set([
1468
+ "a",
1469
+ "an",
1470
+ "the",
1471
+ "and",
1472
+ "or",
1473
+ "but",
1474
+ "in",
1475
+ "on",
1476
+ "at",
1477
+ "to",
1478
+ "for",
1479
+ "of",
1480
+ "with",
1481
+ "by",
1482
+ "from",
1483
+ "is",
1484
+ "are",
1485
+ "was",
1486
+ "were",
1487
+ "be",
1488
+ "been",
1489
+ "being",
1490
+ "have",
1491
+ "has",
1492
+ "had",
1493
+ "do",
1494
+ "does",
1495
+ "did",
1496
+ "will",
1497
+ "would",
1498
+ "could",
1499
+ "should",
1500
+ "may",
1501
+ "might",
1502
+ "shall",
1503
+ "can",
1504
+ "need",
1505
+ "must",
1506
+ "what",
1507
+ "which",
1508
+ "who",
1509
+ "how",
1510
+ "why",
1511
+ "when",
1512
+ "where",
1513
+ "that",
1514
+ "this",
1515
+ "these",
1516
+ "those",
1517
+ "it",
1518
+ "its",
1519
+ "me",
1520
+ "my",
1521
+ "you",
1522
+ "your",
1523
+ "we",
1524
+ "our",
1525
+ "they",
1526
+ "their",
1527
+ "he",
1528
+ "she",
1529
+ "i",
1530
+ "all",
1531
+ "any",
1532
+ "each",
1533
+ "some",
1534
+ "no",
1535
+ "not",
1536
+ "so",
1537
+ "yet",
1538
+ "use",
1539
+ "used",
1540
+ "using",
1541
+ "about",
1542
+ "tell",
1543
+ "know",
1544
+ "get",
1545
+ "got",
1546
+ "make",
1547
+ "made",
1548
+ "see",
1549
+ "give",
1550
+ "go",
1551
+ "come",
1552
+ "take"
1553
+ ]);
1398
1554
  QueryEngine = class {
1399
1555
  sqliteStore;
1400
1556
  vectorStore;
@@ -1430,16 +1586,8 @@ var init_query_engine = __esm({
1430
1586
  }
1431
1587
  const privacyFiltered = await this.filterByPrivacy(contextEntities);
1432
1588
  const entityIds = new Set(privacyFiltered.map((e) => e.id));
1433
- const relationships = [];
1434
- for (const entity of privacyFiltered) {
1435
- const rels = await this.sqliteStore.getRelationshipsForEntity(entity.id);
1436
- for (const rel of rels) {
1437
- if (entityIds.has(rel.sourceEntityId) && entityIds.has(rel.targetEntityId)) {
1438
- relationships.push(rel);
1439
- }
1440
- }
1441
- }
1442
- const uniqueRels = [...new Map(relationships.map((r) => [r.id, r])).values()];
1589
+ const allRels = await this.sqliteStore.getRelationshipsForEntities([...entityIds]);
1590
+ const uniqueRels = allRels.filter((r) => entityIds.has(r.sourceEntityId) && entityIds.has(r.targetEntityId));
1443
1591
  const relTokens = uniqueRels.reduce((sum, r) => sum + estimateTokens(r.description ?? "") + 20, 0);
1444
1592
  const filteredTokens = privacyFiltered.reduce((sum, e) => sum + estimateTokens(e.content) + estimateTokens(e.name), 0);
1445
1593
  logger2.debug("Context assembled", {
@@ -1493,94 +1641,7 @@ var init_query_engine = __esm({
1493
1641
  * entities matching ANY meaningful keyword are returned.
1494
1642
  */
1495
1643
  buildFtsQuery(query) {
1496
- const stopWords = /* @__PURE__ */ new Set([
1497
- "a",
1498
- "an",
1499
- "the",
1500
- "and",
1501
- "or",
1502
- "but",
1503
- "in",
1504
- "on",
1505
- "at",
1506
- "to",
1507
- "for",
1508
- "of",
1509
- "with",
1510
- "by",
1511
- "from",
1512
- "is",
1513
- "are",
1514
- "was",
1515
- "were",
1516
- "be",
1517
- "been",
1518
- "being",
1519
- "have",
1520
- "has",
1521
- "had",
1522
- "do",
1523
- "does",
1524
- "did",
1525
- "will",
1526
- "would",
1527
- "could",
1528
- "should",
1529
- "may",
1530
- "might",
1531
- "shall",
1532
- "can",
1533
- "need",
1534
- "must",
1535
- "what",
1536
- "which",
1537
- "who",
1538
- "how",
1539
- "why",
1540
- "when",
1541
- "where",
1542
- "that",
1543
- "this",
1544
- "these",
1545
- "those",
1546
- "it",
1547
- "its",
1548
- "me",
1549
- "my",
1550
- "you",
1551
- "your",
1552
- "we",
1553
- "our",
1554
- "they",
1555
- "their",
1556
- "he",
1557
- "she",
1558
- "i",
1559
- "all",
1560
- "any",
1561
- "each",
1562
- "some",
1563
- "no",
1564
- "not",
1565
- "so",
1566
- "yet",
1567
- "use",
1568
- "used",
1569
- "using",
1570
- "about",
1571
- "tell",
1572
- "know",
1573
- "get",
1574
- "got",
1575
- "make",
1576
- "made",
1577
- "see",
1578
- "give",
1579
- "go",
1580
- "come",
1581
- "take"
1582
- ]);
1583
- const keywords = query.replace(/[^a-zA-Z0-9\s]/g, " ").toLowerCase().split(/\s+/).filter((w) => w.length >= 3 && !stopWords.has(w));
1644
+ const keywords = query.replace(/[^a-zA-Z0-9\s]/g, " ").toLowerCase().split(/\s+/).filter((w) => w.length >= 3 && !FTS_STOP_WORDS.has(w));
1584
1645
  if (keywords.length === 0) {
1585
1646
  return query.replace(/[^a-zA-Z0-9\s]/g, " ").trim();
1586
1647
  }
@@ -1796,12 +1857,38 @@ var init_anthropic = __esm({
1796
1857
  });
1797
1858
 
1798
1859
  // packages/llm/dist/providers/ollama.js
1799
- var logger4, OllamaProvider;
1860
+ function validateOllamaHost(host) {
1861
+ let parsed;
1862
+ try {
1863
+ parsed = new URL(host);
1864
+ } catch {
1865
+ throw new CortexError(LLM_PROVIDER_UNAVAILABLE, "high", "llm", `Invalid Ollama host URL: ${host}`, { host }, "Set a valid URL like http://localhost:11434", false);
1866
+ }
1867
+ const hostname = parsed.hostname;
1868
+ for (const pattern of BLOCKED_HOST_PATTERNS) {
1869
+ if (pattern.test(hostname)) {
1870
+ throw new CortexError(LLM_PROVIDER_UNAVAILABLE, "high", "llm", `Ollama host "${hostname}" is blocked \u2014 it matches a link-local or cloud metadata IP range.`, { host }, "Use a non-link-local address for Ollama.", false);
1871
+ }
1872
+ }
1873
+ const localhostNames = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
1874
+ if (!localhostNames.has(hostname)) {
1875
+ logger4.warn(`Ollama host is not localhost (${hostname}). Ensure the remote Ollama instance is trusted and network-secured.`);
1876
+ }
1877
+ }
1878
+ var logger4, BLOCKED_HOST_PATTERNS, OllamaProvider;
1800
1879
  var init_ollama = __esm({
1801
1880
  "packages/llm/dist/providers/ollama.js"() {
1802
1881
  "use strict";
1803
1882
  init_dist();
1804
1883
  logger4 = createLogger("llm:ollama");
1884
+ BLOCKED_HOST_PATTERNS = [
1885
+ /^169\.254\./,
1886
+ // AWS/Azure metadata link-local
1887
+ /^fd[0-9a-f]{2}:/i,
1888
+ // IPv6 unique local (fd00::/8)
1889
+ /^fe80:/i
1890
+ // IPv6 link-local
1891
+ ];
1805
1892
  OllamaProvider = class {
1806
1893
  name = "ollama";
1807
1894
  type = "local";
@@ -1812,6 +1899,7 @@ var init_ollama = __esm({
1812
1899
  numGpu;
1813
1900
  timeoutMs;
1814
1901
  keepAlive;
1902
+ streamInactivityTimeoutMs;
1815
1903
  capabilities = {
1816
1904
  supportedTasks: [
1817
1905
  LLMTask.ENTITY_EXTRACTION,
@@ -1830,12 +1918,14 @@ var init_ollama = __esm({
1830
1918
  };
1831
1919
  constructor(options = {}) {
1832
1920
  this.host = options.host ?? process.env["CORTEX_OLLAMA_HOST"] ?? "http://localhost:11434";
1921
+ validateOllamaHost(this.host);
1833
1922
  this.model = options.model ?? "mistral:7b-instruct-q5_K_M";
1834
1923
  this.embeddingModel = options.embeddingModel ?? "nomic-embed-text";
1835
1924
  this.numCtx = options.numCtx ?? 8192;
1836
1925
  this.numGpu = options.numGpu ?? -1;
1837
1926
  this.timeoutMs = options.timeoutMs ?? 3e5;
1838
1927
  this.keepAlive = options.keepAlive ?? "5m";
1928
+ this.streamInactivityTimeoutMs = 6e4;
1839
1929
  this.capabilities.maxContextTokens = this.numCtx;
1840
1930
  }
1841
1931
  getModel() {
@@ -1942,25 +2032,43 @@ var init_ollama = __esm({
1942
2032
  const decoder = new TextDecoder();
1943
2033
  let inputTokens = 0;
1944
2034
  let outputTokens = 0;
1945
- while (true) {
1946
- const { done, value } = await reader.read();
1947
- if (done)
1948
- break;
1949
- const chunk = decoder.decode(value, { stream: true });
1950
- const lines = chunk.split("\n").filter((line) => line.trim());
1951
- for (const line of lines) {
1952
- try {
1953
- const data = JSON.parse(line);
1954
- if (data.response) {
1955
- yield data.response;
1956
- }
1957
- if (data.done) {
1958
- inputTokens = data.prompt_eval_count ?? 0;
1959
- outputTokens = data.eval_count ?? 0;
2035
+ const streamController = new AbortController();
2036
+ let inactivityTimer = setTimeout(() => streamController.abort(), this.streamInactivityTimeoutMs);
2037
+ try {
2038
+ while (true) {
2039
+ const readPromise = reader.read();
2040
+ const raceResult = await Promise.race([
2041
+ readPromise,
2042
+ new Promise((_, reject) => {
2043
+ streamController.signal.addEventListener("abort", () => reject(new Error("Stream inactivity timeout")), { once: true });
2044
+ if (streamController.signal.aborted) {
2045
+ reject(new Error("Stream inactivity timeout"));
2046
+ }
2047
+ })
2048
+ ]);
2049
+ const { done, value } = raceResult;
2050
+ if (done)
2051
+ break;
2052
+ clearTimeout(inactivityTimer);
2053
+ inactivityTimer = setTimeout(() => streamController.abort(), this.streamInactivityTimeoutMs);
2054
+ const chunk = decoder.decode(value, { stream: true });
2055
+ const lines = chunk.split("\n").filter((line) => line.trim());
2056
+ for (const line of lines) {
2057
+ try {
2058
+ const data = JSON.parse(line);
2059
+ if (data.response) {
2060
+ yield data.response;
2061
+ }
2062
+ if (data.done) {
2063
+ inputTokens = data.prompt_eval_count ?? 0;
2064
+ outputTokens = data.eval_count ?? 0;
2065
+ }
2066
+ } catch {
1960
2067
  }
1961
- } catch {
1962
2068
  }
1963
2069
  }
2070
+ } finally {
2071
+ clearTimeout(inactivityTimer);
1964
2072
  }
1965
2073
  return {
1966
2074
  inputTokens,
@@ -2125,7 +2233,12 @@ var init_openai_compatible = __esm({
2125
2233
  });
2126
2234
  this.primaryModel = options.primaryModel ?? "gpt-4o";
2127
2235
  this.fastModel = options.fastModel ?? "gpt-4o-mini";
2128
- this.isGemini = options.baseUrl.includes("generativelanguage.googleapis.com");
2236
+ try {
2237
+ const parsedUrl = new URL(options.baseUrl);
2238
+ this.isGemini = parsedUrl.hostname === "generativelanguage.googleapis.com";
2239
+ } catch {
2240
+ this.isGemini = false;
2241
+ }
2129
2242
  logger5.info("OpenAI-compatible provider initialized", {
2130
2243
  baseUrl: options.baseUrl,
2131
2244
  primaryModel: this.primaryModel,
@@ -2252,7 +2365,7 @@ function estimateCost(model, inputTokens, outputTokens) {
2252
2365
  const costs = MODEL_COSTS[model] ?? DEFAULT_COST;
2253
2366
  return inputTokens / 1e6 * costs.input + outputTokens / 1e6 * costs.output;
2254
2367
  }
2255
- var logger6, MODEL_COSTS, DEFAULT_COST, TokenTracker;
2368
+ var logger6, MODEL_COSTS, DEFAULT_COST, MAX_IN_MEMORY_RECORDS, TokenTracker;
2256
2369
  var init_token_tracker = __esm({
2257
2370
  "packages/llm/dist/token-tracker.js"() {
2258
2371
  "use strict";
@@ -2263,6 +2376,7 @@ var init_token_tracker = __esm({
2263
2376
  "claude-haiku-4-5-20251001": { input: 0.8, output: 4 }
2264
2377
  };
2265
2378
  DEFAULT_COST = { input: 3, output: 15 };
2379
+ MAX_IN_MEMORY_RECORDS = 1e4;
2266
2380
  TokenTracker = class {
2267
2381
  records = [];
2268
2382
  monthlyBudgetUsd;
@@ -2287,9 +2401,23 @@ var init_token_tracker = __esm({
2287
2401
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2288
2402
  };
2289
2403
  this.records.push(record);
2404
+ this.trimOldRecords();
2290
2405
  this.checkBudget();
2291
2406
  return record;
2292
2407
  }
2408
+ trimOldRecords() {
2409
+ if (this.records.length <= MAX_IN_MEMORY_RECORDS)
2410
+ return;
2411
+ const currentMonth = (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
2412
+ const currentMonthRecords = this.records.filter((r) => r.timestamp.startsWith(currentMonth));
2413
+ const priorRecords = this.records.filter((r) => !r.timestamp.startsWith(currentMonth));
2414
+ if (currentMonthRecords.length >= MAX_IN_MEMORY_RECORDS) {
2415
+ this.records = currentMonthRecords.slice(-MAX_IN_MEMORY_RECORDS);
2416
+ } else {
2417
+ const keepFromPrior = MAX_IN_MEMORY_RECORDS - currentMonthRecords.length;
2418
+ this.records = [...priorRecords.slice(-keepFromPrior), ...currentMonthRecords];
2419
+ }
2420
+ }
2293
2421
  checkBudget() {
2294
2422
  const spent = this.getCurrentMonthSpend();
2295
2423
  const usedPercent = this.monthlyBudgetUsd > 0 ? spent / this.monthlyBudgetUsd : 0;
@@ -2402,9 +2530,9 @@ var init_cache = __esm({
2402
2530
  if (!this.enabled)
2403
2531
  return;
2404
2532
  if (this.cache.size >= this.maxEntries) {
2405
- const oldest = [...this.cache.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt)[0];
2406
- if (oldest) {
2407
- this.cache.delete(oldest[0]);
2533
+ const firstKey = this.cache.keys().next().value;
2534
+ if (firstKey !== void 0) {
2535
+ this.cache.delete(firstKey);
2408
2536
  }
2409
2537
  }
2410
2538
  const key = this.buildKey(contentHash, promptId, promptVersion);
@@ -3906,7 +4034,7 @@ function isConversationMarkdown(content) {
3906
4034
  const lines = content.split("\n");
3907
4035
  const headings = [];
3908
4036
  for (const line of lines) {
3909
- const m = line.match(/^#{1,3}\s+(.+)$/);
4037
+ const m = line.match(/^#{1,3}\s+(\S.*)$/);
3910
4038
  if (m) {
3911
4039
  headings.push(m[1].trim());
3912
4040
  if (headings.length >= 2)
@@ -3988,7 +4116,7 @@ function parseConversationMarkdown(content) {
3988
4116
  };
3989
4117
  for (let i = 0; i < lines.length; i++) {
3990
4118
  const line = lines[i];
3991
- const headingMatch = line.match(/^#{1,3}\s+(.+)$/);
4119
+ const headingMatch = line.match(/^#{1,3}\s+(\S.*)$/);
3992
4120
  if (headingMatch) {
3993
4121
  flush(i);
3994
4122
  currentRole = headingMatch[1].trim();
@@ -4176,6 +4304,11 @@ import { extname } from "node:path";
4176
4304
  function escapeRegex(s) {
4177
4305
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4178
4306
  }
4307
+ function globToRegex(pattern) {
4308
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
4309
+ const regexStr = escaped.replace(/\*\*/g, "___GLOBSTAR___").replace(/\*/g, "[^/\\\\]*").replace(/___GLOBSTAR___/g, ".*");
4310
+ return new RegExp("^" + regexStr + "$");
4311
+ }
4179
4312
  var logger9, FileWatcher;
4180
4313
  var init_watcher = __esm({
4181
4314
  "packages/ingest/dist/watcher.js"() {
@@ -4187,8 +4320,15 @@ var init_watcher = __esm({
4187
4320
  options;
4188
4321
  handler = null;
4189
4322
  debounceTimers = /* @__PURE__ */ new Map();
4323
+ compiledExcludePatterns;
4190
4324
  constructor(options) {
4191
4325
  this.options = options;
4326
+ this.compiledExcludePatterns = options.exclude.map((pattern) => {
4327
+ if (pattern.includes("*")) {
4328
+ return { pattern: globToRegex(pattern), isGlob: true };
4329
+ }
4330
+ return pattern;
4331
+ });
4192
4332
  }
4193
4333
  static fromConfig(config8) {
4194
4334
  return new _FileWatcher({
@@ -4272,13 +4412,12 @@ var init_watcher = __esm({
4272
4412
  }
4273
4413
  isExcluded(filePath) {
4274
4414
  const parts = filePath.split(/[\\/]/);
4275
- for (const pattern of this.options.exclude) {
4276
- if (pattern.includes("*")) {
4277
- const re = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/\\\\]*") + "$");
4278
- if (parts.some((p) => re.test(p)))
4415
+ for (const compiled of this.compiledExcludePatterns) {
4416
+ if (typeof compiled === "string") {
4417
+ if (parts.some((p) => p === compiled))
4279
4418
  return true;
4280
4419
  } else {
4281
- if (parts.some((p) => p === pattern))
4420
+ if (parts.some((p) => compiled.pattern.test(p)))
4282
4421
  return true;
4283
4422
  }
4284
4423
  }
@@ -4288,8 +4427,7 @@ var init_watcher = __esm({
4288
4427
  const patterns = [];
4289
4428
  for (const pattern of this.options.exclude) {
4290
4429
  if (pattern.includes("*")) {
4291
- const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "___GLOBSTAR___").replace(/\*/g, "[^/]*").replace(/___GLOBSTAR___/g, ".*");
4292
- patterns.push(new RegExp(regexStr));
4430
+ patterns.push(globToRegex(pattern));
4293
4431
  } else if (pattern.includes(".")) {
4294
4432
  patterns.push(new RegExp(`(^|[\\\\/])${escapeRegex(pattern)}$`));
4295
4433
  } else {
@@ -4466,10 +4604,34 @@ var init_pipeline = __esm({
4466
4604
  // Shared across all ingestFile calls — prevents the same entity pair from being
4467
4605
  // evaluated twice when multiple files ingest in the same batch.
4468
4606
  checkedContradictionPairs = /* @__PURE__ */ new Set();
4607
+ // Pre-compiled secret patterns for scrubbing before cloud LLM calls
4608
+ compiledSecretPatterns;
4469
4609
  constructor(router, store, options) {
4470
4610
  this.router = router;
4471
4611
  this.store = store;
4472
4612
  this.options = options;
4613
+ this.compiledSecretPatterns = (options.secretPatterns ?? []).map((pattern) => {
4614
+ try {
4615
+ return new RegExp(pattern, "g");
4616
+ } catch {
4617
+ logger11.warn("Invalid secret pattern, skipping", { pattern });
4618
+ return null;
4619
+ }
4620
+ }).filter((r) => r !== null);
4621
+ }
4622
+ /**
4623
+ * Scrub secrets from content before sending to cloud LLMs.
4624
+ * Only applied for standard privacy (sensitive/restricted use local provider).
4625
+ */
4626
+ scrubSecrets(content) {
4627
+ if (this.compiledSecretPatterns.length === 0)
4628
+ return content;
4629
+ let scrubbed = content;
4630
+ for (const re of this.compiledSecretPatterns) {
4631
+ re.lastIndex = 0;
4632
+ scrubbed = scrubbed.replace(re, "[SECRET_REDACTED]");
4633
+ }
4634
+ return scrubbed;
4473
4635
  }
4474
4636
  async ingestFile(filePath) {
4475
4637
  try {
@@ -4545,9 +4707,12 @@ var init_pipeline = __esm({
4545
4707
  const deduped = this.deduplicateEntities(allEntities);
4546
4708
  logger11.debug("Extracted entities", { filePath, raw: allEntities.length, deduped: deduped.length });
4547
4709
  const storedEntities = [];
4548
- for (const entity of deduped) {
4549
- const stored = await this.store.createEntity(entity);
4550
- storedEntities.push(stored);
4710
+ this.store.transaction(() => {
4711
+ for (const entity of deduped) {
4712
+ storedEntities.push(this.store.createEntitySync(entity));
4713
+ }
4714
+ });
4715
+ for (const stored of storedEntities) {
4551
4716
  eventBus.emit({
4552
4717
  type: "entity.created",
4553
4718
  payload: { entity: stored },
@@ -4608,6 +4773,7 @@ var init_pipeline = __esm({
4608
4773
  async extractEntities(chunk, filePath, fileType) {
4609
4774
  const contentHash = createHash2("sha256").update(chunk.content).digest("hex");
4610
4775
  const privacyOverride = this.options.projectPrivacyLevel !== "standard" ? { forceProvider: "local" } : {};
4776
+ const safeContent = this.options.projectPrivacyLevel === "standard" ? this.scrubSecrets(chunk.content) : chunk.content;
4611
4777
  try {
4612
4778
  const result = await this.router.completeStructured({
4613
4779
  systemPrompt: entity_extraction_exports.systemPrompt,
@@ -4615,7 +4781,7 @@ var init_pipeline = __esm({
4615
4781
  filePath,
4616
4782
  projectName: this.options.projectName,
4617
4783
  fileType,
4618
- content: chunk.content
4784
+ content: safeContent
4619
4785
  }),
4620
4786
  promptId: entity_extraction_exports.PROMPT_ID,
4621
4787
  promptVersion: entity_extraction_exports.PROMPT_VERSION,
@@ -5153,7 +5319,7 @@ var init_status = __esm({
5153
5319
  for (let i = 0; i < 6; i++) {
5154
5320
  try {
5155
5321
  const pkg = JSON.parse(readFileSync9(resolve19(dir, "package.json"), "utf-8"));
5156
- if (pkg.name === "gzoo-cortex" && pkg.version) {
5322
+ if ((pkg.name === "@gzoo/cortex" || pkg.name === "gzoo-cortex") && pkg.version) {
5157
5323
  _version = pkg.version;
5158
5324
  break;
5159
5325
  }
@@ -5385,7 +5551,7 @@ async function startServer(options) {
5385
5551
  callback(new Error("CORS not allowed"));
5386
5552
  }
5387
5553
  }));
5388
- app.use(express.json());
5554
+ app.use(express.json({ limit: "1mb" }));
5389
5555
  const isLocal = host === "127.0.0.1" || host === "localhost" || host === "::1";
5390
5556
  if (!isLocal && !config8.server.auth.enabled) {
5391
5557
  logger34.warn("Server bound to non-localhost without auth enabled. Set server.auth.enabled=true and server.auth.token in config, or set CORTEX_SERVER_AUTH_TOKEN env var.");
@@ -5393,6 +5559,15 @@ async function startServer(options) {
5393
5559
  const rateLimitWindow = 6e4;
5394
5560
  const rateLimitMax = 30;
5395
5561
  const rateLimitMap = /* @__PURE__ */ new Map();
5562
+ const rateLimitCleanupInterval = setInterval(() => {
5563
+ const now2 = Date.now();
5564
+ for (const [key, entry] of rateLimitMap) {
5565
+ if (now2 >= entry.resetAt) {
5566
+ rateLimitMap.delete(key);
5567
+ }
5568
+ }
5569
+ }, 6e4);
5570
+ rateLimitCleanupInterval.unref();
5396
5571
  const rateLimiter = (req, res, next) => {
5397
5572
  const key = req.ip ?? "unknown";
5398
5573
  const now2 = Date.now();
@@ -5412,11 +5587,12 @@ async function startServer(options) {
5412
5587
  next();
5413
5588
  };
5414
5589
  const api = express.Router();
5590
+ api.use(rateLimiter);
5415
5591
  api.use(createAuthMiddleware({ config: config8, host }));
5416
5592
  api.use("/entities", createEntityRoutes(bundle));
5417
5593
  api.use("/relationships", createRelationshipRoutes(bundle));
5418
5594
  api.use("/projects", createProjectRoutes(bundle));
5419
- api.use("/query", rateLimiter, createQueryRoutes(bundle));
5595
+ api.use("/query", createQueryRoutes(bundle));
5420
5596
  api.use("/contradictions", createContradictionRoutes(bundle));
5421
5597
  api.use("/", createStatusRoutes(bundle));
5422
5598
  app.use("/api/v1", api);
@@ -5443,7 +5619,8 @@ async function startServer(options) {
5443
5619
  maxFileSize: config8.ingest.maxFileSize,
5444
5620
  batchSize: config8.ingest.batchSize,
5445
5621
  projectPrivacyLevel: project.privacyLevel,
5446
- mergeConfidenceThreshold: 0.85
5622
+ mergeConfidenceThreshold: 0.85,
5623
+ secretPatterns: config8.privacy.secretPatterns
5447
5624
  });
5448
5625
  const watcher = new FileWatcher2({
5449
5626
  dirs: [project.rootPath],
@@ -5488,6 +5665,7 @@ async function startServer(options) {
5488
5665
  });
5489
5666
  const shutdown = () => {
5490
5667
  logger34.info("Shutting down...");
5668
+ clearInterval(rateLimitCleanupInterval);
5491
5669
  relay.close();
5492
5670
  server.close();
5493
5671
  bundle.store.close();
@@ -5852,7 +6030,8 @@ async function runWatch(projectName, opts, globals) {
5852
6030
  maxFileSize: config8.ingest.maxFileSize,
5853
6031
  batchSize: config8.ingest.batchSize,
5854
6032
  projectPrivacyLevel: project.privacyLevel,
5855
- mergeConfidenceThreshold: config8.graph.mergeConfidenceThreshold
6033
+ mergeConfidenceThreshold: config8.graph.mergeConfidenceThreshold,
6034
+ secretPatterns: config8.privacy.secretPatterns
5856
6035
  });
5857
6036
  const watcher = FileWatcher.fromConfig(config8.ingest);
5858
6037
  let ingestedCount = 0;
@@ -6247,6 +6426,16 @@ import { resolve as resolve7 } from "node:path";
6247
6426
  import { statSync as statSync3 } from "node:fs";
6248
6427
  import chalk5 from "chalk";
6249
6428
  var logger15 = createLogger("cli:status");
6429
+ function sanitizeUrl(url) {
6430
+ try {
6431
+ const parsed = new URL(url);
6432
+ parsed.username = "";
6433
+ parsed.password = "";
6434
+ return parsed.toString().replace(/\/$/, "");
6435
+ } catch {
6436
+ return "[invalid-url]";
6437
+ }
6438
+ }
6250
6439
  async function checkOllamaAvailable(host) {
6251
6440
  try {
6252
6441
  const controller = new AbortController();
@@ -6322,7 +6511,7 @@ async function runStatus(globals) {
6322
6511
  local: {
6323
6512
  provider: "ollama",
6324
6513
  available: ollamaAvailable,
6325
- host: config8.llm.local.host,
6514
+ host: sanitizeUrl(config8.llm.local.host),
6326
6515
  model: config8.llm.local.model,
6327
6516
  numCtx: localProvider?.getNumCtx() ?? config8.llm.local.numCtx
6328
6517
  }
@@ -6351,7 +6540,7 @@ async function runStatus(globals) {
6351
6540
  const numGpu = localProvider?.getNumGpu() ?? config8.llm.local.numGpu;
6352
6541
  console.log(chalk5.white("LLM Mode: ") + mode);
6353
6542
  const cloudLabel = `${config8.llm.cloud.models.primary} / ${config8.llm.cloud.models.fast} (${config8.llm.cloud.provider})`;
6354
- const localLabel = `${config8.llm.local.model} @ ${config8.llm.local.host}`;
6543
+ const localLabel = `${config8.llm.local.model} @ ${sanitizeUrl(config8.llm.local.host)}`;
6355
6544
  const localDetail = `${numCtx.toLocaleString()} ctx | GPU: ${numGpu === -1 ? "auto" : numGpu} layers | ~30 tok/s est.`;
6356
6545
  if (mode === "cloud-first") {
6357
6546
  const llmStatus = hasApiKey ? chalk5.green("\u2713") : chalk5.red("\u2717");
@@ -6390,7 +6579,7 @@ async function runStatus(globals) {
6390
6579
  let statusMsg = "";
6391
6580
  if (mode === "local-only") {
6392
6581
  statusOk = ollamaAvailable;
6393
- statusMsg = ollamaAvailable ? "\u2713 Fully operational" : `\u26A0 Ollama not available at ${config8.llm.local.host}. Run \`ollama serve\`.`;
6582
+ statusMsg = ollamaAvailable ? "\u2713 Fully operational" : `\u26A0 Ollama not available at ${sanitizeUrl(config8.llm.local.host)}. Run \`ollama serve\`.`;
6394
6583
  } else if (mode === "local-first") {
6395
6584
  statusOk = ollamaAvailable || hasApiKey;
6396
6585
  if (ollamaAvailable) {
@@ -6413,7 +6602,7 @@ async function runStatus(globals) {
6413
6602
  }
6414
6603
  } else {
6415
6604
  statusOk = hasApiKey;
6416
- statusMsg = hasApiKey ? "\u2713 Fully operational" : "\u26A0 API key not set. Run `cortex init` or set " + (apiKeyEnvVar ?? "your cloud API key env var") + ".";
6605
+ statusMsg = hasApiKey ? "\u2713 Fully operational" : "\u26A0 API key not set. Run `cortex init` to configure.";
6417
6606
  }
6418
6607
  console.log(chalk5.white("Status: ") + (statusOk ? chalk5.green(statusMsg) : chalk5.yellow(statusMsg)));
6419
6608
  console.log("");
@@ -6613,10 +6802,13 @@ function registerConfigCommand(program2) {
6613
6802
  await runExcludeRemove(pattern, globals);
6614
6803
  });
6615
6804
  }
6805
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
6616
6806
  function getNestedValue(obj, path) {
6617
6807
  const parts = path.split(".");
6618
6808
  let current = obj;
6619
6809
  for (const part of parts) {
6810
+ if (DANGEROUS_KEYS.has(part))
6811
+ return void 0;
6620
6812
  if (current === null || current === void 0 || typeof current !== "object") {
6621
6813
  return void 0;
6622
6814
  }
@@ -6629,12 +6821,17 @@ function setNestedValue(obj, path, value) {
6629
6821
  let current = obj;
6630
6822
  for (let i = 0; i < parts.length - 1; i++) {
6631
6823
  const part = parts[i];
6824
+ if (DANGEROUS_KEYS.has(part))
6825
+ throw new Error(`Invalid config key: ${part}`);
6632
6826
  if (current[part] === void 0 || typeof current[part] !== "object") {
6633
6827
  current[part] = {};
6634
6828
  }
6635
6829
  current = current[part];
6636
6830
  }
6637
- current[parts[parts.length - 1]] = value;
6831
+ const lastKey = parts[parts.length - 1];
6832
+ if (DANGEROUS_KEYS.has(lastKey))
6833
+ throw new Error(`Invalid config key: ${lastKey}`);
6834
+ current[lastKey] = value;
6638
6835
  }
6639
6836
  function parseValue(value) {
6640
6837
  try {
@@ -7355,7 +7552,8 @@ async function runIngest(pattern, opts, globals) {
7355
7552
  const lastSep = Math.max(resolvedPattern.lastIndexOf("/"), resolvedPattern.lastIndexOf("\\"));
7356
7553
  const dir = lastSep >= 0 ? resolvedPattern.slice(0, lastSep) : process.cwd();
7357
7554
  const filePattern = lastSep >= 0 ? resolvedPattern.slice(lastSep + 1) : resolvedPattern;
7358
- const regex = new RegExp("^" + filePattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$");
7555
+ const escaped = filePattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
7556
+ const regex = new RegExp("^" + escaped.replace(/\*/g, ".*") + "$");
7359
7557
  if (existsSync6(dir)) {
7360
7558
  for (const entry of readdirSync(dir)) {
7361
7559
  if (regex.test(entry)) {
@@ -7436,7 +7634,8 @@ Dry run complete: ~${totalSections * 3} entities estimated across ${filePaths.le
7436
7634
  maxFileSize: config8.ingest.maxFileSize,
7437
7635
  batchSize: config8.ingest.batchSize,
7438
7636
  projectPrivacyLevel: project.privacyLevel,
7439
- mergeConfidenceThreshold: config8.graph.mergeConfidenceThreshold
7637
+ mergeConfidenceThreshold: config8.graph.mergeConfidenceThreshold,
7638
+ secretPatterns: config8.privacy.secretPatterns
7440
7639
  });
7441
7640
  let totalEntities = 0;
7442
7641
  let totalRelationships = 0;
@@ -7576,7 +7775,7 @@ async function runModelsPull(model, globals) {
7576
7775
  Pulling model: ${chalk13.cyan(model)}`));
7577
7776
  console.log(chalk13.dim("This may take several minutes for large models...\n"));
7578
7777
  }
7579
- const result = spawnSync("ollama", ["pull", model], { stdio: "inherit", shell: true });
7778
+ const result = spawnSync("ollama", ["pull", model], { stdio: "inherit" });
7580
7779
  if (result.status !== 0) {
7581
7780
  console.error(chalk13.red(`
7582
7781
  \u2717 Failed to pull model "${model}"`));
@@ -7722,7 +7921,7 @@ function findPackageRoot(startDir) {
7722
7921
  try {
7723
7922
  const pkgPath = resolve16(dir, "package.json");
7724
7923
  const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
7725
- if (pkg.name === "gzoo-cortex")
7924
+ if (pkg.name === "@gzoo/cortex" || pkg.name === "gzoo-cortex")
7726
7925
  return dir;
7727
7926
  } catch {
7728
7927
  }
@@ -8018,7 +8217,7 @@ function findPkgRoot(startDir) {
8018
8217
  try {
8019
8218
  const pkgPath = resolve21(dir, "package.json");
8020
8219
  const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
8021
- if (pkg.name === "gzoo-cortex")
8220
+ if (pkg.name === "@gzoo/cortex" || pkg.name === "gzoo-cortex")
8022
8221
  return dir;
8023
8222
  } catch {
8024
8223
  }
@@ -8180,7 +8379,7 @@ function getVersion() {
8180
8379
  for (let i = 0; i < 6; i++) {
8181
8380
  try {
8182
8381
  const pkg = JSON.parse(readFileSync12(resolve23(dir, "package.json"), "utf-8"));
8183
- if (pkg.name === "gzoo-cortex" && pkg.version)
8382
+ if ((pkg.name === "@gzoo/cortex" || pkg.name === "gzoo-cortex") && pkg.version)
8184
8383
  return pkg.version;
8185
8384
  } catch {
8186
8385
  }