yamchart 0.10.1 → 0.10.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 (82) hide show
  1. package/dist/{advisor-5TJDNAY7.js → advisor-5F73GF54.js} +12 -12
  2. package/dist/{chunk-S2CH4HUZ.js → chunk-25ES7MGF.js} +4 -4
  3. package/dist/{chunk-QUIDZO5G.js → chunk-5CRXV5PQ.js} +22 -6
  4. package/dist/chunk-5CRXV5PQ.js.map +1 -0
  5. package/dist/chunk-7HWFVUGP.js +1239 -0
  6. package/dist/chunk-7HWFVUGP.js.map +1 -0
  7. package/dist/{chunk-5FHV22X2.js → chunk-PYVROL7Z.js} +4 -4
  8. package/dist/{chunk-64CI3HSY.js → chunk-UOSOZT47.js} +15 -8
  9. package/dist/{chunk-64CI3HSY.js.map → chunk-UOSOZT47.js.map} +1 -1
  10. package/dist/{chunk-ZBCQNWVN.js → chunk-WVSDQNAN.js} +5 -5
  11. package/dist/{chunk-UFDQ3C7Q.js → chunk-X3UG6T3Q.js} +257 -257
  12. package/dist/chunk-X3UG6T3Q.js.map +1 -0
  13. package/dist/{connection-utils-FEUWER5I.js → connection-utils-HNX3473B.js} +5 -5
  14. package/dist/{describe-MEP72B56.js → describe-KDGXFPDQ.js} +7 -7
  15. package/dist/{dev-H6SJZ5UA.js → dev-KOWFGDOP.js} +933 -127
  16. package/dist/dev-KOWFGDOP.js.map +1 -0
  17. package/dist/dev-supervisor-JXLVGEHP.js +43 -0
  18. package/dist/dev-supervisor-JXLVGEHP.js.map +1 -0
  19. package/dist/{dist-5DQO6L2S.js → dist-LK3ZG74Q.js} +5 -5
  20. package/dist/{dist-PPAD6KOM.js → dist-WMP7BFKP.js} +24 -2
  21. package/dist/{dist-XNCED7JW.js → dist-YB5RBWCC.js} +4 -4
  22. package/dist/index.js +65 -42
  23. package/dist/index.js.map +1 -1
  24. package/dist/{init-SECXKD5G.js → init-TJJVKMEN.js} +24 -5
  25. package/dist/init-TJJVKMEN.js.map +1 -0
  26. package/dist/public/assets/DataView-BfwnWnLH.js +9 -0
  27. package/dist/public/assets/{EventManagement-BnmeJDQl.js → EventManagement-BIkVUSCe.js} +2 -2
  28. package/dist/public/assets/ExplorePage-CeaAulqT.js +1 -0
  29. package/dist/public/assets/{LoginPage-CzaFkkjg.js → LoginPage-4WeNcdRi.js} +1 -1
  30. package/dist/public/assets/{PublicViewer-irjxqH6a.js → PublicViewer-CkW-G56D.js} +1 -1
  31. package/dist/public/assets/{SetupWizard-ConWIcmy.js → SetupWizard-CwTr3PCy.js} +1 -1
  32. package/dist/public/assets/{ShareManagement-CP4wdwLR.js → ShareManagement-DSon6CoC.js} +1 -1
  33. package/dist/public/assets/{SourceDetailView-DZS5518E.js → SourceDetailView-BA4XVKqV.js} +1 -1
  34. package/dist/public/assets/UserManagement-BWD2Ugkv.js +1 -0
  35. package/dist/public/assets/{index-C1X8RW4Z.css → index-Cb4omrVn.css} +1 -1
  36. package/dist/public/assets/{index-jlfTO7f5.js → index-iR9gLdl9.js} +41 -41
  37. package/dist/public/assets/{index.es-CgnvEWi5.js → index.es-BuEtjxhO.js} +1 -1
  38. package/dist/public/assets/{jspdf.es.min-Cw5WefMt.js → jspdf.es.min-CScx2_oK.js} +3 -3
  39. package/dist/public/index.html +2 -2
  40. package/dist/{query-2H3YOPI2.js → query-7XHKSZ5H.js} +5 -5
  41. package/dist/sample-YNSJ3I3T.js +76 -0
  42. package/dist/sample-YNSJ3I3T.js.map +1 -0
  43. package/dist/{search-IPE4ISFB.js → search-NKRXXLRS.js} +7 -7
  44. package/dist/semantic-DD7P4634.js +30 -0
  45. package/dist/{semantic-K3MYXXJI.js → semantic-NBAX4WU4.js} +39 -4
  46. package/dist/semantic-NBAX4WU4.js.map +1 -0
  47. package/dist/{source-resolver-HZQLOODU.js → source-resolver-7NY3KEZ2.js} +7 -7
  48. package/dist/source-resolver-7NY3KEZ2.js.map +1 -0
  49. package/dist/{sync-warehouse-TUNULDUY.js → sync-warehouse-PK7HXW7U.js} +15 -11
  50. package/dist/sync-warehouse-PK7HXW7U.js.map +1 -0
  51. package/dist/{tables-K5NAN2WK.js → tables-WQTRZSXR.js} +7 -7
  52. package/dist/templates/default/docs/yamchart-reference.md +26 -0
  53. package/dist/{test-SRHVOXZB.js → test-MKNKUR3N.js} +19 -7
  54. package/dist/test-MKNKUR3N.js.map +1 -0
  55. package/package.json +5 -5
  56. package/dist/chunk-QUIDZO5G.js.map +0 -1
  57. package/dist/chunk-UFDQ3C7Q.js.map +0 -1
  58. package/dist/chunk-ZIY22VO7.js +0 -1190
  59. package/dist/chunk-ZIY22VO7.js.map +0 -1
  60. package/dist/dev-H6SJZ5UA.js.map +0 -1
  61. package/dist/init-SECXKD5G.js.map +0 -1
  62. package/dist/public/assets/DataView-DUCz_96y.js +0 -9
  63. package/dist/public/assets/ExplorePage-kk4z9ldZ.js +0 -1
  64. package/dist/public/assets/UserManagement-AubGd9hl.js +0 -1
  65. package/dist/sample-ODUGGSFA.js +0 -42
  66. package/dist/sample-ODUGGSFA.js.map +0 -1
  67. package/dist/semantic-K3MYXXJI.js.map +0 -1
  68. package/dist/sync-warehouse-TUNULDUY.js.map +0 -1
  69. package/dist/test-SRHVOXZB.js.map +0 -1
  70. /package/dist/{advisor-5TJDNAY7.js.map → advisor-5F73GF54.js.map} +0 -0
  71. /package/dist/{chunk-S2CH4HUZ.js.map → chunk-25ES7MGF.js.map} +0 -0
  72. /package/dist/{chunk-5FHV22X2.js.map → chunk-PYVROL7Z.js.map} +0 -0
  73. /package/dist/{chunk-ZBCQNWVN.js.map → chunk-WVSDQNAN.js.map} +0 -0
  74. /package/dist/{connection-utils-FEUWER5I.js.map → connection-utils-HNX3473B.js.map} +0 -0
  75. /package/dist/{describe-MEP72B56.js.map → describe-KDGXFPDQ.js.map} +0 -0
  76. /package/dist/{dist-5DQO6L2S.js.map → dist-LK3ZG74Q.js.map} +0 -0
  77. /package/dist/{dist-PPAD6KOM.js.map → dist-WMP7BFKP.js.map} +0 -0
  78. /package/dist/{dist-XNCED7JW.js.map → dist-YB5RBWCC.js.map} +0 -0
  79. /package/dist/{query-2H3YOPI2.js.map → query-7XHKSZ5H.js.map} +0 -0
  80. /package/dist/{search-IPE4ISFB.js.map → search-NKRXXLRS.js.map} +0 -0
  81. /package/dist/{source-resolver-HZQLOODU.js.map → semantic-DD7P4634.js.map} +0 -0
  82. /package/dist/{tables-K5NAN2WK.js.map → tables-WQTRZSXR.js.map} +0 -0
@@ -2,7 +2,7 @@ import { createRequire as __yamchartCreateRequire } from 'module'; if (!globalTh
2
2
  import {
3
3
  loadEnvFile,
4
4
  validateProject
5
- } from "./chunk-S2CH4HUZ.js";
5
+ } from "./chunk-25ES7MGF.js";
6
6
  import {
7
7
  box,
8
8
  detail,
@@ -18,9 +18,10 @@ import {
18
18
  filterCatalogEntries
19
19
  } from "./chunk-ZMJPRNOA.js";
20
20
  import {
21
+ buildProvider,
21
22
  createEmbedder,
22
23
  createProviderForTask
23
- } from "./chunk-64CI3HSY.js";
24
+ } from "./chunk-UOSOZT47.js";
24
25
  import {
25
26
  BigQueryConnector,
26
27
  DuckDBConnector,
@@ -39,7 +40,7 @@ import {
39
40
  resolveMySQLAuth,
40
41
  resolvePostgresAuth,
41
42
  resolveSnowflakeAuth
42
- } from "./chunk-QUIDZO5G.js";
43
+ } from "./chunk-5CRXV5PQ.js";
43
44
  import {
44
45
  SemanticModelBuilder,
45
46
  SemanticQueryCompiler,
@@ -56,8 +57,9 @@ import {
56
57
  isRelativeDateRange,
57
58
  loadSemanticDefinitions,
58
59
  validateSemanticQuery
59
- } from "./chunk-UFDQ3C7Q.js";
60
+ } from "./chunk-X3UG6T3Q.js";
60
61
  import {
62
+ AiConfigSchema,
61
63
  ChartSchema,
62
64
  ConnectionSchema,
63
65
  DashboardSchema,
@@ -65,11 +67,14 @@ import {
65
67
  EventsFileSchema,
66
68
  NewBlindspotSchema,
67
69
  ProjectSchema,
70
+ ProviderConfigSchema,
71
+ RuntimeConfigSchema,
72
+ RuntimeConnectionSchema,
68
73
  ScheduleSchema,
69
74
  SemanticQuerySchema,
70
75
  deepMerge,
71
76
  resolveProjectConfig
72
- } from "./chunk-ZIY22VO7.js";
77
+ } from "./chunk-7HWFVUGP.js";
73
78
  import {
74
79
  AuthDatabase,
75
80
  generateOpaqueToken,
@@ -7113,6 +7118,9 @@ var ConfigLoader = class {
7113
7118
  projectDir;
7114
7119
  project = null;
7115
7120
  connections = /* @__PURE__ */ new Map();
7121
+ connectionSecrets = /* @__PURE__ */ new Map();
7122
+ runtimeKnowledgeUrl;
7123
+ runtimeAi;
7116
7124
  charts = /* @__PURE__ */ new Map();
7117
7125
  chartFolders = /* @__PURE__ */ new Map();
7118
7126
  chartFilePaths = /* @__PURE__ */ new Map();
@@ -7133,6 +7141,9 @@ var ConfigLoader = class {
7133
7141
  }
7134
7142
  async load() {
7135
7143
  this.connections.clear();
7144
+ this.connectionSecrets.clear();
7145
+ this.runtimeKnowledgeUrl = void 0;
7146
+ this.runtimeAi = void 0;
7136
7147
  this.charts.clear();
7137
7148
  this.chartFolders.clear();
7138
7149
  this.chartFilePaths.clear();
@@ -7447,6 +7458,31 @@ Fix: run "ulimit -n 10240" before starting yamchart dev, or add it to your shell
7447
7458
  getConnectionByName(name) {
7448
7459
  return this.connections.get(name);
7449
7460
  }
7461
+ /** Merge a runtime config layer over the Git-loaded config. Call after load(). */
7462
+ applyRuntimeOverrides(rc) {
7463
+ for (const rconn of rc.connections ?? []) {
7464
+ const { secrets, ...rest } = rconn;
7465
+ this.connections.set(rconn.name, rest);
7466
+ if (secrets)
7467
+ this.connectionSecrets.set(rconn.name, secrets);
7468
+ }
7469
+ this.runtimeKnowledgeUrl = rc.knowledge?.database_url;
7470
+ this.runtimeAi = rc.ai;
7471
+ }
7472
+ getConnectionSecrets(name) {
7473
+ return this.connectionSecrets.get(name);
7474
+ }
7475
+ /** Knowledge DB URL: runtime override wins, else the named env var. */
7476
+ getKnowledgeDbUrl(envVarName, env) {
7477
+ return this.runtimeKnowledgeUrl ?? env[envVarName];
7478
+ }
7479
+ /** Effective AI config: Git project.ai with the runtime ai deep-merged over it. */
7480
+ getAiConfig() {
7481
+ const base2 = this.project?.ai;
7482
+ if (!this.runtimeAi)
7483
+ return base2;
7484
+ return deepMerge(base2 ?? {}, this.runtimeAi);
7485
+ }
7450
7486
  getCharts() {
7451
7487
  return Array.from(this.charts.values());
7452
7488
  }
@@ -7508,6 +7544,152 @@ Fix: run "ulimit -n 10240" before starting yamchart dev, or add it to your shell
7508
7544
  }
7509
7545
  };
7510
7546
 
7547
+ // ../server/dist/services/runtime-config-store.js
7548
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
7549
+ import { dirname as dirname3, join as join3, isAbsolute } from "path";
7550
+
7551
+ // ../server/dist/services/secret-crypto.js
7552
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
7553
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
7554
+ import { dirname as dirname2, join as join2 } from "path";
7555
+ var ALGO = "aes-256-gcm";
7556
+ function deriveKeyFromEnv(env) {
7557
+ const b64 = env.YAMCHART_CONFIG_SECRET;
7558
+ if (!b64)
7559
+ return null;
7560
+ const key = Buffer.from(b64, "base64");
7561
+ if (key.length !== 32) {
7562
+ throw new Error("YAMCHART_CONFIG_SECRET must be a base64-encoded 32-byte key");
7563
+ }
7564
+ return key;
7565
+ }
7566
+ function getConfigKey(dataDir, env, log) {
7567
+ const fromEnv = deriveKeyFromEnv(env);
7568
+ if (fromEnv)
7569
+ return fromEnv;
7570
+ const keyPath = join2(dataDir, ".config-secret");
7571
+ if (existsSync(keyPath)) {
7572
+ const key2 = Buffer.from(readFileSync(keyPath, "utf8").trim(), "base64");
7573
+ if (key2.length !== 32) {
7574
+ throw new Error(`Key file at ${keyPath} does not contain a 32-byte key (got ${key2.length} bytes). Delete it to regenerate.`);
7575
+ }
7576
+ return key2;
7577
+ }
7578
+ mkdirSync(dirname2(keyPath), { recursive: true });
7579
+ const key = randomBytes(32);
7580
+ writeFileSync(keyPath, key.toString("base64"), { mode: 384 });
7581
+ log?.(`No YAMCHART_CONFIG_SECRET set \u2014 generated a machine-local key at ${keyPath}. Set YAMCHART_CONFIG_SECRET to keep secrets readable across deploys/volumes.`);
7582
+ return key;
7583
+ }
7584
+ function encryptSecret(plain, key) {
7585
+ const iv = randomBytes(12);
7586
+ const cipher = createCipheriv(ALGO, key, iv);
7587
+ const ct = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
7588
+ const tag = cipher.getAuthTag();
7589
+ return `${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
7590
+ }
7591
+ function decryptSecret(enc, key) {
7592
+ const [ivB64, tagB64, ctB64] = enc.split(":");
7593
+ if (!ivB64 || !tagB64 || !ctB64)
7594
+ throw new Error("Malformed encrypted secret");
7595
+ const decipher = createDecipheriv(ALGO, key, Buffer.from(ivB64, "base64"));
7596
+ decipher.setAuthTag(Buffer.from(tagB64, "base64"));
7597
+ return Buffer.concat([decipher.update(Buffer.from(ctB64, "base64")), decipher.final()]).toString("utf8");
7598
+ }
7599
+ function maskSecret(plain) {
7600
+ if (plain.length <= 4)
7601
+ return "\u2022\u2022\u2022\u2022";
7602
+ return "\u2022\u2022\u2022\u2022" + plain.slice(-4);
7603
+ }
7604
+
7605
+ // ../server/dist/services/runtime-config-store.js
7606
+ var RuntimeConfigError = class extends Error {
7607
+ };
7608
+ var SECRET_LEAF_KEYS = /* @__PURE__ */ new Set(["password", "private_key", "credentials_json", "database_url", "api_key"]);
7609
+ function transformSecretLeaves(obj, fn) {
7610
+ if (Array.isArray(obj))
7611
+ return obj.map((v) => transformSecretLeaves(v, fn));
7612
+ if (obj && typeof obj === "object") {
7613
+ const out = {};
7614
+ for (const [k, v] of Object.entries(obj)) {
7615
+ out[k] = SECRET_LEAF_KEYS.has(k) && typeof v === "string" ? fn(v) : transformSecretLeaves(v, fn);
7616
+ }
7617
+ return out;
7618
+ }
7619
+ return obj;
7620
+ }
7621
+ var RuntimeConfigStore = class {
7622
+ filePath;
7623
+ key;
7624
+ constructor(filePath, key) {
7625
+ this.filePath = filePath;
7626
+ this.key = key;
7627
+ }
7628
+ /** Directory the store (and key file) live in — useful for getConfigKey(). */
7629
+ get dataDir() {
7630
+ return dirname3(this.filePath);
7631
+ }
7632
+ static resolvePath(opts) {
7633
+ if (opts.env.YAMCHART_RUNTIME_CONFIG)
7634
+ return opts.env.YAMCHART_RUNTIME_CONFIG;
7635
+ if (opts.authDbPath && isAbsolute(opts.authDbPath))
7636
+ return join3(dirname3(opts.authDbPath), "runtime-config.json");
7637
+ return join3(opts.projectDir, ".yamchart", "runtime-config.json");
7638
+ }
7639
+ read() {
7640
+ if (!existsSync2(this.filePath))
7641
+ return { version: 1 };
7642
+ let parsed;
7643
+ try {
7644
+ parsed = JSON.parse(readFileSync2(this.filePath, "utf8"));
7645
+ } catch (err) {
7646
+ throw new RuntimeConfigError(`runtime-config.json is not valid JSON: ${err.message}`);
7647
+ }
7648
+ let decrypted;
7649
+ try {
7650
+ decrypted = transformSecretLeaves(parsed, (s) => decryptSecret(s, this.key));
7651
+ } catch (err) {
7652
+ throw new RuntimeConfigError(`Could not decrypt runtime-config.json (wrong YAMCHART_CONFIG_SECRET?): ${err.message}`);
7653
+ }
7654
+ const result = RuntimeConfigSchema.safeParse(decrypted);
7655
+ if (!result.success)
7656
+ throw new RuntimeConfigError(`Invalid runtime-config.json: ${result.error.message}`);
7657
+ return result.data;
7658
+ }
7659
+ write(config) {
7660
+ const validated = RuntimeConfigSchema.parse(config);
7661
+ const encrypted = transformSecretLeaves(validated, (s) => encryptSecret(s, this.key));
7662
+ mkdirSync2(dirname3(this.filePath), { recursive: true, mode: 448 });
7663
+ writeFileSync2(this.filePath, JSON.stringify(encrypted, null, 2), { mode: 384 });
7664
+ }
7665
+ };
7666
+
7667
+ // ../server/dist/services/restart.js
7668
+ function requestRestart(env, exit = (c) => process.exit(c), schedule = (fn, ms) => {
7669
+ setTimeout(fn, ms).unref?.();
7670
+ }) {
7671
+ if (env.YAMCHART_DEV_CHILD === "1") {
7672
+ schedule(() => exit(75), 150);
7673
+ return { applying: true, restartRequired: false };
7674
+ }
7675
+ return { applying: false, restartRequired: true };
7676
+ }
7677
+
7678
+ // ../server/dist/services/connection-refs.js
7679
+ function findConnectionRefs(connectionName, input) {
7680
+ const refs = [];
7681
+ const resolves = (explicit) => (explicit ?? input.defaultConnection) === connectionName;
7682
+ for (const c of input.charts) {
7683
+ if (resolves(c.source?.connection))
7684
+ refs.push({ kind: "chart", name: c.name });
7685
+ }
7686
+ for (const m of input.models) {
7687
+ if (resolves(m.metadata.connection))
7688
+ refs.push({ kind: "model", name: m.metadata.name });
7689
+ }
7690
+ return refs;
7691
+ }
7692
+
7511
7693
  // ../server/dist/services/cache.js
7512
7694
  import { LRUCache } from "lru-cache";
7513
7695
  var MemoryCache = class {
@@ -7804,18 +7986,18 @@ var QueryService = class {
7804
7986
  };
7805
7987
 
7806
7988
  // ../server/dist/services/connector-factory.js
7807
- import { join as join2 } from "path";
7989
+ import { join as join4 } from "path";
7808
7990
  function createConnectorFromConfig(connection, options) {
7809
7991
  const { projectDir } = options;
7810
7992
  switch (connection.type) {
7811
7993
  case "duckdb": {
7812
7994
  const duckdbConfig = connection;
7813
- const dbPath = duckdbConfig.config.path.startsWith("/") ? duckdbConfig.config.path : join2(projectDir, duckdbConfig.config.path);
7995
+ const dbPath = duckdbConfig.config.path.startsWith("/") ? duckdbConfig.config.path : join4(projectDir, duckdbConfig.config.path);
7814
7996
  return new DuckDBConnector({ path: dbPath });
7815
7997
  }
7816
7998
  case "postgres": {
7817
7999
  const pgConnection = connection;
7818
- const credentials = resolvePostgresAuth(pgConnection);
8000
+ const credentials = resolvePostgresAuth(pgConnection, options.secrets);
7819
8001
  return new PostgresConnector({
7820
8002
  host: pgConnection.config.host,
7821
8003
  port: pgConnection.config.port,
@@ -7832,7 +8014,7 @@ function createConnectorFromConfig(connection, options) {
7832
8014
  }
7833
8015
  case "mysql": {
7834
8016
  const mysqlConnection = connection;
7835
- const credentials = resolveMySQLAuth(mysqlConnection);
8017
+ const credentials = resolveMySQLAuth(mysqlConnection, options.secrets);
7836
8018
  return new MySQLConnector({
7837
8019
  host: mysqlConnection.config.host,
7838
8020
  port: mysqlConnection.config.port,
@@ -7848,12 +8030,12 @@ function createConnectorFromConfig(connection, options) {
7848
8030
  }
7849
8031
  case "sqlite": {
7850
8032
  const sqliteConnection = connection;
7851
- const dbPath = sqliteConnection.config.path.startsWith("/") ? sqliteConnection.config.path : sqliteConnection.config.path === ":memory:" ? ":memory:" : join2(projectDir, sqliteConnection.config.path);
8033
+ const dbPath = sqliteConnection.config.path.startsWith("/") ? sqliteConnection.config.path : sqliteConnection.config.path === ":memory:" ? ":memory:" : join4(projectDir, sqliteConnection.config.path);
7852
8034
  return new SQLiteConnector({ path: dbPath });
7853
8035
  }
7854
8036
  case "snowflake": {
7855
8037
  const sfConnection = connection;
7856
- const credentials = resolveSnowflakeAuth(sfConnection);
8038
+ const credentials = resolveSnowflakeAuth(sfConnection, options.secrets);
7857
8039
  return new SnowflakeConnector({
7858
8040
  account: sfConnection.config.account,
7859
8041
  username: credentials.username,
@@ -7870,8 +8052,8 @@ function createConnectorFromConfig(connection, options) {
7870
8052
  }
7871
8053
  case "bigquery": {
7872
8054
  const bqConnection = connection;
7873
- const creds = resolveBigQueryAuth(bqConnection);
7874
- const keyFilename = creds.keyFilename ? creds.keyFilename.startsWith("/") ? creds.keyFilename : join2(projectDir, creds.keyFilename) : void 0;
8055
+ const creds = resolveBigQueryAuth(bqConnection, options.secrets);
8056
+ const keyFilename = creds.keyFilename ? creds.keyFilename.startsWith("/") ? creds.keyFilename : join4(projectDir, creds.keyFilename) : void 0;
7875
8057
  return new BigQueryConnector({
7876
8058
  projectId: bqConnection.config.project_id,
7877
8059
  dataset: bqConnection.config.dataset,
@@ -8004,7 +8186,10 @@ async function configRoutes(fastify, options) {
8004
8186
  });
8005
8187
  fastify.get("/api/connections/status", async () => {
8006
8188
  const connections = configLoader.getConnections();
8007
- const results = await Promise.all(connections.map((conn) => testConnection(conn, { projectDir: options.projectDir })));
8189
+ const results = await Promise.all(connections.map((conn) => testConnection(conn, {
8190
+ projectDir: options.projectDir,
8191
+ secrets: options.getConnectionSecrets?.(conn.name)
8192
+ })));
8008
8193
  return { connections: results };
8009
8194
  });
8010
8195
  fastify.get("/api/connections/:name/status", async (request, reply) => {
@@ -8013,7 +8198,10 @@ async function configRoutes(fastify, options) {
8013
8198
  if (!connection) {
8014
8199
  return reply.status(404).send({ error: `Connection not found: ${name}` });
8015
8200
  }
8016
- const result = await testConnection(connection, { projectDir: options.projectDir });
8201
+ const result = await testConnection(connection, {
8202
+ projectDir: options.projectDir,
8203
+ secrets: options.getConnectionSecrets?.(connection.name)
8204
+ });
8017
8205
  return result;
8018
8206
  });
8019
8207
  fastify.get("/api/cache/stats", async () => {
@@ -8492,10 +8680,10 @@ async function chartRoutes(fastify, options) {
8492
8680
 
8493
8681
  // ../server/dist/routes/chart-events.js
8494
8682
  import { readFile as readFile2, writeFile } from "fs/promises";
8495
- import { join as join3 } from "path";
8683
+ import { join as join5 } from "path";
8496
8684
  import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
8497
8685
  async function readEventsFile(projectDir) {
8498
- const eventsPath = join3(projectDir, "events.yaml");
8686
+ const eventsPath = join5(projectDir, "events.yaml");
8499
8687
  try {
8500
8688
  const content = await readFile2(eventsPath, "utf-8");
8501
8689
  const parsed = parseYaml2(content);
@@ -8506,7 +8694,7 @@ async function readEventsFile(projectDir) {
8506
8694
  }
8507
8695
  }
8508
8696
  async function writeEventsFile(projectDir, events) {
8509
- const eventsPath = join3(projectDir, "events.yaml");
8697
+ const eventsPath = join5(projectDir, "events.yaml");
8510
8698
  await writeFile(eventsPath, stringifyYaml({ events }));
8511
8699
  }
8512
8700
  async function chartEventsRoutes(fastify, options) {
@@ -8555,7 +8743,7 @@ async function chartEventsRoutes(fastify, options) {
8555
8743
 
8556
8744
  // ../server/dist/routes/dashboards.js
8557
8745
  import { writeFile as writeFile2 } from "fs/promises";
8558
- import { join as join4 } from "path";
8746
+ import { join as join6 } from "path";
8559
8747
  import { stringify as stringifyYaml2 } from "yaml";
8560
8748
  function classifyError2(error2) {
8561
8749
  const msg = error2 instanceof Error ? error2.message : String(error2);
@@ -8634,7 +8822,7 @@ async function dashboardRoutes(fastify, options) {
8634
8822
  }
8635
8823
  updated = { ...dashboard, layout };
8636
8824
  }
8637
- const filePath = configLoader.getDashboardFilePath(id) ?? join4(projectDir, "dashboards", `${id}.yaml`);
8825
+ const filePath = configLoader.getDashboardFilePath(id) ?? join6(projectDir, "dashboards", `${id}.yaml`);
8638
8826
  await writeFile2(filePath, stringifyYaml2(updated));
8639
8827
  await configLoader.load();
8640
8828
  const commitMessage = message || `Update ${dashboard.title} layout`;
@@ -8830,11 +9018,11 @@ async function semanticRoutes(fastify, options) {
8830
9018
  errorType: "validation_error"
8831
9019
  });
8832
9020
  }
8833
- const { writeFileSync, mkdirSync } = await import("fs");
8834
- const { join: join12, dirname: dirname3 } = await import("path");
8835
- const fullPath = join12(projectDir, filePath);
8836
- mkdirSync(dirname3(fullPath), { recursive: true });
8837
- writeFileSync(fullPath, sql, "utf-8");
9021
+ const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync3 } = await import("fs");
9022
+ const { join: join14, dirname: dirname5 } = await import("path");
9023
+ const fullPath = join14(projectDir, filePath);
9024
+ mkdirSync3(dirname5(fullPath), { recursive: true });
9025
+ writeFileSync3(fullPath, sql, "utf-8");
8838
9026
  return { saved: true, path: filePath };
8839
9027
  });
8840
9028
  fastify.post("/api/semantic/rebuild", async () => {
@@ -8849,8 +9037,8 @@ async function semanticRoutes(fastify, options) {
8849
9037
  }
8850
9038
 
8851
9039
  // ../server/dist/services/semantic-service.js
8852
- import { readFileSync, existsSync } from "fs";
8853
- import { join as join5 } from "path";
9040
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
9041
+ import { join as join7 } from "path";
8854
9042
  var SemanticService = class {
8855
9043
  model = null;
8856
9044
  builder;
@@ -8867,10 +9055,10 @@ var SemanticService = class {
8867
9055
  this.model = this.builder.build(input);
8868
9056
  }
8869
9057
  buildFromDisk(models, chartHints) {
8870
- const catalogPath = join5(this.projectDir, ".yamchart", "catalog.json");
9058
+ const catalogPath = join7(this.projectDir, ".yamchart", "catalog.json");
8871
9059
  let catalog;
8872
- if (existsSync(catalogPath)) {
8873
- const raw = readFileSync(catalogPath, "utf-8");
9060
+ if (existsSync3(catalogPath)) {
9061
+ const raw = readFileSync3(catalogPath, "utf-8");
8874
9062
  catalog = JSON.parse(raw);
8875
9063
  }
8876
9064
  this.build({ catalog, models, chartHints });
@@ -9190,6 +9378,14 @@ var GitService = class {
9190
9378
  }
9191
9379
  };
9192
9380
 
9381
+ // ../server/dist/services/ai-error-redact.js
9382
+ function redactProviderError(msg, apiKey) {
9383
+ let out = msg.replace(/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, "[url redacted]");
9384
+ if (apiKey)
9385
+ out = out.split(apiKey).join("[redacted]");
9386
+ return out;
9387
+ }
9388
+
9193
9389
  // ../../packages/knowledge/dist/memory-store.js
9194
9390
  import { randomUUID } from "crypto";
9195
9391
 
@@ -9474,6 +9670,63 @@ var PostgresKnowledgeStore = class {
9474
9670
  const r = rows[0];
9475
9671
  return { id: r.id, connection: r.connection, source_kind: r.source_kind, source_id: r.source_id, text: r.text, vector: r.vector };
9476
9672
  }
9673
+ async exportAll() {
9674
+ const profilesRes = await this.pool.query(`SELECT connection, profile FROM source_profiles`);
9675
+ return {
9676
+ profiles: profilesRes.rows.map((r) => ({ connection: r.connection, profile: r.profile })),
9677
+ hunches: await this.listHunches({}),
9678
+ blindspots: await this.listBlindspots({}),
9679
+ knowledgeDocs: await this.listKnowledgeDocs({}),
9680
+ embeddings: await this.listEmbeddings({})
9681
+ };
9682
+ }
9683
+ async importAll(snapshot) {
9684
+ for (const p of snapshot.profiles)
9685
+ await this.saveProfile(p.connection, p.profile);
9686
+ for (const h of snapshot.hunches) {
9687
+ await this.pool.query(`INSERT INTO hunches (id, connection, question, sql, generated_sql, status, tables_used, rationale, confidence, last_run_sample, created_at, updated_at)
9688
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT (id) DO NOTHING`, [
9689
+ h.id,
9690
+ h.connection,
9691
+ h.question,
9692
+ h.sql,
9693
+ h.generatedSql,
9694
+ h.status,
9695
+ JSON.stringify(h.tablesUsed ?? []),
9696
+ h.rationale ?? null,
9697
+ h.confidence ?? null,
9698
+ h.lastRunSample ? JSON.stringify(h.lastRunSample) : null,
9699
+ h.createdAt,
9700
+ h.updatedAt
9701
+ ]);
9702
+ }
9703
+ for (const b of snapshot.blindspots) {
9704
+ await this.pool.query(`INSERT INTO blindspots (id, connection, kind, title, detail, subject, suggested_model, confidence, status, answer, created_at, updated_at)
9705
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT (id) DO NOTHING`, [
9706
+ b.id,
9707
+ b.connection,
9708
+ b.kind,
9709
+ b.title,
9710
+ b.detail,
9711
+ b.subject ?? null,
9712
+ JSON.stringify(b.suggestedModel ?? null),
9713
+ b.confidence ?? null,
9714
+ b.status,
9715
+ b.answer ?? null,
9716
+ b.createdAt,
9717
+ b.updatedAt
9718
+ ]);
9719
+ }
9720
+ for (const d of snapshot.knowledgeDocs) {
9721
+ await this.pool.query(`INSERT INTO knowledge_docs (id, connection, title, content, source, created_at, updated_at)
9722
+ VALUES ($1,$2,$3,$4,$5,$6,$7) ON CONFLICT (id) DO NOTHING`, [d.id, d.connection, d.title, d.content, d.source, d.createdAt, d.updatedAt]);
9723
+ }
9724
+ for (const e of snapshot.embeddings)
9725
+ await this.upsertEmbedding(e);
9726
+ }
9727
+ async close() {
9728
+ await this.pool.end();
9729
+ }
9477
9730
  };
9478
9731
 
9479
9732
  // ../../node_modules/.pnpm/tslib@2.8.1/node_modules/tslib/tslib.es6.mjs
@@ -12165,8 +12418,8 @@ var RealtimeChannel = class _RealtimeChannel {
12165
12418
  _trigger(type, payload, ref) {
12166
12419
  var _a, _b;
12167
12420
  const typeLower = type.toLocaleLowerCase();
12168
- const { close, error: error2, leave, join: join12 } = CHANNEL_EVENTS;
12169
- const events = [close, error2, leave, join12];
12421
+ const { close, error: error2, leave, join: join14 } = CHANNEL_EVENTS;
12422
+ const events = [close, error2, leave, join14];
12170
12423
  if (ref && events.indexOf(typeLower) >= 0 && ref !== this._joinRef()) {
12171
12424
  return;
12172
12425
  }
@@ -21598,14 +21851,14 @@ async function publicRoutes(fastify, options) {
21598
21851
  }
21599
21852
 
21600
21853
  // ../server/dist/routes/chat.js
21601
- import { resolve, join as join6 } from "path";
21854
+ import { resolve, join as join8 } from "path";
21602
21855
  import { readFile as readFile3 } from "fs/promises";
21603
21856
  function isChatEnabled(configLoader) {
21604
21857
  const project = configLoader.getProject();
21605
21858
  return !!(project?.features?.chat && process.env.ANTHROPIC_API_KEY);
21606
21859
  }
21607
21860
  async function chatRoutes(fastify, options) {
21608
- const { configLoader, queryService } = options;
21861
+ const { configLoader, queryService, aiConfig } = options;
21609
21862
  fastify.post("/api/chat/generate-text", async (request, reply) => {
21610
21863
  const { prompt, mode, dashboard: dashboardName, existingContent, filters } = request.body ?? {};
21611
21864
  if (!prompt || !mode || !dashboardName) {
@@ -21616,10 +21869,9 @@ async function chatRoutes(fastify, options) {
21616
21869
  if (mode !== "insert" && mode !== "replace") {
21617
21870
  return reply.status(400).send({ error: 'mode must be "insert" or "replace"' });
21618
21871
  }
21619
- const apiKey = process.env.ANTHROPIC_API_KEY;
21620
- if (!apiKey) {
21872
+ if (!aiConfig && !process.env.ANTHROPIC_API_KEY) {
21621
21873
  return reply.status(400).send({
21622
- error: "AI text generation requires an API key. Set ANTHROPIC_API_KEY in your environment."
21874
+ error: "AI text generation requires an API key. Set ANTHROPIC_API_KEY in your environment, or configure an ai: block in yamchart.yaml."
21623
21875
  });
21624
21876
  }
21625
21877
  const dashboard = configLoader.getDashboardByName(dashboardName);
@@ -21726,8 +21978,7 @@ async function chatRoutes(fastify, options) {
21726
21978
  systemParts.push("Output ONLY the new text to insert. Do NOT repeat the existing content.");
21727
21979
  }
21728
21980
  const systemPrompt = systemParts.join("\n");
21729
- const { AnthropicProvider } = await import("./dist-5DQO6L2S.js");
21730
- const provider = new AnthropicProvider(apiKey);
21981
+ const provider = createProviderForTask(aiConfig, "text_generation", process.env);
21731
21982
  const response = await provider.chat({
21732
21983
  system: systemPrompt,
21733
21984
  messages: [{ role: "user", content: prompt }],
@@ -21850,7 +22101,7 @@ IMPORTANT: When using query_model, pass the chart name (e.g. "${chartRefs[0] ??
21850
22101
  The user is viewing the "${chart.title ?? chart.name}" chart (id: "${chart.name}", type: ${chart.chart?.type}).${model}${params}`;
21851
22102
  }
21852
22103
  } else if (chatContext.type === "explore") {
21853
- const catalogPath = join6(options.projectDir, ".yamchart", "catalog.json");
22104
+ const catalogPath = join8(options.projectDir, ".yamchart", "catalog.json");
21854
22105
  const tableLines = [];
21855
22106
  try {
21856
22107
  const catalogData = JSON.parse(await readFile3(catalogPath, "utf-8"));
@@ -28839,10 +29090,10 @@ async function runModel(deps, args) {
28839
29090
  }
28840
29091
 
28841
29092
  // ../../packages/mcp-core/dist/catalog.js
28842
- import { join as join7 } from "path";
29093
+ import { join as join9 } from "path";
28843
29094
  import { readFile as readFile4 } from "fs/promises";
28844
29095
  async function readCatalogTables(projectDir, include) {
28845
- const catalogPath = join7(projectDir, ".yamchart", "catalog.json");
29096
+ const catalogPath = join9(projectDir, ".yamchart", "catalog.json");
28846
29097
  let parsed;
28847
29098
  try {
28848
29099
  parsed = JSON.parse(await readFile4(catalogPath, "utf-8"));
@@ -29040,7 +29291,7 @@ async function visualize(_deps, args) {
29040
29291
 
29041
29292
  // ../../packages/mcp-core/dist/tools/save.js
29042
29293
  import { mkdir, writeFile as writeFile3 } from "fs/promises";
29043
- import { join as join8 } from "path";
29294
+ import { join as join10 } from "path";
29044
29295
  import { stringify as stringifyYaml3 } from "yaml";
29045
29296
  var SAFE_NAME = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
29046
29297
  async function saveChart(deps, args) {
@@ -29058,9 +29309,9 @@ async function saveChart(deps, args) {
29058
29309
  if (name.length > 200) {
29059
29310
  return `Error: Chart name is too long (${name.length} chars). Maximum is 200.`;
29060
29311
  }
29061
- const chartsDir = join8(deps.projectDir, "charts");
29312
+ const chartsDir = join10(deps.projectDir, "charts");
29062
29313
  await mkdir(chartsDir, { recursive: true });
29063
- await writeFile3(join8(chartsDir, `${name}.yaml`), stringifyYaml3(parsed.data), "utf-8");
29314
+ await writeFile3(join10(chartsDir, `${name}.yaml`), stringifyYaml3(parsed.data), "utf-8");
29064
29315
  await deps.configLoader.load();
29065
29316
  return `Chart "${name}" saved to charts/${name}.yaml`;
29066
29317
  }
@@ -29367,15 +29618,15 @@ async function scanBlindspots(input) {
29367
29618
  }
29368
29619
 
29369
29620
  // ../../packages/learning/dist/context.js
29370
- import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync } from "fs";
29371
- import { join as join9 } from "path";
29621
+ import { readFileSync as readFileSync4, existsSync as existsSync4, readdirSync } from "fs";
29622
+ import { join as join11 } from "path";
29372
29623
  var MAX_DOC = 6e3;
29373
29624
  var MAX_TOTAL = 16e3;
29374
29625
  function loadKnowledgeDocs(projectDir) {
29375
- const dir = join9(projectDir, "knowledge");
29376
- if (!existsSync2(dir))
29626
+ const dir = join11(projectDir, "knowledge");
29627
+ if (!existsSync4(dir))
29377
29628
  return [];
29378
- return readdirSync(dir).filter((f) => /\.md$/i.test(f)).map((f) => readFileSync2(join9(dir, f), "utf8"));
29629
+ return readdirSync(dir).filter((f) => /\.md$/i.test(f)).map((f) => readFileSync4(join11(dir, f), "utf8"));
29379
29630
  }
29380
29631
  function buildContextBlock(input) {
29381
29632
  const parts = [];
@@ -29494,6 +29745,34 @@ function limit(sql) {
29494
29745
  return /\blimit\b/i.test(sql) ? sql : `${sql.replace(/;\s*$/, "")} LIMIT 1000`;
29495
29746
  }
29496
29747
 
29748
+ // ../../packages/learning/dist/reindex.js
29749
+ async function reindexEmbeddings(input) {
29750
+ const { store, embedder, connection } = input;
29751
+ const corpus = [];
29752
+ for (const h of await store.listHunches({ connection, status: "confirmed" })) {
29753
+ corpus.push({ kind: "hunch", id: h.id, connection: h.connection, text: h.question });
29754
+ }
29755
+ for (const b of await store.listBlindspots({ connection, status: "answered" })) {
29756
+ if (b.answer)
29757
+ corpus.push({ kind: "blindspot", id: b.id, connection: b.connection, text: `${b.title}: ${b.answer}` });
29758
+ }
29759
+ for (const d of await store.listKnowledgeDocs({ connection })) {
29760
+ corpus.push({ kind: "knowledge", id: d.id, connection: d.connection, text: `${d.title}
29761
+ ${d.content}` });
29762
+ }
29763
+ if (corpus.length === 0)
29764
+ return 0;
29765
+ const vectors = await embedder.embed(corpus.map((c) => c.text));
29766
+ if (vectors.length !== corpus.length) {
29767
+ throw new Error(`Embedder returned ${vectors.length} vectors for ${corpus.length} texts`);
29768
+ }
29769
+ for (let i = 0; i < corpus.length; i++) {
29770
+ const item = corpus[i];
29771
+ await store.upsertEmbedding({ connection: item.connection, source_kind: item.kind, source_id: item.id, text: item.text, vector: vectors[i] });
29772
+ }
29773
+ return corpus.length;
29774
+ }
29775
+
29497
29776
  // ../../packages/mcp-core/dist/tools/answer.js
29498
29777
  async function answerQuestionTool(deps, args) {
29499
29778
  const connection = args.connection ?? deps.defaultConnectionName ?? "default";
@@ -29599,31 +29878,129 @@ var text = (s) => ({
29599
29878
  content: [{ type: "text", text: s }],
29600
29879
  ...s.startsWith("Error:") ? { isError: true } : {}
29601
29880
  });
29881
+ var READ_ONLY = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
29882
+ var WRITE_SAFE = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
29883
+ function buildResourceMeta(baseUrl) {
29884
+ return {
29885
+ ui: {
29886
+ domain: baseUrl,
29887
+ csp: {
29888
+ connectDomains: [baseUrl],
29889
+ resourceDomains: ["https://persistent.oaistatic.com"]
29890
+ }
29891
+ }
29892
+ };
29893
+ }
29602
29894
  function createMcpServer(deps) {
29603
29895
  const server = new McpServer({ name: "yamchart", version: "0.1.0" });
29604
- server.registerResource("chart-renderer", TEMPLATE_URI, { mimeType: "text/html;profile=mcp-app", description: "Interactive yamchart chart renderer" }, async () => {
29896
+ const baseUrl = deps.baseUrl ?? process.env["BASE_URL"] ?? "https://yamchart.example.com";
29897
+ server.registerResource("chart-renderer", TEMPLATE_URI, {
29898
+ mimeType: "text/html;profile=mcp-app",
29899
+ description: "Interactive yamchart chart renderer",
29900
+ _meta: buildResourceMeta(baseUrl)
29901
+ }, async () => {
29605
29902
  const tpl = await buildTemplateResource();
29606
29903
  return { contents: [{ uri: TEMPLATE_URI, mimeType: tpl.resource.mimeType, text: tpl.resource.text ?? "" }] };
29607
29904
  });
29608
- server.registerTool("list_charts", { description: "List governed yamchart charts (name, title, type, model, parameters).", inputSchema: {} }, async () => text(await listCharts(deps)));
29609
- server.registerTool("list_models", { description: "List governed SQL models (name, description).", inputSchema: {} }, async () => text(await listModels(deps)));
29905
+ server.registerTool("list_charts", {
29906
+ description: "List governed yamchart charts (name, title, type, model, parameters).",
29907
+ inputSchema: {},
29908
+ annotations: READ_ONLY
29909
+ }, async () => {
29910
+ const raw = await listCharts(deps);
29911
+ const charts = JSON.parse(raw);
29912
+ return {
29913
+ content: [{ type: "text", text: raw }],
29914
+ structuredContent: { charts }
29915
+ };
29916
+ });
29917
+ server.registerTool("list_models", {
29918
+ description: "List governed SQL models (name, description).",
29919
+ inputSchema: {},
29920
+ annotations: READ_ONLY
29921
+ }, async () => {
29922
+ const raw = await listModels(deps);
29923
+ const models = JSON.parse(raw);
29924
+ return {
29925
+ content: [{ type: "text", text: raw }],
29926
+ structuredContent: { models }
29927
+ };
29928
+ });
29610
29929
  server.registerTool("run_model", {
29611
29930
  description: "Run a governed chart/model by name with parameters and return its data.",
29612
- inputSchema: { name: z2.string(), params: z2.record(z2.unknown()).optional() }
29613
- }, async (args) => text(await runModel(deps, args)));
29931
+ inputSchema: { name: z2.string(), params: z2.record(z2.unknown()).optional() },
29932
+ annotations: READ_ONLY
29933
+ }, async (args) => {
29934
+ const raw = await runModel(deps, args);
29935
+ if (raw.startsWith("Error:"))
29936
+ return { content: [{ type: "text", text: raw }], isError: true };
29937
+ const parsed = JSON.parse(raw);
29938
+ return {
29939
+ content: [{ type: "text", text: raw }],
29940
+ structuredContent: {
29941
+ rowCount: parsed.rows.length,
29942
+ columns: parsed.columns.map((c) => c.name),
29943
+ preview: parsed.rows.slice(0, 5)
29944
+ }
29945
+ };
29946
+ });
29614
29947
  server.registerTool("run_sql", {
29615
29948
  description: "Run a read-only ad-hoc SQL query (destructive statements blocked, row-capped).",
29616
- inputSchema: { sql: z2.string(), limit: z2.number().optional() }
29617
- }, async (args) => text(await runSql(deps, args)));
29618
- server.registerTool("list_tables", { description: "List warehouse tables from the cached catalog (name, schema, connection, column count).", inputSchema: {} }, async () => text(await listTables(deps)));
29949
+ inputSchema: { sql: z2.string(), limit: z2.number().optional() },
29950
+ annotations: READ_ONLY
29951
+ }, async (args) => {
29952
+ const raw = await runSql(deps, args);
29953
+ if (raw.startsWith("Error:"))
29954
+ return { content: [{ type: "text", text: raw }], isError: true };
29955
+ const parsed = JSON.parse(raw);
29956
+ return {
29957
+ content: [{ type: "text", text: raw }],
29958
+ structuredContent: {
29959
+ rowCount: parsed.rows.length,
29960
+ columns: parsed.columns.map((c) => c.name),
29961
+ preview: parsed.rows.slice(0, 5)
29962
+ }
29963
+ };
29964
+ });
29965
+ server.registerTool("list_tables", {
29966
+ description: "List warehouse tables from the cached catalog (name, schema, connection, column count).",
29967
+ inputSchema: {},
29968
+ annotations: READ_ONLY
29969
+ }, async () => {
29970
+ const raw = await listTables(deps);
29971
+ const tables = JSON.parse(raw);
29972
+ return {
29973
+ content: [{ type: "text", text: raw }],
29974
+ structuredContent: { tables }
29975
+ };
29976
+ });
29619
29977
  server.registerTool("describe_table", {
29620
29978
  description: 'Show columns and types for a catalog table. Accepts { table } (with optional dot-notation schema: "schema.table") or { table, schema } separately.',
29621
- inputSchema: { table: z2.string(), schema: z2.string().optional() }
29979
+ inputSchema: { table: z2.string(), schema: z2.string().optional() },
29980
+ annotations: READ_ONLY
29622
29981
  }, async (args) => text(await describeTable(deps, args)));
29623
- server.registerTool("sample_table", { description: "Return a few sample rows from a table.", inputSchema: { table: z2.string(), limit: z2.number().optional() } }, async (args) => text(await sampleTable(deps, args)));
29982
+ server.registerTool("sample_table", {
29983
+ description: "Return a few sample rows from a table.",
29984
+ inputSchema: { table: z2.string(), limit: z2.number().optional() },
29985
+ annotations: READ_ONLY
29986
+ }, async (args) => {
29987
+ const raw = await sampleTable(deps, args);
29988
+ if (raw.startsWith("Error:"))
29989
+ return { content: [{ type: "text", text: raw }], isError: true };
29990
+ const parsed = JSON.parse(raw);
29991
+ return {
29992
+ content: [{ type: "text", text: raw }],
29993
+ structuredContent: {
29994
+ rowCount: parsed.rows.length,
29995
+ columns: parsed.columns.map((c) => c.name),
29996
+ preview: parsed.rows.slice(0, 5)
29997
+ }
29998
+ };
29999
+ });
29624
30000
  server.registerTool("visualize", {
29625
30001
  description: "Render a yamchart chart spec + data rows to an interactive chart (with a static image fallback). Provide a full chart spec (name, title, source.model, chart) and the data array.",
29626
30002
  inputSchema: { spec: z2.record(z2.unknown()), data: z2.array(z2.record(z2.unknown())) },
30003
+ annotations: READ_ONLY,
29627
30004
  _meta: {
29628
30005
  ui: { resourceUri: TEMPLATE_URI },
29629
30006
  "openai/outputTemplate": TEMPLATE_URI
@@ -29645,17 +30022,27 @@ function createMcpServer(deps) {
29645
30022
  });
29646
30023
  server.registerTool("save_chart", {
29647
30024
  description: "Save a yamchart chart spec to charts/<name>.yaml (requires editor/admin).",
29648
- inputSchema: { spec: z2.record(z2.unknown()) }
30025
+ inputSchema: { spec: z2.record(z2.unknown()) },
30026
+ annotations: WRITE_SAFE
29649
30027
  }, async (args) => text(await saveChart(deps, args)));
29650
30028
  if (deps.semanticLayer) {
29651
30029
  const semanticLayer = deps.semanticLayer;
29652
30030
  server.registerTool("list_metrics", {
29653
30031
  description: "List governed business metrics (the semantic glossary): name, synonyms, the entity they belong to, and which dimensions you can slice them by. Prefer querying these by name over writing raw SQL.",
29654
- inputSchema: {}
29655
- }, async () => text(listMetrics({ semanticLayer })));
30032
+ inputSchema: {},
30033
+ annotations: READ_ONLY
30034
+ }, async () => {
30035
+ const raw = listMetrics({ semanticLayer });
30036
+ const metrics = JSON.parse(raw);
30037
+ return {
30038
+ content: [{ type: "text", text: raw }],
30039
+ structuredContent: { metrics }
30040
+ };
30041
+ });
29656
30042
  server.registerTool("get_metric", {
29657
30043
  description: "Get the full governed definition of a metric (by name or synonym): how it is computed (aggregation, field/sql, governed filter), its source table, format, and the dimensions/time dimension you can group it by.",
29658
- inputSchema: { name: z2.string() }
30044
+ inputSchema: { name: z2.string() },
30045
+ annotations: READ_ONLY
29659
30046
  }, async (args) => text(getMetric({ semanticLayer }, args)));
29660
30047
  server.registerTool("query_metric", {
29661
30048
  description: "Query governed metrics by name (or synonym), optionally sliced by dimensions and a time grain, and filtered by a time range. Returns the data rows, the generated SQL (for transparency), and a ready-to-use suggested_chart spec. Prefer this over run_sql for business questions about governed metrics \u2014 it guarantees correct, governed definitions.",
@@ -29670,10 +30057,25 @@ function createMcpServer(deps) {
29670
30057
  filters: z2.array(z2.object({ dimension: z2.string(), operator: z2.string(), value: z2.any() })).optional(),
29671
30058
  order_by: z2.object({ field: z2.string(), direction: z2.enum(["asc", "desc"]).optional() }).optional(),
29672
30059
  limit: z2.number().int().positive().max(1e4).optional()
29673
- }
30060
+ },
30061
+ annotations: READ_ONLY
29674
30062
  }, async (args) => {
29675
30063
  try {
29676
- return text(await queryMetric({ semanticLayer, queryService: deps.queryService, connectionType: deps.connectionType }, args));
30064
+ const raw = await queryMetric({ semanticLayer, queryService: deps.queryService, connectionType: deps.connectionType }, args);
30065
+ if (raw.startsWith("Error:") || raw.startsWith("{") && JSON.parse(raw).error) {
30066
+ return text(raw);
30067
+ }
30068
+ const parsed = JSON.parse(raw);
30069
+ return {
30070
+ content: [{ type: "text", text: raw }],
30071
+ structuredContent: {
30072
+ rowCount: parsed.rows.length,
30073
+ columns: parsed.columns.map((c) => c.name),
30074
+ preview: parsed.rows.slice(0, 5),
30075
+ sql: parsed.sql,
30076
+ suggested_chart: parsed.suggested_chart
30077
+ }
30078
+ };
29677
30079
  } catch (err) {
29678
30080
  return text(`Error: ${err instanceof Error ? err.message : String(err)}`);
29679
30081
  }
@@ -29685,27 +30087,48 @@ function createMcpServer(deps) {
29685
30087
  const answerProvider = deps.answerProvider;
29686
30088
  server.registerTool("answer_question", {
29687
30089
  description: 'Answer a business question using governed knowledge: retrieves confirmed Hunches + business context for the source and either reuses a matching SQL or generates SQL grounded in trusted examples. Prefer this for "how many / how much / which" questions.',
29688
- inputSchema: { question: z2.string(), connection: z2.string().optional() }
29689
- }, async (args) => text(await answerQuestionTool({
29690
- knowledgeStore,
29691
- embedder,
29692
- provider: answerProvider,
29693
- queryService: deps.queryService,
29694
- connectionType: deps.connectionType,
29695
- defaultConnectionName: deps.aiConnectionName
29696
- }, args)));
30090
+ inputSchema: { question: z2.string(), connection: z2.string().optional() },
30091
+ annotations: READ_ONLY
30092
+ }, async (args) => {
30093
+ const raw = await answerQuestionTool({
30094
+ knowledgeStore,
30095
+ embedder,
30096
+ provider: answerProvider,
30097
+ queryService: deps.queryService,
30098
+ connectionType: deps.connectionType,
30099
+ defaultConnectionName: deps.aiConnectionName
30100
+ }, args);
30101
+ if (raw.startsWith("Error:"))
30102
+ return { content: [{ type: "text", text: raw }], isError: true };
30103
+ const parsed = JSON.parse(raw);
30104
+ const rows = parsed.rows ?? [];
30105
+ const columns = parsed.columns ?? [];
30106
+ return {
30107
+ content: [{ type: "text", text: raw }],
30108
+ structuredContent: {
30109
+ rowCount: rows.length,
30110
+ columns: columns.map((c) => c.name),
30111
+ preview: rows.slice(0, 5),
30112
+ sql: parsed.sql,
30113
+ answer: parsed.answer
30114
+ }
30115
+ };
30116
+ });
29697
30117
  }
29698
30118
  server.registerTool("get_chart_schema", {
29699
30119
  description: "Get the exact yamchart chart spec shape and a working example for a chart type (bar, line, area, kpi, pie, scatter, table). Call this before building a chart. Axes are objects: { field, type: temporal|quantitative|ordinal|nominal }.",
29700
- inputSchema: { type: z2.string().optional() }
30120
+ inputSchema: { type: z2.string().optional() },
30121
+ annotations: READ_ONLY
29701
30122
  }, async (args) => text(getChartSchema(args)));
29702
30123
  server.registerTool("validate_chart", {
29703
30124
  description: "Validate a yamchart chart spec without saving it. Returns { valid: true } or field-level errors with hints so you can self-correct before calling save_chart or visualize.",
29704
- inputSchema: { spec: z2.record(z2.unknown()) }
30125
+ inputSchema: { spec: z2.record(z2.unknown()) },
30126
+ annotations: READ_ONLY
29705
30127
  }, async (args) => text(validateChart(args)));
29706
30128
  server.registerTool("get_chart_spec", {
29707
30129
  description: "Get the full saved spec of an existing governed chart by name (so you can copy or adapt it). Call list_charts to discover names.",
29708
- inputSchema: { name: z2.string() }
30130
+ inputSchema: { name: z2.string() },
30131
+ annotations: READ_ONLY
29709
30132
  }, async (args) => text(getChartSpec({ configLoader: deps.configLoader }, args)));
29710
30133
  return server;
29711
30134
  }
@@ -29730,7 +30153,8 @@ async function mcpRoutes(fastify, options) {
29730
30153
  knowledgeStore,
29731
30154
  embedder,
29732
30155
  answerProvider,
29733
- aiConnectionName
30156
+ aiConnectionName,
30157
+ baseUrl: process.env["BASE_URL"]
29734
30158
  });
29735
30159
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
29736
30160
  reply.raw.on("close", () => {
@@ -29754,7 +30178,7 @@ async function mcpRoutes(fastify, options) {
29754
30178
  }
29755
30179
 
29756
30180
  // ../server/dist/routes/catalog.js
29757
- import { join as join10 } from "path";
30181
+ import { join as join12 } from "path";
29758
30182
  import { readFile as readFile5 } from "fs/promises";
29759
30183
  async function catalogRoutes(fastify, options) {
29760
30184
  const { configLoader, projectDir } = options;
@@ -29767,7 +30191,7 @@ async function catalogRoutes(fastify, options) {
29767
30191
  name: m.metadata.name,
29768
30192
  description: m.metadata.description
29769
30193
  }));
29770
- const catalogPath = join10(projectDir, ".yamchart", "catalog.json");
30194
+ const catalogPath = join12(projectDir, ".yamchart", "catalog.json");
29771
30195
  let tables = [];
29772
30196
  try {
29773
30197
  const catalogData = JSON.parse(await readFile5(catalogPath, "utf-8"));
@@ -29826,10 +30250,14 @@ async function runHunch(deps, id) {
29826
30250
  if (!isReadOnlySql(hunch.sql))
29827
30251
  return { error: "Only read-only SELECT queries can be run." };
29828
30252
  const limited = /\blimit\b/i.test(hunch.sql) ? hunch.sql : `${hunch.sql.replace(/;\s*$/, "")} LIMIT 50`;
29829
- const res = await deps.queryService.executeRawSql(limited);
29830
- const sample = { columns: res.columns, rows: res.rows, ranAt: (/* @__PURE__ */ new Date()).toISOString() };
29831
- await deps.store.updateHunch(id, { lastRunSample: sample });
29832
- return { sample };
30253
+ try {
30254
+ const res = await deps.queryService.executeRawSql(limited);
30255
+ const sample = { columns: res.columns, rows: res.rows, ranAt: (/* @__PURE__ */ new Date()).toISOString() };
30256
+ await deps.store.updateHunch(id, { lastRunSample: sample });
30257
+ return { sample };
30258
+ } catch (e) {
30259
+ return { error: e.message };
30260
+ }
29833
30261
  }
29834
30262
  async function answerBlindspot(deps, id, body) {
29835
30263
  return deps.store.updateBlindspot(id, { status: "answered", answer: body.answer });
@@ -29895,7 +30323,17 @@ async function learningRoutes(fastify, opts) {
29895
30323
  contextBlock
29896
30324
  });
29897
30325
  const created = await store.createHunches(candidates);
29898
- send("done", { hunches: created });
30326
+ const threshold = opts.getConnectionThreshold?.(connection);
30327
+ let autoConfirmed = 0;
30328
+ if (threshold != null) {
30329
+ for (const h of created) {
30330
+ if ((h.confidence ?? 0) >= threshold) {
30331
+ await store.updateHunch(h.id, { status: "confirmed" });
30332
+ autoConfirmed++;
30333
+ }
30334
+ }
30335
+ }
30336
+ send("done", { hunches: created, autoConfirmed });
29899
30337
  } catch (e) {
29900
30338
  send("error", { message: e.message });
29901
30339
  }
@@ -29940,7 +30378,12 @@ async function learningRoutes(fastify, opts) {
29940
30378
  reply.code(403).send({ error: "Requires editor or admin role." });
29941
30379
  return;
29942
30380
  }
29943
- return runHunch({ store, queryService }, request.params.id);
30381
+ const result = await runHunch({ store, queryService }, request.params.id);
30382
+ if (result.error) {
30383
+ reply.code(422).send({ error: result.error });
30384
+ return;
30385
+ }
30386
+ return result;
29944
30387
  });
29945
30388
  fastify.post("/api/blindspots/scan", async (request, reply) => {
29946
30389
  if (!requireStore(reply))
@@ -30017,6 +30460,36 @@ async function learningRoutes(fastify, opts) {
30017
30460
  }
30018
30461
  return dismissBlindspot({ store }, request.params.id);
30019
30462
  });
30463
+ fastify.get("/api/coverage", async (request, reply) => {
30464
+ if (!requireStore(reply))
30465
+ return;
30466
+ const q = request.query;
30467
+ const connection = q.connection ?? opts.defaultConnectionName;
30468
+ const [hunches, blindspots] = await Promise.all([
30469
+ store.listHunches({ connection }),
30470
+ store.listBlindspots({ connection })
30471
+ ]);
30472
+ const profile = await store.getProfile(connection);
30473
+ const profileTables = profile?.tables?.map((t) => t.table) ?? [];
30474
+ const tableCounts = {};
30475
+ const ensure = (t) => {
30476
+ tableCounts[t] ??= { hunches: 0, blindspots: 0 };
30477
+ };
30478
+ profileTables.forEach(ensure);
30479
+ for (const h of hunches) {
30480
+ for (const t of h.tablesUsed ?? []) {
30481
+ ensure(t);
30482
+ tableCounts[t].hunches++;
30483
+ }
30484
+ }
30485
+ for (const b of blindspots) {
30486
+ if (b.subject) {
30487
+ ensure(b.subject);
30488
+ tableCounts[b.subject].blindspots++;
30489
+ }
30490
+ }
30491
+ return Object.entries(tableCounts).map(([name, counts]) => ({ name, ...counts })).sort((a, b) => b.hunches + b.blindspots - (a.hunches + a.blindspots));
30492
+ });
30020
30493
  }
30021
30494
 
30022
30495
  // ../server/dist/routes/data.js
@@ -30105,6 +30578,257 @@ async function dataRoutes(fastify, opts) {
30105
30578
  });
30106
30579
  }
30107
30580
 
30581
+ // ../server/dist/routes/admin-config.js
30582
+ function redactUrls(msg) {
30583
+ return msg.replace(/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, "[url redacted]");
30584
+ }
30585
+ function maskSecrets(secrets) {
30586
+ if (!secrets)
30587
+ return void 0;
30588
+ const out = {};
30589
+ for (const [k, v] of Object.entries(secrets))
30590
+ if (typeof v === "string")
30591
+ out[k] = maskSecret(v);
30592
+ return out;
30593
+ }
30594
+ function maskProviderKey(p) {
30595
+ if (p && typeof p.api_key === "string")
30596
+ return { ...p, api_key: maskSecret(p.api_key) };
30597
+ return p;
30598
+ }
30599
+ function maskAiConfig(ai) {
30600
+ if (!ai)
30601
+ return null;
30602
+ const providers = ai.providers ? Object.fromEntries(Object.entries(ai.providers).map(([n, p]) => [n, maskProviderKey(p)])) : void 0;
30603
+ return { ...ai, providers, embedding: maskProviderKey(ai.embedding) };
30604
+ }
30605
+ function mergeAiSecrets(incoming, stored) {
30606
+ const keep = (inc, st) => {
30607
+ if (!inc)
30608
+ return inc;
30609
+ if (inc.api_key === void 0 || inc.api_key === "" || inc.api_key.startsWith("\u2022\u2022\u2022\u2022")) {
30610
+ return { ...inc, api_key: st?.api_key };
30611
+ }
30612
+ return inc;
30613
+ };
30614
+ const providers = incoming.providers ? Object.fromEntries(Object.entries(incoming.providers).map(([n, p]) => [n, keep(p, stored?.providers?.[n])])) : void 0;
30615
+ return { ...incoming, providers, embedding: keep(incoming.embedding, stored?.embedding) };
30616
+ }
30617
+ async function adminConfigRoutes(fastify, opts) {
30618
+ const guard = (req, reply) => {
30619
+ if (!opts.canAdmin(req)) {
30620
+ reply.code(403).send({ error: "Requires admin role." });
30621
+ return false;
30622
+ }
30623
+ return true;
30624
+ };
30625
+ fastify.get("/api/admin/config", async (req, reply) => {
30626
+ if (!guard(req, reply))
30627
+ return;
30628
+ const runtime = opts.runtimeStore.read();
30629
+ const runtimeByName = new Map((runtime.connections ?? []).map((c) => [c.name, c]));
30630
+ const git = opts.gitConnections();
30631
+ const names = /* @__PURE__ */ new Set([...git.map((g) => g.name), ...runtimeByName.keys()]);
30632
+ const connections = [...names].map((name) => {
30633
+ const rt = runtimeByName.get(name);
30634
+ if (rt) {
30635
+ return { name, type: rt.type, source: "runtime", config: rt.config, secrets: maskSecrets(rt.secrets) };
30636
+ }
30637
+ const g = git.find((x) => x.name === name);
30638
+ return { name, type: g.type, source: "git" };
30639
+ });
30640
+ return {
30641
+ connections,
30642
+ knowledge: { configured: opts.knowledgeDbConfigured, urlSet: !!runtime.knowledge?.database_url },
30643
+ ai: maskAiConfig(opts.aiConfig)
30644
+ };
30645
+ });
30646
+ fastify.post("/api/admin/restart", async (req, reply) => {
30647
+ if (!guard(req, reply))
30648
+ return;
30649
+ return opts.requestRestart();
30650
+ });
30651
+ const mergeStoredSecrets = (conn, cfg) => {
30652
+ const existing = cfg.connections?.find((c) => c.name === conn.name);
30653
+ const incoming = Object.fromEntries(
30654
+ // Drop blank AND masked-sentinel ("••••…") values so neither wipes nor overwrites the
30655
+ // stored secret (the existing value is kept via the spread below).
30656
+ Object.entries(conn.secrets ?? {}).filter(([, v]) => typeof v === "string" && v !== "" && !v.startsWith("\u2022\u2022\u2022\u2022"))
30657
+ );
30658
+ const merged = { ...existing?.secrets ?? {}, ...incoming };
30659
+ const secrets = Object.keys(merged).length > 0 ? merged : void 0;
30660
+ return { ...conn, secrets };
30661
+ };
30662
+ const upsertConnection = (conn) => {
30663
+ const cfg = opts.runtimeStore.read();
30664
+ const merged = mergeStoredSecrets(conn, cfg);
30665
+ const list = (cfg.connections ?? []).filter((c) => c.name !== merged.name);
30666
+ list.push(merged);
30667
+ opts.runtimeStore.write({ ...cfg, connections: list });
30668
+ };
30669
+ const createOrUpdate = async (req, reply) => {
30670
+ if (!guard(req, reply))
30671
+ return;
30672
+ const parsed = RuntimeConnectionSchema.safeParse(req.body);
30673
+ if (!parsed.success) {
30674
+ reply.code(400).send({ error: "Invalid connection", details: parsed.error.flatten() });
30675
+ return;
30676
+ }
30677
+ upsertConnection(parsed.data);
30678
+ return opts.requestRestart();
30679
+ };
30680
+ fastify.post("/api/admin/connections", createOrUpdate);
30681
+ fastify.put("/api/admin/connections/:name", async (req, reply) => {
30682
+ if (!guard(req, reply))
30683
+ return;
30684
+ const parsed = RuntimeConnectionSchema.safeParse(req.body);
30685
+ if (!parsed.success) {
30686
+ reply.code(400).send({ error: "Invalid connection", details: parsed.error.flatten() });
30687
+ return;
30688
+ }
30689
+ const { name } = req.params;
30690
+ if (parsed.data.name !== name) {
30691
+ reply.code(400).send({ error: `Connection name mismatch: URL "${name}" vs body "${parsed.data.name}". Renaming is not supported.` });
30692
+ return;
30693
+ }
30694
+ upsertConnection(parsed.data);
30695
+ return opts.requestRestart();
30696
+ });
30697
+ fastify.delete("/api/admin/connections/:name", async (req, reply) => {
30698
+ if (!guard(req, reply))
30699
+ return;
30700
+ const { name } = req.params;
30701
+ const refs = opts.findConnectionRefs(name);
30702
+ if (refs.length > 0) {
30703
+ reply.code(409).send({ error: "Connection is in use", references: refs });
30704
+ return;
30705
+ }
30706
+ const cfg = opts.runtimeStore.read();
30707
+ opts.runtimeStore.write({ ...cfg, connections: (cfg.connections ?? []).filter((c) => c.name !== name) });
30708
+ return opts.requestRestart();
30709
+ });
30710
+ fastify.post("/api/admin/connections/test", async (req, reply) => {
30711
+ if (!guard(req, reply))
30712
+ return;
30713
+ const parsed = RuntimeConnectionSchema.safeParse(req.body);
30714
+ if (!parsed.success) {
30715
+ reply.code(400).send({ error: "Invalid connection", details: parsed.error.flatten() });
30716
+ return;
30717
+ }
30718
+ const cfg = opts.runtimeStore.read();
30719
+ const merged = mergeStoredSecrets(parsed.data, cfg);
30720
+ const { secrets, ...connLike } = merged;
30721
+ return testConnection(connLike, { projectDir: opts.projectDir, secrets });
30722
+ });
30723
+ fastify.post("/api/admin/knowledge-db/test", async (req, reply) => {
30724
+ if (!guard(req, reply))
30725
+ return;
30726
+ const url = req.body?.url;
30727
+ if (!url || url.trim() === "") {
30728
+ reply.code(400).send({ error: "A database URL is required." });
30729
+ return;
30730
+ }
30731
+ const target = opts.createKnowledgeStore(url);
30732
+ try {
30733
+ await target.init();
30734
+ return { ok: true };
30735
+ } catch (err) {
30736
+ return { ok: false, error: redactUrls(err instanceof Error ? err.message : "Connection failed") };
30737
+ } finally {
30738
+ try {
30739
+ await target.close();
30740
+ } catch {
30741
+ }
30742
+ }
30743
+ });
30744
+ fastify.put("/api/admin/knowledge-db", async (req, reply) => {
30745
+ if (!guard(req, reply))
30746
+ return;
30747
+ const url = req.body?.url;
30748
+ if (!url || url.trim() === "") {
30749
+ reply.code(400).send({ error: "A non-empty database URL is required." });
30750
+ return;
30751
+ }
30752
+ const cfg = opts.runtimeStore.read();
30753
+ opts.runtimeStore.write({ ...cfg, knowledge: { database_url: url } });
30754
+ return opts.requestRestart();
30755
+ });
30756
+ fastify.post("/api/admin/knowledge-db/migrate", async (req, reply) => {
30757
+ if (!guard(req, reply))
30758
+ return;
30759
+ const url = req.body?.url;
30760
+ if (!url) {
30761
+ reply.code(400).send({ error: "A target database URL is required." });
30762
+ return;
30763
+ }
30764
+ if (!opts.knowledgeStore) {
30765
+ reply.code(503).send({ error: "No current knowledge database to copy from." });
30766
+ return;
30767
+ }
30768
+ const target = opts.createKnowledgeStore(url);
30769
+ try {
30770
+ await target.init();
30771
+ const snapshot = await opts.knowledgeStore.exportAll();
30772
+ await target.importAll(snapshot);
30773
+ return {
30774
+ migrated: {
30775
+ hunches: snapshot.hunches.length,
30776
+ blindspots: snapshot.blindspots.length,
30777
+ knowledgeDocs: snapshot.knowledgeDocs.length,
30778
+ embeddings: snapshot.embeddings.length,
30779
+ profiles: snapshot.profiles.length
30780
+ }
30781
+ };
30782
+ } catch (err) {
30783
+ reply.code(500).send({ error: redactUrls(err instanceof Error ? err.message : "Migration failed") });
30784
+ } finally {
30785
+ try {
30786
+ await target.close();
30787
+ } catch {
30788
+ }
30789
+ }
30790
+ });
30791
+ fastify.put("/api/admin/ai", async (req, reply) => {
30792
+ if (!guard(req, reply))
30793
+ return;
30794
+ const parsed = AiConfigSchema.safeParse(req.body);
30795
+ if (!parsed.success) {
30796
+ reply.code(400).send({ error: "Invalid AI config", details: parsed.error.flatten() });
30797
+ return;
30798
+ }
30799
+ const cfg = opts.runtimeStore.read();
30800
+ const merged = mergeAiSecrets(parsed.data, cfg.ai);
30801
+ opts.runtimeStore.write({ ...cfg, ai: merged });
30802
+ return opts.requestRestart();
30803
+ });
30804
+ fastify.post("/api/admin/ai/test", async (req, reply) => {
30805
+ if (!guard(req, reply))
30806
+ return;
30807
+ const body = req.body;
30808
+ const parsed = ProviderConfigSchema.safeParse(body?.provider);
30809
+ if (!parsed.success) {
30810
+ reply.code(400).send({ error: "Invalid provider config", details: parsed.error.flatten() });
30811
+ return;
30812
+ }
30813
+ const kind = body?.kind === "embedding" ? "embedding" : "llm";
30814
+ return opts.testAiProvider(parsed.data, kind);
30815
+ });
30816
+ fastify.post("/api/admin/knowledge-db/reembed", async (req, reply) => {
30817
+ if (!guard(req, reply))
30818
+ return;
30819
+ if (!opts.knowledgeStore || !opts.embedder) {
30820
+ reply.code(503).send({ error: "Reindex requires a knowledge database and a configured embedder." });
30821
+ return;
30822
+ }
30823
+ try {
30824
+ const embedded = await reindexEmbeddings({ store: opts.knowledgeStore, embedder: opts.embedder });
30825
+ return { embedded };
30826
+ } catch (err) {
30827
+ reply.code(500).send({ error: redactUrls(err instanceof Error ? err.message : "Reindex failed") });
30828
+ }
30829
+ });
30830
+ }
30831
+
30108
30832
  // ../server/dist/oauth/service.js
30109
30833
  import { randomUUID as randomUUID3, timingSafeEqual as timingSafeEqual2 } from "crypto";
30110
30834
 
@@ -30631,17 +31355,17 @@ data: ${data}
30631
31355
  }
30632
31356
 
30633
31357
  // ../server/dist/server.js
30634
- import { join as join11, dirname as dirname2 } from "path";
31358
+ import { join as join13, dirname as dirname4 } from "path";
30635
31359
  import { fileURLToPath } from "url";
30636
31360
  import { access as access2, readFile as readFile6 } from "fs/promises";
30637
31361
  import { watch as fsWatch } from "fs";
30638
- var __dirname = dirname2(fileURLToPath(import.meta.url));
30639
- var packageJsonPath = join11(__dirname, "..", "package.json");
31362
+ var __dirname = dirname4(fileURLToPath(import.meta.url));
31363
+ var packageJsonPath = join13(__dirname, "..", "package.json");
30640
31364
  var packageJson = JSON.parse(await readFile6(packageJsonPath, "utf-8"));
30641
31365
  var VERSION = packageJson.version;
30642
31366
  async function enrichRefsFromCatalog(projectDir, refs) {
30643
31367
  try {
30644
- const catalogPath = join11(projectDir, ".yamchart", "catalog.json");
31368
+ const catalogPath = join13(projectDir, ".yamchart", "catalog.json");
30645
31369
  const catalogData = JSON.parse(await readFile6(catalogPath, "utf-8"));
30646
31370
  for (const model of catalogData.models ?? []) {
30647
31371
  if (model.name && model.table) {
@@ -30652,7 +31376,7 @@ async function enrichRefsFromCatalog(projectDir, refs) {
30652
31376
  }
30653
31377
  }
30654
31378
  async function createServer(options) {
30655
- const { projectDir, port = 3001, host = "0.0.0.0", watch: watch2 = false, serveStatic = process.env.NODE_ENV === "production", staticDir = join11(__dirname, "public"), auth } = options;
31379
+ const { projectDir, port = 3001, host = "0.0.0.0", watch: watch2 = false, serveStatic = process.env.NODE_ENV === "production", staticDir = join13(__dirname, "public"), auth } = options;
30656
31380
  if (auth) {
30657
31381
  initAuthServer(auth.supabaseUrl, auth.supabaseServiceKey);
30658
31382
  }
@@ -30682,12 +31406,35 @@ async function createServer(options) {
30682
31406
  }
30683
31407
  const configLoader = new ConfigLoader(projectDir, options.env);
30684
31408
  await configLoader.load();
31409
+ const runtimeAuthDbPath = options.localAuth?.dbPath;
31410
+ const runtimeConfigPath = RuntimeConfigStore.resolvePath({
31411
+ env: process.env,
31412
+ projectDir,
31413
+ authDbPath: runtimeAuthDbPath
31414
+ });
31415
+ const configKey = getConfigKey(dirname4(runtimeConfigPath), process.env, (m) => fastify.log.warn(m));
31416
+ const runtimeStore = new RuntimeConfigStore(runtimeConfigPath, configKey);
31417
+ const applyRuntime = () => {
31418
+ try {
31419
+ configLoader.applyRuntimeOverrides(runtimeStore.read());
31420
+ } catch (err) {
31421
+ if (err instanceof RuntimeConfigError) {
31422
+ fastify.log.error(`Runtime config ignored (Git-only fallback): ${err.message}`);
31423
+ } else
31424
+ throw err;
31425
+ }
31426
+ };
31427
+ applyRuntime();
31428
+ const effectiveAi = configLoader.getAiConfig();
30685
31429
  const gitService = new GitService(projectDir);
30686
31430
  const defaultConnection = configLoader.getDefaultConnection();
30687
31431
  if (!defaultConnection) {
30688
31432
  throw new Error("No connection configured");
30689
31433
  }
30690
- const rawConnector = createConnectorFromConfig(defaultConnection, { projectDir });
31434
+ const rawConnector = createConnectorFromConfig(defaultConnection, {
31435
+ projectDir,
31436
+ secrets: configLoader.getConnectionSecrets(defaultConnection.name)
31437
+ });
30691
31438
  const connector = new ReconnectingConnector(rawConnector);
30692
31439
  await connector.connect();
30693
31440
  const project = configLoader.getProject();
@@ -30697,7 +31444,7 @@ async function createServer(options) {
30697
31444
  defaultTtlMs: cacheTtl
30698
31445
  });
30699
31446
  const knowledgeUrlVar = project.knowledge?.database_url_var ?? "KNOWLEDGE_DATABASE_URL";
30700
- const knowledgeDbUrl = process.env[knowledgeUrlVar];
31447
+ const knowledgeDbUrl = configLoader.getKnowledgeDbUrl(knowledgeUrlVar, process.env);
30701
31448
  let knowledgeStore;
30702
31449
  if (knowledgeDbUrl) {
30703
31450
  const store = new PostgresKnowledgeStore(knowledgeDbUrl);
@@ -30751,20 +31498,6 @@ async function createServer(options) {
30751
31498
  authType: defaultConnection.type === "snowflake" && defaultConnection.auth ? defaultConnection.auth.type : void 0
30752
31499
  };
30753
31500
  });
30754
- fastify.post("/api/connections/reconnect", async (_request2, reply) => {
30755
- try {
30756
- try {
30757
- await connector.disconnect();
30758
- } catch {
30759
- }
30760
- await connector.connect();
30761
- return { success: true };
30762
- } catch (err) {
30763
- return reply.status(500).send({
30764
- error: err instanceof Error ? err.message : "Reconnection failed"
30765
- });
30766
- }
30767
- });
30768
31501
  await fastify.register(semanticRoutes, { semanticService, queryService, projectDir });
30769
31502
  if (authDb) {
30770
31503
  const secureCookies = process.env.NODE_ENV === "production";
@@ -30827,31 +31560,31 @@ async function createServer(options) {
30827
31560
  await fastify.register(async (protectedRoutes) => {
30828
31561
  protectedRoutes.addHook("preHandler", authMiddleware);
30829
31562
  protectedRoutes.addHook("preHandler", orgMiddleware);
30830
- await protectedRoutes.register(configRoutes, { configLoader, projectDir, queryService });
31563
+ await protectedRoutes.register(configRoutes, { configLoader, projectDir, queryService, getConnectionSecrets: (name) => configLoader.getConnectionSecrets(name) });
30831
31564
  await protectedRoutes.register(chartRoutes, { configLoader, queryService });
30832
31565
  await protectedRoutes.register(chartEventsRoutes, { configLoader, projectDir });
30833
31566
  await protectedRoutes.register(dashboardRoutes, { configLoader, gitService, projectDir, queryService });
30834
- await protectedRoutes.register(chatRoutes, { configLoader, queryService, projectDir });
31567
+ await protectedRoutes.register(chatRoutes, { configLoader, queryService, projectDir, aiConfig: effectiveAi });
30835
31568
  await protectedRoutes.register(catalogRoutes, { configLoader, projectDir });
30836
31569
  });
30837
31570
  } else if (authDb) {
30838
31571
  const localMiddleware = createLocalAuthMiddleware(authDb);
30839
31572
  await fastify.register(async (protectedRoutes) => {
30840
31573
  protectedRoutes.addHook("preHandler", localMiddleware);
30841
- await protectedRoutes.register(configRoutes, { configLoader, projectDir, queryService });
31574
+ await protectedRoutes.register(configRoutes, { configLoader, projectDir, queryService, getConnectionSecrets: (name) => configLoader.getConnectionSecrets(name) });
30842
31575
  await protectedRoutes.register(chartRoutes, { configLoader, queryService });
30843
31576
  await protectedRoutes.register(chartEventsRoutes, { configLoader, projectDir });
30844
31577
  await protectedRoutes.register(dashboardRoutes, { configLoader, gitService, projectDir, queryService });
30845
- await protectedRoutes.register(chatRoutes, { configLoader, queryService, projectDir });
31578
+ await protectedRoutes.register(chatRoutes, { configLoader, queryService, projectDir, aiConfig: effectiveAi });
30846
31579
  await protectedRoutes.register(catalogRoutes, { configLoader, projectDir });
30847
31580
  await protectedRoutes.register(sharesRoutes, { authDb });
30848
31581
  });
30849
31582
  } else {
30850
- await fastify.register(configRoutes, { configLoader, projectDir, queryService });
31583
+ await fastify.register(configRoutes, { configLoader, projectDir, queryService, getConnectionSecrets: (name) => configLoader.getConnectionSecrets(name) });
30851
31584
  await fastify.register(chartRoutes, { configLoader, queryService });
30852
31585
  await fastify.register(chartEventsRoutes, { configLoader, projectDir });
30853
31586
  await fastify.register(dashboardRoutes, { configLoader, gitService, projectDir, queryService });
30854
- await fastify.register(chatRoutes, { configLoader, queryService, projectDir });
31587
+ await fastify.register(chatRoutes, { configLoader, queryService, projectDir, aiConfig: effectiveAi });
30855
31588
  await fastify.register(catalogRoutes, { configLoader, projectDir });
30856
31589
  }
30857
31590
  const mcpBaseUrl = process.env.BASE_URL || project.defaults?.base_url || `http://localhost:${port}`;
@@ -30868,12 +31601,12 @@ async function createServer(options) {
30868
31601
  let embedder;
30869
31602
  let answerProvider;
30870
31603
  try {
30871
- embedder = createEmbedder(project.ai?.embedding, process.env);
31604
+ embedder = createEmbedder(effectiveAi?.embedding, process.env);
30872
31605
  } catch (err) {
30873
31606
  fastify.log.warn({ err }, "Failed to build embedder; answer_question tool disabled");
30874
31607
  }
30875
31608
  try {
30876
- answerProvider = createProviderForTask(project.ai, "answer", process.env);
31609
+ answerProvider = createProviderForTask(effectiveAi, "answer", process.env);
30877
31610
  } catch (err) {
30878
31611
  fastify.log.warn({ err }, "Failed to build answer provider; answer_question tool disabled");
30879
31612
  }
@@ -30906,7 +31639,7 @@ async function createServer(options) {
30906
31639
  dialect: defaultConnection?.type ?? "duckdb",
30907
31640
  metricNames: semanticLayer?.listMetrics().map((m) => m.name) ?? [],
30908
31641
  getConnectionTables: async (connection) => {
30909
- const catalogPath = join11(projectDir, ".yamchart", "catalog.json");
31642
+ const catalogPath = join13(projectDir, ".yamchart", "catalog.json");
30910
31643
  try {
30911
31644
  const catalogData = JSON.parse(await readFile6(catalogPath, "utf-8"));
30912
31645
  return (catalogData.models ?? []).filter((m) => !m.connection || m.connection === connection).map((m) => ({
@@ -30925,7 +31658,11 @@ async function createServer(options) {
30925
31658
  return true;
30926
31659
  return user.role !== "viewer";
30927
31660
  },
30928
- aiConfig: project.ai
31661
+ aiConfig: effectiveAi,
31662
+ getConnectionThreshold: (name) => {
31663
+ const conn = configLoader.getConnections().find((c) => c.name === name);
31664
+ return conn?.hunch_confidence_threshold;
31665
+ }
30929
31666
  };
30930
31667
  const dataOptions = {
30931
31668
  store: knowledgeStore,
@@ -30933,12 +31670,76 @@ async function createServer(options) {
30933
31670
  getConnectionTables: learningOptions.getConnectionTables,
30934
31671
  canWrite: learningOptions.canWrite
30935
31672
  };
31673
+ const adminConfigOptions = {
31674
+ runtimeStore,
31675
+ gitConnections: () => configLoader.getConnections().map((c) => ({ name: c.name, type: c.type })),
31676
+ knowledgeDbConfigured: !!knowledgeStore,
31677
+ aiConfig: effectiveAi,
31678
+ projectDir,
31679
+ findConnectionRefs: (name) => findConnectionRefs(name, {
31680
+ charts: configLoader.getCharts().map((c) => ({
31681
+ name: c.name,
31682
+ source: { connection: c.source.connection }
31683
+ })),
31684
+ models: configLoader.getModels().map((m) => ({
31685
+ metadata: {
31686
+ name: m.metadata.name,
31687
+ connection: m.metadata.connection
31688
+ }
31689
+ })),
31690
+ defaultConnection: configLoader.getDefaultConnection()?.name
31691
+ }),
31692
+ requestRestart: () => requestRestart(process.env),
31693
+ // Admin-only: when auth disabled (no req.user) allow (dev); else require admin role.
31694
+ canAdmin: (req) => {
31695
+ const user = req.user;
31696
+ if (!user)
31697
+ return true;
31698
+ return user.role === "admin";
31699
+ },
31700
+ knowledgeStore,
31701
+ embedder,
31702
+ createKnowledgeStore: (url) => new PostgresKnowledgeStore(url),
31703
+ testAiProvider: async (cfg, kind) => {
31704
+ try {
31705
+ if (kind === "embedding") {
31706
+ const e = createEmbedder(cfg, process.env);
31707
+ if (!e)
31708
+ return { ok: false, error: "Could not build embedder for this config." };
31709
+ await e.embed(["ping"]);
31710
+ } else {
31711
+ const provider = buildProvider(cfg, process.env);
31712
+ await provider.chat({ system: "", messages: [{ role: "user", content: "ping" }], tools: [], maxTokens: 1 });
31713
+ }
31714
+ return { ok: true };
31715
+ } catch (err) {
31716
+ const msg = err instanceof Error ? err.message : "Test failed";
31717
+ return { ok: false, error: redactProviderError(msg, cfg.api_key) };
31718
+ }
31719
+ }
31720
+ };
31721
+ const registerReconnect = async (scope) => {
31722
+ scope.post("/api/connections/reconnect", async (_request2, reply) => {
31723
+ try {
31724
+ try {
31725
+ await connector.disconnect();
31726
+ } catch {
31727
+ }
31728
+ await connector.connect();
31729
+ return { success: true };
31730
+ } catch (err) {
31731
+ return reply.status(500).send({ error: err instanceof Error ? err.message : "Reconnection failed" });
31732
+ }
31733
+ });
31734
+ };
30936
31735
  if (auth) {
30937
31736
  await fastify.register(async (protectedRoutes) => {
30938
31737
  protectedRoutes.addHook("preHandler", authMiddleware);
30939
31738
  protectedRoutes.addHook("preHandler", orgMiddleware);
30940
31739
  await protectedRoutes.register(learningRoutes, learningOptions);
30941
31740
  await protectedRoutes.register(dataRoutes, dataOptions);
31741
+ await protectedRoutes.register(adminConfigRoutes, adminConfigOptions);
31742
+ await registerReconnect(protectedRoutes);
30942
31743
  });
30943
31744
  } else if (authDb) {
30944
31745
  const localMiddleware = createLocalAuthMiddleware(authDb);
@@ -30946,12 +31747,16 @@ async function createServer(options) {
30946
31747
  protectedRoutes.addHook("preHandler", localMiddleware);
30947
31748
  await protectedRoutes.register(learningRoutes, learningOptions);
30948
31749
  await protectedRoutes.register(dataRoutes, dataOptions);
31750
+ await protectedRoutes.register(adminConfigRoutes, adminConfigOptions);
31751
+ await registerReconnect(protectedRoutes);
30949
31752
  });
30950
31753
  } else {
30951
31754
  await fastify.register(learningRoutes, learningOptions);
30952
31755
  await fastify.register(dataRoutes, dataOptions);
31756
+ await fastify.register(adminConfigRoutes, adminConfigOptions);
31757
+ await registerReconnect(fastify);
30953
31758
  }
30954
- const assetsDir = join11(projectDir, "assets");
31759
+ const assetsDir = join13(projectDir, "assets");
30955
31760
  try {
30956
31761
  await access2(assetsDir);
30957
31762
  await fastify.register(fastifyStatic, {
@@ -30962,7 +31767,7 @@ async function createServer(options) {
30962
31767
  } catch {
30963
31768
  }
30964
31769
  if (project.theme?.customCss) {
30965
- const cssPath = join11(projectDir, project.theme.customCss);
31770
+ const cssPath = join13(projectDir, project.theme.customCss);
30966
31771
  fastify.get("/api/theme/custom.css", async (_request2, reply) => {
30967
31772
  try {
30968
31773
  const content = await readFile6(cssPath, "utf-8");
@@ -31009,6 +31814,7 @@ async function createServer(options) {
31009
31814
  configLoader.startWatching();
31010
31815
  configLoader.onChange(async () => {
31011
31816
  fastify.log.info("Config reloaded");
31817
+ applyRuntime();
31012
31818
  const { models: newModels, refs: newRefs } = await buildModelsAndRefs();
31013
31819
  queryService.updateCompiler({ models: newModels, refs: newRefs });
31014
31820
  queryService.invalidateAll();
@@ -31021,7 +31827,7 @@ async function createServer(options) {
31021
31827
  fastify.log.info(`Semantic model rebuilt: ${semanticService.getModel()?.entities.length ?? 0} entities`);
31022
31828
  broadcastConfigChange();
31023
31829
  });
31024
- const catalogPath = join11(projectDir, ".yamchart", "catalog.json");
31830
+ const catalogPath = join13(projectDir, ".yamchart", "catalog.json");
31025
31831
  try {
31026
31832
  catalogWatcher = fsWatch(catalogPath, () => {
31027
31833
  semanticService.buildFromDisk();
@@ -31085,15 +31891,15 @@ async function runDevServer(projectDir, options) {
31085
31891
  let usesBrowserAuth = false;
31086
31892
  let localAuth;
31087
31893
  try {
31088
- const { readFileSync: readFileSync3 } = await import("fs");
31894
+ const { readFileSync: readFileSync5 } = await import("fs");
31089
31895
  const { parse: parse2 } = await import("yaml");
31090
- const { resolveProjectConfig: resolveProjectConfig2, deepMerge: deepMerge3 } = await import("./dist-PPAD6KOM.js");
31091
- const raw = readFileSync3(resolve2(projectDir, "yamchart.yaml"), "utf-8");
31896
+ const { resolveProjectConfig: resolveProjectConfig2, deepMerge: deepMerge3 } = await import("./dist-WMP7BFKP.js");
31897
+ const raw = readFileSync5(resolve2(projectDir, "yamchart.yaml"), "utf-8");
31092
31898
  const rawConfig = parse2(raw);
31093
31899
  const resolvedEnv = options.env || process.env.YAMCHART_ENV || void 0;
31094
31900
  let projectConfig = resolveProjectConfig2(rawConfig, resolvedEnv);
31095
31901
  try {
31096
- const localRaw = readFileSync3(resolve2(projectDir, "yamchart.local.yaml"), "utf-8");
31902
+ const localRaw = readFileSync5(resolve2(projectDir, "yamchart.local.yaml"), "utf-8");
31097
31903
  const localOverrides = parse2(localRaw);
31098
31904
  if (localOverrides && typeof localOverrides === "object") {
31099
31905
  projectConfig = deepMerge3(projectConfig, localOverrides);
@@ -31111,7 +31917,7 @@ async function runDevServer(projectDir, options) {
31111
31917
  const targetConn = process.env.YAMCHART_CONNECTION || projectConfig?.defaults?.connection;
31112
31918
  if (targetConn) {
31113
31919
  try {
31114
- const connRaw = readFileSync3(resolve2(connectionDir, `${targetConn}.yaml`), "utf-8");
31920
+ const connRaw = readFileSync5(resolve2(connectionDir, `${targetConn}.yaml`), "utf-8");
31115
31921
  const connConfig = parse2(connRaw);
31116
31922
  if (connConfig?.auth?.type === "externalbrowser") {
31117
31923
  usesBrowserAuth = true;
@@ -31191,4 +31997,4 @@ async function runDevServer(projectDir, options) {
31191
31997
  export {
31192
31998
  runDevServer
31193
31999
  };
31194
- //# sourceMappingURL=dev-H6SJZ5UA.js.map
32000
+ //# sourceMappingURL=dev-KOWFGDOP.js.map