yamchart 0.10.1 → 0.10.3

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 (83) hide show
  1. package/dist/{advisor-5TJDNAY7.js → advisor-XTJQU7E4.js} +12 -12
  2. package/dist/{chunk-QUIDZO5G.js → chunk-2XJZJ6IA.js} +22 -6
  3. package/dist/chunk-2XJZJ6IA.js.map +1 -0
  4. package/dist/{chunk-UFDQ3C7Q.js → chunk-6ELSQNIH.js} +257 -257
  5. package/dist/chunk-6ELSQNIH.js.map +1 -0
  6. package/dist/{chunk-64CI3HSY.js → chunk-KDDO4UII.js} +70 -11
  7. package/dist/chunk-KDDO4UII.js.map +1 -0
  8. package/dist/{chunk-S2CH4HUZ.js → chunk-ODSPBOJW.js} +4 -4
  9. package/dist/chunk-OYBI433Z.js +1296 -0
  10. package/dist/chunk-OYBI433Z.js.map +1 -0
  11. package/dist/{chunk-ZBCQNWVN.js → chunk-PXFTWXSV.js} +5 -5
  12. package/dist/{chunk-5FHV22X2.js → chunk-XNMB25GT.js} +4 -4
  13. package/dist/{connection-utils-FEUWER5I.js → connection-utils-S7EFPTLY.js} +5 -5
  14. package/dist/{describe-MEP72B56.js → describe-TA2CTODC.js} +7 -7
  15. package/dist/{dev-H6SJZ5UA.js → dev-PHFYLJPJ.js} +2589 -1243
  16. package/dist/dev-PHFYLJPJ.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-PPAD6KOM.js → dist-F6TVLP6C.js} +34 -4
  20. package/dist/{dist-XNCED7JW.js → dist-Q5ETETUU.js} +4 -4
  21. package/dist/{dist-5DQO6L2S.js → dist-YJOOH65Y.js} +9 -5
  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-V-E6oxy6.js +9 -0
  27. package/dist/public/assets/{EventManagement-BnmeJDQl.js → EventManagement-Do2xU5lV.js} +2 -2
  28. package/dist/public/assets/ExplorePage-Dg50mvGB.js +1 -0
  29. package/dist/public/assets/{LoginPage-CzaFkkjg.js → LoginPage-DmYGIXcu.js} +1 -1
  30. package/dist/public/assets/{PublicViewer-irjxqH6a.js → PublicViewer-CJTgc9OF.js} +1 -1
  31. package/dist/public/assets/{SetupWizard-ConWIcmy.js → SetupWizard-Bo-zmh43.js} +1 -1
  32. package/dist/public/assets/{ShareManagement-CP4wdwLR.js → ShareManagement-6Wp6b2Gp.js} +1 -1
  33. package/dist/public/assets/{SourceDetailView-DZS5518E.js → SourceDetailView-CTzSXhZc.js} +1 -1
  34. package/dist/public/assets/UserManagement-DQhB13T0.js +1 -0
  35. package/dist/public/assets/{index-jlfTO7f5.js → index-CY8xsfQH.js} +41 -41
  36. package/dist/public/assets/{index-C1X8RW4Z.css → index-Dkn-tJCT.css} +1 -1
  37. package/dist/public/assets/{index.es-CgnvEWi5.js → index.es-Bc3yys5L.js} +1 -1
  38. package/dist/public/assets/{jspdf.es.min-Cw5WefMt.js → jspdf.es.min-D94JEH-J.js} +3 -3
  39. package/dist/public/index.html +2 -2
  40. package/dist/{query-2H3YOPI2.js → query-AX4BOPMH.js} +5 -5
  41. package/dist/sample-4NUBJGIL.js +76 -0
  42. package/dist/sample-4NUBJGIL.js.map +1 -0
  43. package/dist/{search-IPE4ISFB.js → search-IBVVYN4I.js} +7 -7
  44. package/dist/{semantic-K3MYXXJI.js → semantic-33YIUMUV.js} +39 -4
  45. package/dist/semantic-33YIUMUV.js.map +1 -0
  46. package/dist/semantic-5WGVZ7HY.js +30 -0
  47. package/dist/{source-resolver-HZQLOODU.js → source-resolver-GYXIRIVK.js} +7 -7
  48. package/dist/source-resolver-GYXIRIVK.js.map +1 -0
  49. package/dist/{sync-warehouse-TUNULDUY.js → sync-warehouse-5NKENCOZ.js} +15 -11
  50. package/dist/sync-warehouse-5NKENCOZ.js.map +1 -0
  51. package/dist/{tables-K5NAN2WK.js → tables-QMZMYYV5.js} +7 -7
  52. package/dist/templates/default/docs/yamchart-reference.md +26 -0
  53. package/dist/{test-SRHVOXZB.js → test-6MCPGXJP.js} +19 -7
  54. package/dist/test-6MCPGXJP.js.map +1 -0
  55. package/package.json +2 -2
  56. package/dist/chunk-64CI3HSY.js.map +0 -1
  57. package/dist/chunk-QUIDZO5G.js.map +0 -1
  58. package/dist/chunk-UFDQ3C7Q.js.map +0 -1
  59. package/dist/chunk-ZIY22VO7.js +0 -1190
  60. package/dist/chunk-ZIY22VO7.js.map +0 -1
  61. package/dist/dev-H6SJZ5UA.js.map +0 -1
  62. package/dist/init-SECXKD5G.js.map +0 -1
  63. package/dist/public/assets/DataView-DUCz_96y.js +0 -9
  64. package/dist/public/assets/ExplorePage-kk4z9ldZ.js +0 -1
  65. package/dist/public/assets/UserManagement-AubGd9hl.js +0 -1
  66. package/dist/sample-ODUGGSFA.js +0 -42
  67. package/dist/sample-ODUGGSFA.js.map +0 -1
  68. package/dist/semantic-K3MYXXJI.js.map +0 -1
  69. package/dist/sync-warehouse-TUNULDUY.js.map +0 -1
  70. package/dist/test-SRHVOXZB.js.map +0 -1
  71. /package/dist/{advisor-5TJDNAY7.js.map → advisor-XTJQU7E4.js.map} +0 -0
  72. /package/dist/{chunk-S2CH4HUZ.js.map → chunk-ODSPBOJW.js.map} +0 -0
  73. /package/dist/{chunk-ZBCQNWVN.js.map → chunk-PXFTWXSV.js.map} +0 -0
  74. /package/dist/{chunk-5FHV22X2.js.map → chunk-XNMB25GT.js.map} +0 -0
  75. /package/dist/{connection-utils-FEUWER5I.js.map → connection-utils-S7EFPTLY.js.map} +0 -0
  76. /package/dist/{describe-MEP72B56.js.map → describe-TA2CTODC.js.map} +0 -0
  77. /package/dist/{dist-5DQO6L2S.js.map → dist-F6TVLP6C.js.map} +0 -0
  78. /package/dist/{dist-PPAD6KOM.js.map → dist-Q5ETETUU.js.map} +0 -0
  79. /package/dist/{dist-XNCED7JW.js.map → dist-YJOOH65Y.js.map} +0 -0
  80. /package/dist/{query-2H3YOPI2.js.map → query-AX4BOPMH.js.map} +0 -0
  81. /package/dist/{search-IPE4ISFB.js.map → search-IBVVYN4I.js.map} +0 -0
  82. /package/dist/{source-resolver-HZQLOODU.js.map → semantic-5WGVZ7HY.js.map} +0 -0
  83. /package/dist/{tables-K5NAN2WK.js.map → tables-QMZMYYV5.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-ODSPBOJW.js";
6
6
  import {
7
7
  box,
8
8
  detail,
@@ -18,9 +18,16 @@ import {
18
18
  filterCatalogEntries
19
19
  } from "./chunk-ZMJPRNOA.js";
20
20
  import {
21
+ buildModelPath,
22
+ buildProvider,
21
23
  createEmbedder,
22
- createProviderForTask
23
- } from "./chunk-64CI3HSY.js";
24
+ createProviderForTask,
25
+ detectConventions,
26
+ formatModelSql,
27
+ scaffoldModelSql,
28
+ updateSchemaYml,
29
+ writeDbtModel
30
+ } from "./chunk-KDDO4UII.js";
24
31
  import {
25
32
  BigQueryConnector,
26
33
  DuckDBConnector,
@@ -39,7 +46,7 @@ import {
39
46
  resolveMySQLAuth,
40
47
  resolvePostgresAuth,
41
48
  resolveSnowflakeAuth
42
- } from "./chunk-QUIDZO5G.js";
49
+ } from "./chunk-2XJZJ6IA.js";
43
50
  import {
44
51
  SemanticModelBuilder,
45
52
  SemanticQueryCompiler,
@@ -56,8 +63,9 @@ import {
56
63
  isRelativeDateRange,
57
64
  loadSemanticDefinitions,
58
65
  validateSemanticQuery
59
- } from "./chunk-UFDQ3C7Q.js";
66
+ } from "./chunk-6ELSQNIH.js";
60
67
  import {
68
+ AiConfigSchema,
61
69
  ChartSchema,
62
70
  ConnectionSchema,
63
71
  DashboardSchema,
@@ -65,11 +73,14 @@ import {
65
73
  EventsFileSchema,
66
74
  NewBlindspotSchema,
67
75
  ProjectSchema,
76
+ ProviderConfigSchema,
77
+ RuntimeConfigSchema,
78
+ RuntimeConnectionSchema,
68
79
  ScheduleSchema,
69
80
  SemanticQuerySchema,
70
81
  deepMerge,
71
82
  resolveProjectConfig
72
- } from "./chunk-ZIY22VO7.js";
83
+ } from "./chunk-OYBI433Z.js";
73
84
  import {
74
85
  AuthDatabase,
75
86
  generateOpaqueToken,
@@ -3035,7 +3046,7 @@ var require_compile = __commonJS({
3035
3046
  const schOrFunc = root.refs[ref];
3036
3047
  if (schOrFunc)
3037
3048
  return schOrFunc;
3038
- let _sch = resolve3.call(this, root, ref);
3049
+ let _sch = resolve4.call(this, root, ref);
3039
3050
  if (_sch === void 0) {
3040
3051
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
3041
3052
  const { schemaId } = this.opts;
@@ -3062,7 +3073,7 @@ var require_compile = __commonJS({
3062
3073
  function sameSchemaEnv(s1, s2) {
3063
3074
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3064
3075
  }
3065
- function resolve3(root, ref) {
3076
+ function resolve4(root, ref) {
3066
3077
  let sch;
3067
3078
  while (typeof (sch = this.refs[ref]) == "string")
3068
3079
  ref = sch;
@@ -3637,7 +3648,7 @@ var require_fast_uri = __commonJS({
3637
3648
  }
3638
3649
  return uri;
3639
3650
  }
3640
- function resolve3(baseURI, relativeURI, options) {
3651
+ function resolve4(baseURI, relativeURI, options) {
3641
3652
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3642
3653
  const resolved = resolveComponent(parse2(baseURI, schemelessOptions), parse2(relativeURI, schemelessOptions), schemelessOptions, true);
3643
3654
  schemelessOptions.skipEscape = true;
@@ -3864,7 +3875,7 @@ var require_fast_uri = __commonJS({
3864
3875
  var fastUri = {
3865
3876
  SCHEMES,
3866
3877
  normalize,
3867
- resolve: resolve3,
3878
+ resolve: resolve4,
3868
3879
  resolveComponent,
3869
3880
  equal,
3870
3881
  serialize,
@@ -6845,7 +6856,7 @@ var require_dist = __commonJS({
6845
6856
  });
6846
6857
 
6847
6858
  // src/commands/dev.ts
6848
- import { resolve as resolve2 } from "path";
6859
+ import { resolve as resolve3 } from "path";
6849
6860
  import { homedir } from "os";
6850
6861
 
6851
6862
  // ../server/dist/server.js
@@ -6853,1126 +6864,1309 @@ import Fastify from "fastify";
6853
6864
  import cors from "@fastify/cors";
6854
6865
  import fastifyStatic from "@fastify/static";
6855
6866
 
6856
- // ../server/dist/services/config-loader.js
6857
- import { readFile, readdir, access } from "fs/promises";
6858
- import { join, extname, relative, dirname } from "path";
6859
- import { parse as parseYaml } from "yaml";
6860
- import { watch } from "chokidar";
6861
-
6862
- // ../../packages/chat/dist/registry.js
6863
- import { parse } from "yaml";
6864
-
6865
- // ../../packages/chat/dist/agents/dashboard-analyst.js
6866
- var DASHBOARD_ANALYST = {
6867
- name: "dashboard-analyst",
6868
- title: "Dashboard Analyst",
6869
- description: "Summarizes dashboards, explains trends, answers data questions, and can apply filters to the dashboard.",
6870
- systemPrompt: `You are an AI dashboard analyst for a business intelligence platform.
6871
-
6872
- Your job is to help users understand their data by:
6873
- - Summarizing what a dashboard or chart shows
6874
- - Explaining trends, outliers, and patterns in the data
6875
- - Answering questions about metrics and KPIs
6876
- - Suggesting and applying filters to explore data
6877
- - Running ad-hoc SQL to explore data beyond existing charts
6878
-
6879
- ## Rules
6880
- - Be concise. Dashboard users want quick answers, not essays.
6881
- - Use numbers and percentages when discussing data.
6882
- - When you query data, summarize the key findings \u2014 don't dump raw rows.
6883
- - If asked about a trend, explain the direction, magnitude, and possible causes.
6884
- - When suggesting filters, explain what you expect to see.
6885
- - Use the apply_filter tool when the user asks to filter or drill down.
6886
- - If the existing charts don't have the data needed, use run_sql to query the database directly.
6887
- - Use call_agent to delegate to specialized agents when you need expertise you don't have.
6888
- - Use suggest_actions to offer follow-up exploration or save options after answering.
6889
- - If you don't have enough data to answer, say so and suggest what to query.
6890
-
6891
- ## Data Visualization in Responses
6892
- When you have tabular data to share, use these markdown code fences:
6893
-
6894
- For charts (line, bar, pie, scatter):
6895
- \`\`\`chart
6896
- {"type":"line","data":[{"date":"2025-01-01","value":100},...],"x":"date","y":"value","title":"Optional Title"}
6897
- \`\`\`
6898
-
6899
- For data tables:
6900
- \`\`\`data
6901
- {"columns":[{"name":"date","type":"date"},{"name":"value","type":"number"}],"rows":[{"date":"2025-01-01","value":100},...]}
6902
- \`\`\`
6903
-
6904
- Choose charts for trends and comparisons, tables for detailed breakdowns. Keep data concise \u2014 summarize large result sets.`,
6905
- tools: ["query_model", "get_chart_config", "get_dashboard_summary", "apply_filter", "compare_periods", "run_sql", "call_agent", "get_catalog_tables", "suggest_actions", "save_model", "save_chart", "add_to_dashboard"],
6906
- builtIn: true
6907
- };
6908
-
6909
- // ../../packages/chat/dist/agents/sql-explorer.js
6910
- var SQL_EXPLORER = {
6911
- name: "sql-explorer",
6912
- title: "SQL Explorer",
6913
- description: "Runs ad-hoc SQL queries against the connected database for deep data exploration and period comparisons.",
6914
- systemPrompt: `You are a SQL expert helping users explore their data warehouse.
6915
-
6916
- Your job is to:
6917
- - Write and execute SQL queries to answer user questions
6918
- - Compare data across time periods
6919
- - Find specific records or aggregations
6920
- - Explain query results clearly
6921
-
6922
- ## Rules
6923
- - Always LIMIT your queries (default: 50 rows) to avoid large payloads.
6924
- - Use clear column aliases so results are readable.
6925
- - When comparing periods, use the compare_periods tool for models, or write explicit SQL for ad-hoc comparisons.
6926
- - Explain what your query does before showing results.
6927
- - If a query fails, explain the error and try a corrected version.
6928
- - Never run destructive queries (INSERT, UPDATE, DELETE, DROP, etc.).
6929
-
6930
- ## Data Visualization in Responses
6931
- When you have tabular data to share, use these markdown code fences:
6932
-
6933
- For charts (line, bar, pie, scatter):
6934
- \`\`\`chart
6935
- {"type":"line","data":[{"date":"2025-01-01","value":100},...],"x":"date","y":"value","title":"Optional Title"}
6936
- \`\`\`
6937
-
6938
- For data tables:
6939
- \`\`\`data
6940
- {"columns":[{"name":"date","type":"date"},{"name":"value","type":"number"}],"rows":[{"date":"2025-01-01","value":100},...]}
6941
- \`\`\`
6942
-
6943
- Choose charts for trends and comparisons, tables for detailed breakdowns. Keep data concise \u2014 summarize large result sets.`,
6944
- tools: ["query_model", "run_sql", "compare_periods", "get_chart_config", "call_agent", "get_catalog_tables", "suggest_actions", "save_model", "save_chart"],
6945
- builtIn: true
6946
- };
6947
-
6948
- // ../../packages/chat/dist/agents/data-explorer.js
6949
- var DATA_EXPLORER = {
6950
- name: "data-explorer",
6951
- title: "Data Explorer",
6952
- description: "Open-ended data exploration, visualization, and dashboard building.",
6953
- systemPrompt: `You are an AI data explorer for a business intelligence platform.
6954
-
6955
- Your job is to help users explore their data warehouse, build visualizations, and create dashboards \u2014 starting from curiosity and ending with saved, reusable charts.
6956
-
6957
- ## Discovery First
6958
- - Always start with get_catalog_tables to understand what data is available before writing SQL.
6959
- - Prefer querying named models (query_model) over raw SQL when a model already exists.
6960
- - When no model exists, use run_sql to explore \u2014 then offer to save the query as a model.
6961
-
6962
- ## Rules
6963
- - All SQL must be read-only (SELECT only). Never run INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, or CREATE.
6964
- - Always LIMIT queries to 50 rows by default. Use LIMIT explicitly.
6965
- - Use clear column aliases so results are readable.
6966
- - When a query fails, explain the error and try a corrected version.
6967
- - Explain what you're querying and why before showing results.
6968
-
6969
- ## Proactive Visualization
6970
- After retrieving data, always visualize it \u2014 don't just dump raw rows. Choose the right form:
6971
- - Trends over time \u2192 line chart
6972
- - Comparisons across categories \u2192 bar chart
6973
- - Part-to-whole \u2192 pie chart
6974
- - Single key metric \u2192 show as bold text or KPI
6975
- - Detailed breakdowns \u2192 data table
6976
-
6977
- For charts (line, bar, pie, scatter):
6978
- \`\`\`chart
6979
- {"type":"line","data":[{"date":"2025-01-01","value":100},...],"x":"date","y":"value","title":"Optional Title"}
6980
- \`\`\`
6981
-
6982
- For data tables:
6983
- \`\`\`data
6984
- {"columns":[{"name":"date","type":"date"},{"name":"value","type":"number"}],"rows":[{"date":"2025-01-01","value":100},...]}
6985
- \`\`\`
6986
-
6987
- Keep visualizations concise \u2014 summarize large result sets rather than showing every row.
6988
-
6989
- ## Saving Work
6990
- When a user wants to keep a query or chart:
6991
- 1. Use save_model to create a reusable SQL model file.
6992
- 2. Use save_chart to create a chart YAML referencing that model.
6993
- 3. Use create_dashboard to assemble saved charts into a new dashboard.
6994
- 4. After saving, use suggest_actions to offer navigation or further exploration.
6995
-
6996
- ## Suggesting Next Steps
6997
- Always use suggest_actions after answering a question to offer natural follow-up options:
6998
- - Explore related dimensions or time periods
6999
- - Save the current query as a model
7000
- - Create a chart from the data
7001
- - Add the chart to a dashboard
7002
-
7003
- ## Collaboration
7004
- Use call_agent to delegate to specialized agents when needed:
7005
- - Dashboard Analyst for questions about existing dashboards
7006
- - SQL Explorer for complex multi-step SQL investigations`,
7007
- tools: ["run_sql", "query_model", "get_catalog_tables", "compare_periods", "save_model", "save_chart", "create_dashboard", "suggest_actions", "call_agent"],
7008
- builtIn: true
7009
- };
7010
-
7011
- // ../../packages/chat/dist/registry.js
7012
- var AgentRegistry = class {
7013
- agents = /* @__PURE__ */ new Map();
7014
- constructor() {
7015
- this.agents.set(DASHBOARD_ANALYST.name, DASHBOARD_ANALYST);
7016
- this.agents.set(SQL_EXPLORER.name, SQL_EXPLORER);
7017
- this.agents.set(DATA_EXPLORER.name, DATA_EXPLORER);
6867
+ // ../../packages/engine/dist/cache.js
6868
+ import { LRUCache } from "lru-cache";
6869
+ var MemoryCache = class {
6870
+ cache;
6871
+ defaultTtlMs;
6872
+ hits = 0;
6873
+ misses = 0;
6874
+ constructor(options) {
6875
+ this.defaultTtlMs = options.defaultTtlMs;
6876
+ this.cache = new LRUCache({
6877
+ max: options.maxSize,
6878
+ ttl: options.defaultTtlMs,
6879
+ ttlAutopurge: true
6880
+ });
7018
6881
  }
7019
- getAgent(name) {
7020
- return this.agents.get(name);
6882
+ async get(key) {
6883
+ const value = this.cache.get(key);
6884
+ if (value) {
6885
+ this.hits++;
6886
+ return value;
6887
+ }
6888
+ this.misses++;
6889
+ return null;
7021
6890
  }
7022
- listAgents() {
7023
- return Array.from(this.agents.values());
6891
+ async set(key, value, ttlMs) {
6892
+ this.cache.set(key, value, { ttl: ttlMs ?? this.defaultTtlMs });
7024
6893
  }
7025
- registerAgent(config) {
7026
- this.agents.set(config.name, config);
6894
+ async invalidate(key) {
6895
+ this.cache.delete(key);
7027
6896
  }
7028
- loadFromYaml(yamlContent, filePath, onWarning) {
7029
- try {
7030
- const raw = parse(yamlContent);
7031
- if (!raw || typeof raw !== "object") {
7032
- onWarning?.(`Skipping ${filePath}: not a valid YAML object`);
7033
- return;
7034
- }
7035
- if (!raw.name || typeof raw.name !== "string") {
7036
- onWarning?.(`Skipping ${filePath}: missing required "name" field`);
7037
- return;
7038
- }
7039
- if (!raw.description || typeof raw.description !== "string") {
7040
- onWarning?.(`Skipping ${filePath}: missing required "description" field`);
7041
- return;
7042
- }
7043
- if (!raw.system_prompt || typeof raw.system_prompt !== "string") {
7044
- onWarning?.(`Skipping ${filePath}: missing required "system_prompt" field`);
7045
- return;
6897
+ async invalidatePattern(pattern) {
6898
+ const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
6899
+ const regex = new RegExp(`^${regexPattern}$`);
6900
+ for (const key of this.cache.keys()) {
6901
+ if (regex.test(key)) {
6902
+ this.cache.delete(key);
7046
6903
  }
7047
- const config = {
7048
- name: raw.name,
7049
- title: raw.title ?? raw.name,
7050
- description: raw.description,
7051
- systemPrompt: raw.system_prompt,
7052
- tools: Array.isArray(raw.tools) ? raw.tools.filter((t) => typeof t === "string") : [],
7053
- builtIn: false,
7054
- model: typeof raw.model === "string" ? raw.model : void 0,
7055
- models: Array.isArray(raw.models) ? raw.models.filter((m) => typeof m === "string") : void 0
7056
- };
7057
- this.agents.set(config.name, config);
7058
- } catch (err) {
7059
- onWarning?.(`Skipping ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
7060
6904
  }
7061
6905
  }
7062
- clearProjectAgents() {
7063
- for (const [name, config] of this.agents) {
7064
- if (!config.builtIn)
7065
- this.agents.delete(name);
7066
- }
6906
+ async clear() {
6907
+ this.cache.clear();
6908
+ }
6909
+ getStats() {
6910
+ return {
6911
+ size: this.cache.size,
6912
+ hits: this.hits,
6913
+ misses: this.misses
6914
+ };
7067
6915
  }
7068
6916
  };
7069
-
7070
- // ../../packages/chat/dist/router.js
7071
- var SQL_PATTERNS = [
7072
- /\bselect\b.*\bfrom\b/i,
7073
- /\brun\s+(a\s+)?query\b/i,
7074
- /\bwrite\s+(a\s+)?query\b/i,
7075
- /\bshow\s+(me\s+)?the\s+sql\b/i,
7076
- /\bexecute\s+sql\b/i,
7077
- /\bjoin\b.*\btable/i
7078
- ];
7079
- var DEFAULT_AGENT = "dashboard-analyst";
7080
- var RouterAgent = class {
7081
- registry;
7082
- constructor(registry) {
7083
- this.registry = registry;
6917
+ function parseTtl2(ttl) {
6918
+ const match = ttl.match(/^(\d+)(ms|s|m|h|d)$/);
6919
+ if (!match) {
6920
+ throw new Error(`Invalid TTL format: ${ttl}`);
7084
6921
  }
7085
- selectAgent(message, context, explicitAgent) {
7086
- if (explicitAgent) {
7087
- const agent = this.registry.getAgent(explicitAgent);
7088
- if (agent)
7089
- return agent;
7090
- }
7091
- if (context?.type === "explore") {
7092
- return this.registry.getAgent("data-explorer") || this.registry.getAgent(DEFAULT_AGENT);
7093
- }
7094
- if (SQL_PATTERNS.some((pattern) => pattern.test(message))) {
7095
- const sqlExplorer = this.registry.getAgent("sql-explorer");
7096
- if (sqlExplorer)
7097
- return sqlExplorer;
7098
- }
7099
- return this.registry.getAgent(DEFAULT_AGENT);
6922
+ const value = parseInt(match[1] ?? "0", 10);
6923
+ const unit = match[2];
6924
+ switch (unit) {
6925
+ case "ms":
6926
+ return value;
6927
+ case "s":
6928
+ return value * 1e3;
6929
+ case "m":
6930
+ return value * 60 * 1e3;
6931
+ case "h":
6932
+ return value * 60 * 60 * 1e3;
6933
+ case "d":
6934
+ return value * 24 * 60 * 60 * 1e3;
6935
+ default:
6936
+ throw new Error(`Unknown TTL unit: ${unit}`);
7100
6937
  }
7101
- };
7102
-
7103
- // ../../packages/chat/dist/stream.js
7104
- function formatSSEEvent(event) {
7105
- return `event: ${event.type}
7106
- data: ${JSON.stringify(event.data)}
7107
-
7108
- `;
7109
6938
  }
7110
6939
 
7111
- // ../server/dist/services/config-loader.js
7112
- var ConfigLoader = class {
7113
- projectDir;
7114
- project = null;
7115
- connections = /* @__PURE__ */ new Map();
7116
- charts = /* @__PURE__ */ new Map();
7117
- chartFolders = /* @__PURE__ */ new Map();
7118
- chartFilePaths = /* @__PURE__ */ new Map();
7119
- models = /* @__PURE__ */ new Map();
7120
- dashboards = /* @__PURE__ */ new Map();
7121
- dashboardFolders = /* @__PURE__ */ new Map();
7122
- dashboardFilePaths = /* @__PURE__ */ new Map();
7123
- schedules = /* @__PURE__ */ new Map();
7124
- events = [];
7125
- validationErrors = /* @__PURE__ */ new Map();
7126
- watcher = null;
7127
- onChangeCallbacks = [];
7128
- agentRegistry = new AgentRegistry();
7129
- env;
7130
- constructor(projectDir, env) {
7131
- this.projectDir = projectDir;
7132
- this.env = env || process.env.YAMCHART_ENV || void 0;
7133
- }
7134
- async load() {
7135
- this.connections.clear();
7136
- this.charts.clear();
7137
- this.chartFolders.clear();
7138
- this.chartFilePaths.clear();
7139
- this.models.clear();
7140
- this.dashboards.clear();
7141
- this.dashboardFolders.clear();
7142
- this.dashboardFilePaths.clear();
7143
- this.schedules.clear();
7144
- this.events = [];
7145
- this.validationErrors.clear();
7146
- await this.loadProject();
7147
- await this.loadConnections();
7148
- await this.loadCharts();
7149
- await this.loadModels();
7150
- await this.loadDashboards();
7151
- await this.loadSchedules();
7152
- await this.loadEvents();
7153
- await this.loadAgents();
7154
- }
7155
- async loadProject() {
7156
- const projectPath = join(this.projectDir, "yamchart.yaml");
7157
- try {
7158
- await access(projectPath);
7159
- } catch {
7160
- throw new Error("yamchart.yaml not found");
6940
+ // ../../packages/engine/dist/connector-factory.js
6941
+ import { join } from "path";
6942
+ function createConnectorFromConfig(connection, options) {
6943
+ const { projectDir } = options;
6944
+ switch (connection.type) {
6945
+ case "duckdb": {
6946
+ const duckdbConfig = connection;
6947
+ const dbPath = duckdbConfig.config.path.startsWith("/") ? duckdbConfig.config.path : join(projectDir, duckdbConfig.config.path);
6948
+ return new DuckDBConnector({ path: dbPath });
7161
6949
  }
7162
- const content = await readFile(projectPath, "utf-8");
7163
- const raw = parseYaml(content);
7164
- let merged = resolveProjectConfig(raw, this.env);
7165
- const localPath = join(this.projectDir, "yamchart.local.yaml");
7166
- try {
7167
- await access(localPath);
7168
- const localContent = await readFile(localPath, "utf-8");
7169
- const localOverrides = parseYaml(localContent);
7170
- if (localOverrides && typeof localOverrides === "object") {
7171
- merged = deepMerge(merged, localOverrides);
7172
- }
7173
- } catch {
6950
+ case "postgres": {
6951
+ const pgConnection = connection;
6952
+ const credentials = resolvePostgresAuth(pgConnection, options.secrets);
6953
+ return new PostgresConnector({
6954
+ host: pgConnection.config.host,
6955
+ port: pgConnection.config.port,
6956
+ database: pgConnection.config.database,
6957
+ schema: pgConnection.config.schema,
6958
+ ssl: pgConnection.config.ssl,
6959
+ user: credentials.user,
6960
+ password: credentials.password,
6961
+ min: pgConnection.pool?.min_connections,
6962
+ max: pgConnection.pool?.max_connections,
6963
+ idleTimeoutMillis: pgConnection.pool?.idle_timeout,
6964
+ statementTimeout: pgConnection.query?.timeout
6965
+ });
7174
6966
  }
7175
- const result = ProjectSchema.safeParse(merged);
7176
- if (!result.success) {
7177
- throw new Error(`Invalid yamchart.yaml: ${result.error.message}`);
6967
+ case "mysql": {
6968
+ const mysqlConnection = connection;
6969
+ const credentials = resolveMySQLAuth(mysqlConnection, options.secrets);
6970
+ return new MySQLConnector({
6971
+ host: mysqlConnection.config.host,
6972
+ port: mysqlConnection.config.port,
6973
+ database: mysqlConnection.config.database,
6974
+ ssl: mysqlConnection.config.ssl,
6975
+ user: credentials.user,
6976
+ password: credentials.password,
6977
+ min: mysqlConnection.pool?.min_connections,
6978
+ max: mysqlConnection.pool?.max_connections,
6979
+ idleTimeoutMillis: mysqlConnection.pool?.idle_timeout,
6980
+ statementTimeout: mysqlConnection.query?.timeout
6981
+ });
7178
6982
  }
7179
- this.project = result.data;
7180
- }
7181
- async loadConnections() {
7182
- const connectionsDir = join(this.projectDir, "connections");
7183
- try {
7184
- await access(connectionsDir);
7185
- } catch {
7186
- return;
6983
+ case "sqlite": {
6984
+ const sqliteConnection = connection;
6985
+ const dbPath = sqliteConnection.config.path.startsWith("/") ? sqliteConnection.config.path : sqliteConnection.config.path === ":memory:" ? ":memory:" : join(projectDir, sqliteConnection.config.path);
6986
+ return new SQLiteConnector({ path: dbPath });
7187
6987
  }
7188
- const files = await readdir(connectionsDir);
7189
- for (const file of files) {
7190
- if (extname(file) !== ".yaml" && extname(file) !== ".yml")
7191
- continue;
7192
- const filePath = join(connectionsDir, file);
7193
- const content = await readFile(filePath, "utf-8");
7194
- const parsed = parseYaml(content);
7195
- const result = ConnectionSchema.safeParse(parsed);
7196
- if (result.success) {
7197
- this.connections.set(result.data.name, result.data);
7198
- } else {
7199
- console.warn(`Invalid connection file ${file}: ${result.error.message}`);
7200
- }
6988
+ case "snowflake": {
6989
+ const sfConnection = connection;
6990
+ const credentials = resolveSnowflakeAuth(sfConnection, options.secrets);
6991
+ return new SnowflakeConnector({
6992
+ account: sfConnection.config.account,
6993
+ username: credentials.username,
6994
+ password: credentials.password,
6995
+ privateKey: credentials.privateKey,
6996
+ authenticator: credentials.authenticator,
6997
+ clientStoreTemporaryCredential: credentials.clientStoreTemporaryCredential,
6998
+ warehouse: sfConnection.config.warehouse,
6999
+ database: sfConnection.config.database,
7000
+ schema: sfConnection.config.schema,
7001
+ role: sfConnection.config.role,
7002
+ statementTimeout: sfConnection.query?.timeout
7003
+ });
7201
7004
  }
7202
- }
7203
- async loadCharts() {
7204
- const chartsDir = join(this.projectDir, "charts");
7205
- try {
7206
- await access(chartsDir);
7207
- } catch {
7208
- return;
7005
+ case "bigquery": {
7006
+ const bqConnection = connection;
7007
+ const creds = resolveBigQueryAuth(bqConnection, options.secrets);
7008
+ const keyFilename = creds.keyFilename ? creds.keyFilename.startsWith("/") ? creds.keyFilename : join(projectDir, creds.keyFilename) : void 0;
7009
+ return new BigQueryConnector({
7010
+ projectId: bqConnection.config.project_id,
7011
+ dataset: bqConnection.config.dataset,
7012
+ location: bqConnection.config.location,
7013
+ maxBytesBilled: bqConnection.config.max_bytes_billed,
7014
+ credentials: creds.credentials,
7015
+ keyFilename
7016
+ });
7209
7017
  }
7210
- await this.loadChartsFromDir(chartsDir, chartsDir);
7018
+ default:
7019
+ throw new Error(`Unsupported connection type: ${connection.type}`);
7211
7020
  }
7212
- async loadChartsFromDir(dir, baseDir) {
7213
- const entries = await readdir(dir, { withFileTypes: true });
7214
- for (const entry of entries) {
7215
- const fullPath = join(dir, entry.name);
7216
- if (entry.isDirectory() && dir === baseDir) {
7217
- await this.loadChartsFromDir(fullPath, baseDir);
7218
- } else if (entry.isFile() && (extname(entry.name) === ".yaml" || extname(entry.name) === ".yml")) {
7219
- const content = await readFile(fullPath, "utf-8");
7220
- const parsed = parseYaml(content);
7221
- const result = ChartSchema.safeParse(parsed);
7222
- if (result.success) {
7223
- const name = result.data.name;
7224
- if (this.charts.has(name)) {
7225
- console.warn(`Duplicate chart name "${name}" in ${relative(baseDir, fullPath)} (already loaded from ${relative(baseDir, this.chartFilePaths.get(name))})`);
7226
- continue;
7227
- }
7228
- this.charts.set(name, result.data);
7229
- this.chartFilePaths.set(name, fullPath);
7230
- const relDir = relative(baseDir, dirname(fullPath)).replace(/\\/g, "/");
7231
- this.chartFolders.set(name, relDir || "");
7232
- } else {
7233
- const relPath = relative(baseDir, fullPath);
7234
- const name = parsed && typeof parsed === "object" && "name" in parsed && typeof parsed.name === "string" ? parsed.name : void 0;
7235
- const errorMsg = result.error.errors.map((e) => {
7236
- const path = e.path.length > 0 ? e.path.join(".") : "(root)";
7237
- return `${path}: ${e.message}`;
7238
- }).join("; ");
7239
- console.warn(`Invalid chart file ${relPath}: ${errorMsg}`);
7240
- if (name) {
7241
- this.validationErrors.set(name, { file: relPath, message: errorMsg });
7242
- }
7243
- }
7244
- }
7021
+ }
7022
+ function buildConnectionInfo(connection) {
7023
+ const info2 = {
7024
+ name: connection.name,
7025
+ type: connection.type
7026
+ };
7027
+ switch (connection.type) {
7028
+ case "duckdb": {
7029
+ const c = connection;
7030
+ info2.path = c.config.path;
7031
+ break;
7245
7032
  }
7246
- }
7247
- async loadModels() {
7248
- const modelsDir = join(this.projectDir, "models");
7249
- try {
7250
- await access(modelsDir);
7251
- } catch {
7252
- return;
7033
+ case "postgres": {
7034
+ const c = connection;
7035
+ info2.host = c.config.host;
7036
+ info2.port = c.config.port;
7037
+ info2.database = c.config.database;
7038
+ info2.schema = c.config.schema;
7039
+ break;
7253
7040
  }
7254
- await this.loadModelsFromDir(modelsDir);
7041
+ case "mysql": {
7042
+ const c = connection;
7043
+ info2.host = c.config.host;
7044
+ info2.port = c.config.port;
7045
+ info2.database = c.config.database;
7046
+ break;
7047
+ }
7048
+ case "sqlite": {
7049
+ const c = connection;
7050
+ info2.path = c.config.path;
7051
+ break;
7052
+ }
7053
+ case "snowflake": {
7054
+ const c = connection;
7055
+ info2.account = c.config.account;
7056
+ info2.warehouse = c.config.warehouse;
7057
+ info2.database = c.config.database;
7058
+ info2.schema = c.config.schema;
7059
+ info2.role = c.config.role;
7060
+ break;
7061
+ }
7062
+ case "bigquery": {
7063
+ const c = connection;
7064
+ info2.database = c.config.project_id;
7065
+ info2.schema = c.config.dataset;
7066
+ break;
7067
+ }
7068
+ }
7069
+ return info2;
7070
+ }
7071
+ async function testConnection(connection, options) {
7072
+ const start = performance.now();
7073
+ let connector = null;
7074
+ try {
7075
+ connector = createConnectorFromConfig(connection, options);
7076
+ await connector.connect();
7077
+ const healthQuery = connection.type === "duckdb" ? "SELECT 1 AS health" : "SELECT 1";
7078
+ await connector.execute(healthQuery);
7079
+ const latencyMs = performance.now() - start;
7080
+ return {
7081
+ name: connection.name,
7082
+ type: connection.type,
7083
+ status: "healthy",
7084
+ latencyMs: Math.round(latencyMs)
7085
+ };
7086
+ } catch (err) {
7087
+ const latencyMs = performance.now() - start;
7088
+ let errorMessage = "Unknown error";
7089
+ if (err instanceof Error) {
7090
+ const pgError = err;
7091
+ errorMessage = err.message || pgError.code || "Connection failed";
7092
+ } else {
7093
+ errorMessage = String(err);
7094
+ }
7095
+ return {
7096
+ name: connection.name,
7097
+ type: connection.type,
7098
+ status: "unhealthy",
7099
+ latencyMs: Math.round(latencyMs),
7100
+ error: errorMessage
7101
+ };
7102
+ } finally {
7103
+ if (connector) {
7104
+ try {
7105
+ await connector.disconnect();
7106
+ } catch {
7107
+ }
7108
+ }
7109
+ }
7110
+ }
7111
+
7112
+ // ../../packages/engine/dist/query-service.js
7113
+ function quoteIdentifier(name, dialect) {
7114
+ if (dialect === "mysql" || dialect === "bigquery")
7115
+ return `\`${name.replace(/`/g, "``")}\``;
7116
+ return `"${name.replace(/"/g, '""')}"`;
7117
+ }
7118
+ var QueryService = class {
7119
+ compiler;
7120
+ connector;
7121
+ cache;
7122
+ models;
7123
+ refs;
7124
+ dialect;
7125
+ constructor(config) {
7126
+ this.compiler = new QueryCompiler({
7127
+ models: config.models,
7128
+ refs: config.refs
7129
+ });
7130
+ this.connector = config.connector;
7131
+ this.cache = config.cache;
7132
+ this.models = new Map(Object.entries(config.models));
7133
+ this.refs = config.refs;
7134
+ this.dialect = config.dialect ?? "duckdb";
7135
+ }
7136
+ async executeChart(chart, params, userContext) {
7137
+ const compiled = this.compiler.compile(chart, params, userContext);
7138
+ const cached = await this.cache.get(compiled.cacheKey);
7139
+ if (cached) {
7140
+ return {
7141
+ columns: cached.columns,
7142
+ rows: cached.rows,
7143
+ rowCount: cached.rowCount,
7144
+ durationMs: cached.durationMs,
7145
+ cached: true,
7146
+ cacheKey: compiled.cacheKey,
7147
+ compiledSql: compiled.sql
7148
+ };
7149
+ }
7150
+ const result = await this.connector.execute(compiled.sql);
7151
+ const cacheEntry = {
7152
+ columns: result.columns,
7153
+ rows: result.rows,
7154
+ rowCount: result.rowCount,
7155
+ durationMs: result.durationMs,
7156
+ cachedAt: Date.now()
7157
+ };
7158
+ await this.cache.set(compiled.cacheKey, cacheEntry);
7159
+ return {
7160
+ ...result,
7161
+ cached: false,
7162
+ cacheKey: compiled.cacheKey,
7163
+ compiledSql: compiled.sql
7164
+ };
7165
+ }
7166
+ /**
7167
+ * Execute a table chart with server-side pagination.
7168
+ * Wraps the compiled SQL with LIMIT/OFFSET and runs a parallel COUNT(*) query.
7169
+ * Optionally computes summary aggregations for specified columns.
7170
+ */
7171
+ async executeChartPaginated(chart, params, options, userContext) {
7172
+ const compiled = this.compiler.compile(chart, params, userContext);
7173
+ const baseSql = compiled.sql;
7174
+ const q = quoteIdentifier;
7175
+ const d = this.dialect;
7176
+ let orderBy = "";
7177
+ if (options.sortField) {
7178
+ const cleanField = options.sortField.replace(/[^\w\s]/g, "");
7179
+ if (cleanField.length > 0) {
7180
+ orderBy = `ORDER BY ${q(cleanField, d)} ${options.sortDirection === "desc" ? "DESC" : "ASC"}`;
7181
+ }
7182
+ }
7183
+ const offset = options.page * options.pageSize;
7184
+ const dataSql = `SELECT * FROM (${baseSql}) _t ${orderBy} LIMIT ${options.pageSize} OFFSET ${offset}`;
7185
+ const countCacheKey = `${compiled.cacheKey}:count`;
7186
+ let totalCount;
7187
+ const cachedCount = await this.cache.get(countCacheKey);
7188
+ if (cachedCount) {
7189
+ totalCount = cachedCount.rowCount;
7190
+ } else {
7191
+ const countSql = `SELECT COUNT(*) AS _count FROM (${baseSql}) _t`;
7192
+ const countResult = await this.connector.execute(countSql);
7193
+ totalCount = Number(countResult.rows[0]?._count ?? 0);
7194
+ await this.cache.set(countCacheKey, {
7195
+ columns: [],
7196
+ rows: [],
7197
+ rowCount: totalCount,
7198
+ durationMs: countResult.durationMs,
7199
+ cachedAt: Date.now()
7200
+ });
7201
+ }
7202
+ let summaryValues;
7203
+ if (options.summaryColumns && options.summaryColumns.length > 0) {
7204
+ const summaryCacheKey = `${compiled.cacheKey}:summary`;
7205
+ const cachedSummary = await this.cache.get(summaryCacheKey);
7206
+ if (cachedSummary && cachedSummary.rows[0]) {
7207
+ summaryValues = cachedSummary.rows[0];
7208
+ } else {
7209
+ const aggExprs = options.summaryColumns.map(({ field, summary }) => {
7210
+ const col = q(field, d);
7211
+ switch (summary) {
7212
+ case "sum":
7213
+ return `SUM(${col}) AS ${q(`_sum_${field}`, d)}`;
7214
+ case "avg":
7215
+ return `AVG(${col}) AS ${q(`_avg_${field}`, d)}`;
7216
+ case "min":
7217
+ return `MIN(${col}) AS ${q(`_min_${field}`, d)}`;
7218
+ case "max":
7219
+ return `MAX(${col}) AS ${q(`_max_${field}`, d)}`;
7220
+ case "count":
7221
+ return `COUNT(${col}) AS ${q(`_count_${field}`, d)}`;
7222
+ default:
7223
+ return null;
7224
+ }
7225
+ }).filter(Boolean);
7226
+ if (aggExprs.length > 0) {
7227
+ const summarySql = `SELECT ${aggExprs.join(", ")} FROM (${baseSql}) _t`;
7228
+ const summaryResult = await this.connector.execute(summarySql);
7229
+ if (summaryResult.rows[0]) {
7230
+ summaryValues = {};
7231
+ for (const { field, summary } of options.summaryColumns) {
7232
+ const key = `_${summary}_${field}`;
7233
+ summaryValues[`${summary}:${field}`] = Number(summaryResult.rows[0][key] ?? 0);
7234
+ }
7235
+ await this.cache.set(summaryCacheKey, {
7236
+ columns: [],
7237
+ rows: [summaryValues],
7238
+ rowCount: 0,
7239
+ durationMs: summaryResult.durationMs,
7240
+ cachedAt: Date.now()
7241
+ });
7242
+ }
7243
+ }
7244
+ }
7245
+ }
7246
+ const result = await this.connector.execute(dataSql);
7247
+ return {
7248
+ ...result,
7249
+ cached: false,
7250
+ cacheKey: compiled.cacheKey,
7251
+ compiledSql: dataSql,
7252
+ totalCount,
7253
+ page: options.page,
7254
+ pageSize: options.pageSize,
7255
+ summaryValues
7256
+ };
7257
+ }
7258
+ /**
7259
+ * Execute a chart query for CSV export with a bounded row limit.
7260
+ * Returns full QueryResult — caller streams to HTTP response.
7261
+ */
7262
+ async executeChartForExport(chart, params, maxRows, userContext) {
7263
+ const compiled = this.compiler.compile(chart, params, userContext);
7264
+ const sql = `SELECT * FROM (${compiled.sql}) _t LIMIT ${maxRows}`;
7265
+ const result = await this.connector.execute(sql);
7266
+ return {
7267
+ ...result,
7268
+ cached: false,
7269
+ cacheKey: compiled.cacheKey,
7270
+ compiledSql: sql
7271
+ };
7272
+ }
7273
+ /**
7274
+ * Compile a chart query without executing it.
7275
+ * Returns the compiled SQL and cache key.
7276
+ */
7277
+ compileChart(chart, params, userContext) {
7278
+ return this.compiler.compile(chart, params, userContext);
7279
+ }
7280
+ invalidateChart(chartName) {
7281
+ this.cache.invalidatePattern(`${chartName}:*`);
7282
+ }
7283
+ invalidateAll() {
7284
+ this.cache.clear();
7285
+ }
7286
+ /**
7287
+ * Update the compiler with new models and refs.
7288
+ * Called when config is reloaded.
7289
+ */
7290
+ updateCompiler(config) {
7291
+ this.compiler = new QueryCompiler({
7292
+ models: config.models,
7293
+ refs: config.refs
7294
+ });
7295
+ this.models = new Map(Object.entries(config.models));
7296
+ this.refs = config.refs;
7297
+ }
7298
+ getCacheStats() {
7299
+ return this.cache.getStats();
7300
+ }
7301
+ async executeRawSql(sql) {
7302
+ return this.connector.execute(sql);
7303
+ }
7304
+ async executeModel(modelName, params, userContext, limit2 = 1e3, filters) {
7305
+ const model = this.models.get(modelName);
7306
+ if (!model) {
7307
+ throw new Error(`Unknown model: ${modelName}`);
7308
+ }
7309
+ const context = createTemplateContext(params, this.refs, userContext);
7310
+ let sql = renderTemplate(model.sql, context);
7311
+ if (filters && Object.keys(filters).length > 0) {
7312
+ const clauses = Object.entries(filters).map(([field, value]) => `${field} = '${value.replace(/'/g, "''")}'`);
7313
+ sql = `SELECT * FROM (${sql}) _detail WHERE ${clauses.join(" AND ")} LIMIT ${limit2}`;
7314
+ } else {
7315
+ sql = `SELECT * FROM (${sql}) _detail LIMIT ${limit2}`;
7316
+ }
7317
+ return this.connector.execute(sql);
7318
+ }
7319
+ async executeParameterOptions(modelName, valueField, labelField) {
7320
+ const model = this.models.get(modelName);
7321
+ if (!model) {
7322
+ throw new Error(`Unknown model: ${modelName}`);
7323
+ }
7324
+ const context = createTemplateContext({}, this.refs);
7325
+ const sql = renderTemplate(model.sql, context);
7326
+ const result = await this.connector.execute(sql);
7327
+ return result.rows.map((row) => ({
7328
+ value: String(row[valueField] ?? ""),
7329
+ label: String(row[labelField] ?? row[valueField] ?? "")
7330
+ }));
7331
+ }
7332
+ };
7333
+
7334
+ // ../../packages/engine/dist/config-loader.js
7335
+ import { readFile, readdir, access } from "fs/promises";
7336
+ import { join as join2, extname, relative, dirname } from "path";
7337
+ import { parse as parseYaml } from "yaml";
7338
+ import { watch } from "chokidar";
7339
+
7340
+ // ../../packages/chat/dist/registry.js
7341
+ import { parse } from "yaml";
7342
+
7343
+ // ../../packages/chat/dist/agents/dashboard-analyst.js
7344
+ var DASHBOARD_ANALYST = {
7345
+ name: "dashboard-analyst",
7346
+ title: "Dashboard Analyst",
7347
+ description: "Summarizes dashboards, explains trends, answers data questions, and can apply filters to the dashboard.",
7348
+ systemPrompt: `You are an AI dashboard analyst for a business intelligence platform.
7349
+
7350
+ Your job is to help users understand their data by:
7351
+ - Summarizing what a dashboard or chart shows
7352
+ - Explaining trends, outliers, and patterns in the data
7353
+ - Answering questions about metrics and KPIs
7354
+ - Suggesting and applying filters to explore data
7355
+ - Running ad-hoc SQL to explore data beyond existing charts
7356
+
7357
+ ## Rules
7358
+ - Be concise. Dashboard users want quick answers, not essays.
7359
+ - Use numbers and percentages when discussing data.
7360
+ - When you query data, summarize the key findings \u2014 don't dump raw rows.
7361
+ - If asked about a trend, explain the direction, magnitude, and possible causes.
7362
+ - When suggesting filters, explain what you expect to see.
7363
+ - Use the apply_filter tool when the user asks to filter or drill down.
7364
+ - If the existing charts don't have the data needed, use run_sql to query the database directly.
7365
+ - Use call_agent to delegate to specialized agents when you need expertise you don't have.
7366
+ - Use suggest_actions to offer follow-up exploration or save options after answering.
7367
+ - If you don't have enough data to answer, say so and suggest what to query.
7368
+
7369
+ ## Data Visualization in Responses
7370
+ When you have tabular data to share, use these markdown code fences:
7371
+
7372
+ For charts (line, bar, pie, scatter):
7373
+ \`\`\`chart
7374
+ {"type":"line","data":[{"date":"2025-01-01","value":100},...],"x":"date","y":"value","title":"Optional Title"}
7375
+ \`\`\`
7376
+
7377
+ For data tables:
7378
+ \`\`\`data
7379
+ {"columns":[{"name":"date","type":"date"},{"name":"value","type":"number"}],"rows":[{"date":"2025-01-01","value":100},...]}
7380
+ \`\`\`
7381
+
7382
+ Choose charts for trends and comparisons, tables for detailed breakdowns. Keep data concise \u2014 summarize large result sets.`,
7383
+ tools: ["query_model", "get_chart_config", "get_dashboard_summary", "apply_filter", "compare_periods", "run_sql", "call_agent", "get_catalog_tables", "suggest_actions", "save_model", "save_chart", "add_to_dashboard"],
7384
+ builtIn: true
7385
+ };
7386
+
7387
+ // ../../packages/chat/dist/agents/sql-explorer.js
7388
+ var SQL_EXPLORER = {
7389
+ name: "sql-explorer",
7390
+ title: "SQL Explorer",
7391
+ description: "Runs ad-hoc SQL queries against the connected database for deep data exploration and period comparisons.",
7392
+ systemPrompt: `You are a SQL expert helping users explore their data warehouse.
7393
+
7394
+ Your job is to:
7395
+ - Write and execute SQL queries to answer user questions
7396
+ - Compare data across time periods
7397
+ - Find specific records or aggregations
7398
+ - Explain query results clearly
7399
+
7400
+ ## Rules
7401
+ - Always LIMIT your queries (default: 50 rows) to avoid large payloads.
7402
+ - Use clear column aliases so results are readable.
7403
+ - When comparing periods, use the compare_periods tool for models, or write explicit SQL for ad-hoc comparisons.
7404
+ - Explain what your query does before showing results.
7405
+ - If a query fails, explain the error and try a corrected version.
7406
+ - Never run destructive queries (INSERT, UPDATE, DELETE, DROP, etc.).
7407
+
7408
+ ## Data Visualization in Responses
7409
+ When you have tabular data to share, use these markdown code fences:
7410
+
7411
+ For charts (line, bar, pie, scatter):
7412
+ \`\`\`chart
7413
+ {"type":"line","data":[{"date":"2025-01-01","value":100},...],"x":"date","y":"value","title":"Optional Title"}
7414
+ \`\`\`
7415
+
7416
+ For data tables:
7417
+ \`\`\`data
7418
+ {"columns":[{"name":"date","type":"date"},{"name":"value","type":"number"}],"rows":[{"date":"2025-01-01","value":100},...]}
7419
+ \`\`\`
7420
+
7421
+ Choose charts for trends and comparisons, tables for detailed breakdowns. Keep data concise \u2014 summarize large result sets.`,
7422
+ tools: ["query_model", "run_sql", "compare_periods", "get_chart_config", "call_agent", "get_catalog_tables", "suggest_actions", "save_model", "save_chart"],
7423
+ builtIn: true
7424
+ };
7425
+
7426
+ // ../../packages/chat/dist/agents/data-explorer.js
7427
+ var DATA_EXPLORER = {
7428
+ name: "data-explorer",
7429
+ title: "Data Explorer",
7430
+ description: "Open-ended data exploration, visualization, and dashboard building.",
7431
+ systemPrompt: `You are an AI data explorer for a business intelligence platform.
7432
+
7433
+ Your job is to help users explore their data warehouse, build visualizations, and create dashboards \u2014 starting from curiosity and ending with saved, reusable charts.
7434
+
7435
+ ## Discovery First
7436
+ - Always start with get_catalog_tables to understand what data is available before writing SQL.
7437
+ - Prefer querying named models (query_model) over raw SQL when a model already exists.
7438
+ - When no model exists, use run_sql to explore \u2014 then offer to save the query as a model.
7439
+
7440
+ ## Rules
7441
+ - All SQL must be read-only (SELECT only). Never run INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, or CREATE.
7442
+ - Always LIMIT queries to 50 rows by default. Use LIMIT explicitly.
7443
+ - Use clear column aliases so results are readable.
7444
+ - When a query fails, explain the error and try a corrected version.
7445
+ - Explain what you're querying and why before showing results.
7446
+
7447
+ ## Proactive Visualization
7448
+ After retrieving data, always visualize it \u2014 don't just dump raw rows. Choose the right form:
7449
+ - Trends over time \u2192 line chart
7450
+ - Comparisons across categories \u2192 bar chart
7451
+ - Part-to-whole \u2192 pie chart
7452
+ - Single key metric \u2192 show as bold text or KPI
7453
+ - Detailed breakdowns \u2192 data table
7454
+
7455
+ For charts (line, bar, pie, scatter):
7456
+ \`\`\`chart
7457
+ {"type":"line","data":[{"date":"2025-01-01","value":100},...],"x":"date","y":"value","title":"Optional Title"}
7458
+ \`\`\`
7459
+
7460
+ For data tables:
7461
+ \`\`\`data
7462
+ {"columns":[{"name":"date","type":"date"},{"name":"value","type":"number"}],"rows":[{"date":"2025-01-01","value":100},...]}
7463
+ \`\`\`
7464
+
7465
+ Keep visualizations concise \u2014 summarize large result sets rather than showing every row.
7466
+
7467
+ ## Saving Work
7468
+ When a user wants to keep a query or chart:
7469
+ 1. Use save_model to create a reusable SQL model file.
7470
+ 2. Use save_chart to create a chart YAML referencing that model.
7471
+ 3. Use create_dashboard to assemble saved charts into a new dashboard.
7472
+ 4. After saving, use suggest_actions to offer navigation or further exploration.
7473
+
7474
+ ## Suggesting Next Steps
7475
+ Always use suggest_actions after answering a question to offer natural follow-up options:
7476
+ - Explore related dimensions or time periods
7477
+ - Save the current query as a model
7478
+ - Create a chart from the data
7479
+ - Add the chart to a dashboard
7480
+
7481
+ ## Collaboration
7482
+ Use call_agent to delegate to specialized agents when needed:
7483
+ - Dashboard Analyst for questions about existing dashboards
7484
+ - SQL Explorer for complex multi-step SQL investigations`,
7485
+ tools: ["run_sql", "query_model", "get_catalog_tables", "compare_periods", "save_model", "save_chart", "create_dashboard", "suggest_actions", "call_agent"],
7486
+ builtIn: true
7487
+ };
7488
+
7489
+ // ../../packages/chat/dist/registry.js
7490
+ var AgentRegistry = class {
7491
+ agents = /* @__PURE__ */ new Map();
7492
+ constructor() {
7493
+ this.agents.set(DASHBOARD_ANALYST.name, DASHBOARD_ANALYST);
7494
+ this.agents.set(SQL_EXPLORER.name, SQL_EXPLORER);
7495
+ this.agents.set(DATA_EXPLORER.name, DATA_EXPLORER);
7255
7496
  }
7256
- async loadModelsFromDir(dir) {
7257
- const entries = await readdir(dir, { withFileTypes: true });
7258
- for (const entry of entries) {
7259
- const fullPath = join(dir, entry.name);
7260
- if (entry.isDirectory()) {
7261
- await this.loadModelsFromDir(fullPath);
7262
- } else if (extname(entry.name) === ".sql") {
7263
- const content = await readFile(fullPath, "utf-8");
7264
- try {
7265
- const parsed = parseModelMetadata(content);
7266
- this.models.set(parsed.name, {
7267
- metadata: parsed,
7268
- sql: parsed.sql,
7269
- filePath: fullPath
7270
- });
7271
- } catch (err) {
7272
- console.warn(`Invalid model file ${entry.name}: ${err}`);
7273
- }
7274
- }
7275
- }
7497
+ getAgent(name) {
7498
+ return this.agents.get(name);
7276
7499
  }
7277
- async loadDashboards() {
7278
- const dashboardsDir = join(this.projectDir, "dashboards");
7279
- try {
7280
- await access(dashboardsDir);
7281
- } catch {
7282
- return;
7283
- }
7284
- await this.loadDashboardsFromDir(dashboardsDir, dashboardsDir);
7500
+ listAgents() {
7501
+ return Array.from(this.agents.values());
7285
7502
  }
7286
- async loadDashboardsFromDir(dir, baseDir) {
7287
- const entries = await readdir(dir, { withFileTypes: true });
7288
- for (const entry of entries) {
7289
- const fullPath = join(dir, entry.name);
7290
- if (entry.isDirectory() && dir === baseDir) {
7291
- await this.loadDashboardsFromDir(fullPath, baseDir);
7292
- } else if (entry.isFile() && (extname(entry.name) === ".yaml" || extname(entry.name) === ".yml")) {
7293
- const content = await readFile(fullPath, "utf-8");
7294
- const parsed = parseYaml(content);
7295
- const result = DashboardSchema.safeParse(parsed);
7296
- if (result.success) {
7297
- const name = result.data.name;
7298
- if (this.dashboards.has(name)) {
7299
- console.warn(`Duplicate dashboard name "${name}" in ${relative(baseDir, fullPath)} (already loaded from ${relative(baseDir, this.dashboardFilePaths.get(name))})`);
7300
- continue;
7301
- }
7302
- this.dashboards.set(name, result.data);
7303
- this.dashboardFilePaths.set(name, fullPath);
7304
- const relDir = relative(baseDir, dirname(fullPath)).replace(/\\/g, "/");
7305
- this.dashboardFolders.set(name, relDir || "");
7306
- } else {
7307
- const relPath = relative(baseDir, fullPath);
7308
- const name = parsed && typeof parsed === "object" && "name" in parsed && typeof parsed.name === "string" ? parsed.name : void 0;
7309
- const errorMsg = result.error.errors.map((e) => {
7310
- const path = e.path.length > 0 ? e.path.join(".") : "(root)";
7311
- return `${path}: ${e.message}`;
7312
- }).join("; ");
7313
- console.warn(`Invalid dashboard file ${relPath}: ${errorMsg}`);
7314
- if (name) {
7315
- this.validationErrors.set(name, { file: relPath, message: errorMsg });
7316
- }
7317
- }
7318
- }
7319
- }
7503
+ registerAgent(config) {
7504
+ this.agents.set(config.name, config);
7320
7505
  }
7321
- async loadSchedules() {
7322
- const schedulesDir = join(this.projectDir, "schedules");
7506
+ loadFromYaml(yamlContent, filePath, onWarning) {
7323
7507
  try {
7324
- await access(schedulesDir);
7325
- } catch {
7326
- return;
7327
- }
7328
- const files = await readdir(schedulesDir);
7329
- for (const file of files) {
7330
- if (extname(file) !== ".yaml" && extname(file) !== ".yml")
7331
- continue;
7332
- const filePath = join(schedulesDir, file);
7333
- const content = await readFile(filePath, "utf-8");
7334
- const parsed = parseYaml(content);
7335
- const result = ScheduleSchema.safeParse(parsed);
7336
- if (result.success) {
7337
- this.schedules.set(result.data.name, result.data);
7338
- } else {
7339
- console.warn(`Invalid schedule file ${file}: ${result.error.message}`);
7508
+ const raw = parse(yamlContent);
7509
+ if (!raw || typeof raw !== "object") {
7510
+ onWarning?.(`Skipping ${filePath}: not a valid YAML object`);
7511
+ return;
7340
7512
  }
7341
- }
7342
- }
7343
- async loadEvents() {
7344
- const eventsPath = join(this.projectDir, "events.yaml");
7345
- try {
7346
- await access(eventsPath);
7347
- } catch {
7348
- return;
7349
- }
7350
- const content = await readFile(eventsPath, "utf-8");
7351
- const parsed = parseYaml(content);
7352
- const result = EventsFileSchema.safeParse(parsed);
7353
- if (result.success) {
7354
- this.events = result.data.events;
7355
- } else {
7356
- console.warn(`Invalid events.yaml: ${result.error.message}`);
7357
- }
7358
- }
7359
- async loadAgents() {
7360
- const agentsDir = join(this.projectDir, "agents");
7361
- this.agentRegistry.clearProjectAgents();
7362
- try {
7363
- const files = await readdir(agentsDir);
7364
- for (const file of files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"))) {
7365
- const content = await readFile(join(agentsDir, file), "utf-8");
7366
- this.agentRegistry.loadFromYaml(content, `agents/${file}`, (msg) => {
7367
- console.warn(msg);
7368
- });
7513
+ if (!raw.name || typeof raw.name !== "string") {
7514
+ onWarning?.(`Skipping ${filePath}: missing required "name" field`);
7515
+ return;
7369
7516
  }
7370
- } catch {
7371
- }
7372
- }
7373
- startWatching() {
7374
- if (this.watcher)
7375
- return;
7376
- const watchPaths = [
7377
- join(this.projectDir, "yamchart.yaml"),
7378
- join(this.projectDir, "yamchart.local.yaml"),
7379
- join(this.projectDir, "connections"),
7380
- join(this.projectDir, "charts"),
7381
- join(this.projectDir, "models"),
7382
- join(this.projectDir, "dashboards"),
7383
- join(this.projectDir, "schedules"),
7384
- join(this.projectDir, "events.yaml"),
7385
- join(this.projectDir, "agents")
7386
- ];
7387
- this.watcher = watch(watchPaths, {
7388
- ignored: /(^|[\/\\])\../,
7389
- // Ignore dotfiles
7390
- persistent: true,
7391
- ignoreInitial: true,
7392
- depth: 1
7393
- // Only watch one level deep in subdirectories
7394
- });
7395
- this.watcher.on("change", async (path) => {
7396
- console.log(`File changed: ${path}`);
7397
- await this.reload();
7398
- });
7399
- this.watcher.on("add", async (path) => {
7400
- console.log(`File added: ${path}`);
7401
- await this.reload();
7402
- });
7403
- this.watcher.on("unlink", async (path) => {
7404
- console.log(`File removed: ${path}`);
7405
- await this.reload();
7406
- });
7407
- this.watcher.on("error", (err) => {
7408
- const code = err.code;
7409
- if (code === "EMFILE") {
7410
- console.warn(`Warning: Too many open files for file watching. The server will continue running but won't auto-reload on file changes.
7411
- Fix: run "ulimit -n 10240" before starting yamchart dev, or add it to your shell profile.`);
7412
- this.watcher?.close();
7413
- this.watcher = null;
7414
- } else {
7415
- console.error("File watcher error:", err);
7517
+ if (!raw.description || typeof raw.description !== "string") {
7518
+ onWarning?.(`Skipping ${filePath}: missing required "description" field`);
7519
+ return;
7416
7520
  }
7417
- });
7418
- }
7419
- async reload() {
7420
- try {
7421
- await this.load();
7422
- for (const callback of this.onChangeCallbacks) {
7423
- callback();
7521
+ if (!raw.system_prompt || typeof raw.system_prompt !== "string") {
7522
+ onWarning?.(`Skipping ${filePath}: missing required "system_prompt" field`);
7523
+ return;
7424
7524
  }
7525
+ const config = {
7526
+ name: raw.name,
7527
+ title: raw.title ?? raw.name,
7528
+ description: raw.description,
7529
+ systemPrompt: raw.system_prompt,
7530
+ tools: Array.isArray(raw.tools) ? raw.tools.filter((t) => typeof t === "string") : [],
7531
+ builtIn: false,
7532
+ model: typeof raw.model === "string" ? raw.model : void 0,
7533
+ models: Array.isArray(raw.models) ? raw.models.filter((m) => typeof m === "string") : void 0
7534
+ };
7535
+ this.agents.set(config.name, config);
7425
7536
  } catch (err) {
7426
- console.error("Error reloading config:", err);
7427
- }
7428
- }
7429
- onChange(callback) {
7430
- this.onChangeCallbacks.push(callback);
7431
- }
7432
- async stop() {
7433
- if (this.watcher) {
7434
- await this.watcher.close();
7435
- this.watcher = null;
7436
- }
7437
- }
7438
- getProject() {
7439
- if (!this.project) {
7440
- throw new Error("Config not loaded");
7441
- }
7442
- return this.project;
7443
- }
7444
- getConnections() {
7445
- return Array.from(this.connections.values());
7446
- }
7447
- getConnectionByName(name) {
7448
- return this.connections.get(name);
7449
- }
7450
- getCharts() {
7451
- return Array.from(this.charts.values());
7452
- }
7453
- getChartByName(name) {
7454
- return this.charts.get(name);
7455
- }
7456
- getChartFolder(name) {
7457
- return this.chartFolders.get(name) ?? "";
7458
- }
7459
- getChartFilePath(name) {
7460
- return this.chartFilePaths.get(name);
7461
- }
7462
- getModels() {
7463
- return Array.from(this.models.values());
7464
- }
7465
- getModelByName(name) {
7466
- return this.models.get(name);
7467
- }
7468
- getDefaultConnection() {
7469
- const envConnection = process.env.YAMCHART_CONNECTION;
7470
- if (envConnection) {
7471
- return this.connections.get(envConnection);
7472
- }
7473
- const defaultName = this.project?.defaults?.connection;
7474
- if (defaultName) {
7475
- return this.connections.get(defaultName);
7537
+ onWarning?.(`Skipping ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
7476
7538
  }
7477
- return this.connections.values().next().value;
7478
- }
7479
- getDashboards() {
7480
- return Array.from(this.dashboards.values());
7481
- }
7482
- getDashboardByName(name) {
7483
- return this.dashboards.get(name);
7484
- }
7485
- getDashboardFolder(name) {
7486
- return this.dashboardFolders.get(name) ?? "";
7487
- }
7488
- getDashboardFilePath(name) {
7489
- return this.dashboardFilePaths.get(name);
7490
- }
7491
- getSchedules() {
7492
- return Array.from(this.schedules.values());
7493
7539
  }
7494
- getEvents() {
7495
- return this.events;
7496
- }
7497
- getProjectDir() {
7498
- return this.projectDir;
7499
- }
7500
- getValidationError(name) {
7501
- return this.validationErrors.get(name);
7502
- }
7503
- getValidationErrors() {
7504
- return this.validationErrors;
7505
- }
7506
- getAgentRegistry() {
7507
- return this.agentRegistry;
7540
+ clearProjectAgents() {
7541
+ for (const [name, config] of this.agents) {
7542
+ if (!config.builtIn)
7543
+ this.agents.delete(name);
7544
+ }
7508
7545
  }
7509
7546
  };
7510
7547
 
7511
- // ../server/dist/services/cache.js
7512
- import { LRUCache } from "lru-cache";
7513
- var MemoryCache = class {
7514
- cache;
7515
- defaultTtlMs;
7516
- hits = 0;
7517
- misses = 0;
7518
- constructor(options) {
7519
- this.defaultTtlMs = options.defaultTtlMs;
7520
- this.cache = new LRUCache({
7521
- max: options.maxSize,
7522
- ttl: options.defaultTtlMs,
7523
- ttlAutopurge: true
7524
- });
7548
+ // ../../packages/chat/dist/router.js
7549
+ var SQL_PATTERNS = [
7550
+ /\bselect\b.*\bfrom\b/i,
7551
+ /\brun\s+(a\s+)?query\b/i,
7552
+ /\bwrite\s+(a\s+)?query\b/i,
7553
+ /\bshow\s+(me\s+)?the\s+sql\b/i,
7554
+ /\bexecute\s+sql\b/i,
7555
+ /\bjoin\b.*\btable/i
7556
+ ];
7557
+ var DEFAULT_AGENT = "dashboard-analyst";
7558
+ var RouterAgent = class {
7559
+ registry;
7560
+ constructor(registry) {
7561
+ this.registry = registry;
7525
7562
  }
7526
- async get(key) {
7527
- const value = this.cache.get(key);
7528
- if (value) {
7529
- this.hits++;
7530
- return value;
7563
+ selectAgent(message, context, explicitAgent) {
7564
+ if (explicitAgent) {
7565
+ const agent = this.registry.getAgent(explicitAgent);
7566
+ if (agent)
7567
+ return agent;
7531
7568
  }
7532
- this.misses++;
7533
- return null;
7569
+ if (context?.type === "explore") {
7570
+ return this.registry.getAgent("data-explorer") || this.registry.getAgent(DEFAULT_AGENT);
7571
+ }
7572
+ if (SQL_PATTERNS.some((pattern) => pattern.test(message))) {
7573
+ const sqlExplorer = this.registry.getAgent("sql-explorer");
7574
+ if (sqlExplorer)
7575
+ return sqlExplorer;
7576
+ }
7577
+ return this.registry.getAgent(DEFAULT_AGENT);
7534
7578
  }
7535
- async set(key, value, ttlMs) {
7536
- this.cache.set(key, value, { ttl: ttlMs ?? this.defaultTtlMs });
7579
+ };
7580
+
7581
+ // ../../packages/chat/dist/stream.js
7582
+ function formatSSEEvent(event) {
7583
+ return `event: ${event.type}
7584
+ data: ${JSON.stringify(event.data)}
7585
+
7586
+ `;
7587
+ }
7588
+
7589
+ // ../../packages/engine/dist/config-loader.js
7590
+ var ConfigLoader = class {
7591
+ projectDir;
7592
+ project = null;
7593
+ connections = /* @__PURE__ */ new Map();
7594
+ connectionSecrets = /* @__PURE__ */ new Map();
7595
+ runtimeKnowledgeUrl;
7596
+ runtimeAi;
7597
+ charts = /* @__PURE__ */ new Map();
7598
+ chartFolders = /* @__PURE__ */ new Map();
7599
+ chartFilePaths = /* @__PURE__ */ new Map();
7600
+ models = /* @__PURE__ */ new Map();
7601
+ dashboards = /* @__PURE__ */ new Map();
7602
+ dashboardFolders = /* @__PURE__ */ new Map();
7603
+ dashboardFilePaths = /* @__PURE__ */ new Map();
7604
+ schedules = /* @__PURE__ */ new Map();
7605
+ events = [];
7606
+ validationErrors = /* @__PURE__ */ new Map();
7607
+ watcher = null;
7608
+ onChangeCallbacks = [];
7609
+ agentRegistry = new AgentRegistry();
7610
+ env;
7611
+ constructor(projectDir, env) {
7612
+ this.projectDir = projectDir;
7613
+ this.env = env || process.env.YAMCHART_ENV || void 0;
7537
7614
  }
7538
- async invalidate(key) {
7539
- this.cache.delete(key);
7615
+ async load() {
7616
+ this.connections.clear();
7617
+ this.connectionSecrets.clear();
7618
+ this.runtimeKnowledgeUrl = void 0;
7619
+ this.runtimeAi = void 0;
7620
+ this.charts.clear();
7621
+ this.chartFolders.clear();
7622
+ this.chartFilePaths.clear();
7623
+ this.models.clear();
7624
+ this.dashboards.clear();
7625
+ this.dashboardFolders.clear();
7626
+ this.dashboardFilePaths.clear();
7627
+ this.schedules.clear();
7628
+ this.events = [];
7629
+ this.validationErrors.clear();
7630
+ await this.loadProject();
7631
+ await this.loadConnections();
7632
+ await this.loadCharts();
7633
+ await this.loadModels();
7634
+ await this.loadDashboards();
7635
+ await this.loadSchedules();
7636
+ await this.loadEvents();
7637
+ await this.loadAgents();
7540
7638
  }
7541
- async invalidatePattern(pattern) {
7542
- const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
7543
- const regex = new RegExp(`^${regexPattern}$`);
7544
- for (const key of this.cache.keys()) {
7545
- if (regex.test(key)) {
7546
- this.cache.delete(key);
7639
+ async loadProject() {
7640
+ const projectPath = join2(this.projectDir, "yamchart.yaml");
7641
+ try {
7642
+ await access(projectPath);
7643
+ } catch {
7644
+ throw new Error("yamchart.yaml not found");
7645
+ }
7646
+ const content = await readFile(projectPath, "utf-8");
7647
+ const raw = parseYaml(content);
7648
+ let merged = resolveProjectConfig(raw, this.env);
7649
+ const localPath = join2(this.projectDir, "yamchart.local.yaml");
7650
+ try {
7651
+ await access(localPath);
7652
+ const localContent = await readFile(localPath, "utf-8");
7653
+ const localOverrides = parseYaml(localContent);
7654
+ if (localOverrides && typeof localOverrides === "object") {
7655
+ merged = deepMerge(merged, localOverrides);
7547
7656
  }
7657
+ } catch {
7548
7658
  }
7659
+ const result = ProjectSchema.safeParse(merged);
7660
+ if (!result.success) {
7661
+ throw new Error(`Invalid yamchart.yaml: ${result.error.message}`);
7662
+ }
7663
+ this.project = result.data;
7549
7664
  }
7550
- async clear() {
7551
- this.cache.clear();
7552
- }
7553
- getStats() {
7554
- return {
7555
- size: this.cache.size,
7556
- hits: this.hits,
7557
- misses: this.misses
7558
- };
7559
- }
7560
- };
7561
- function parseTtl2(ttl) {
7562
- const match = ttl.match(/^(\d+)(ms|s|m|h|d)$/);
7563
- if (!match) {
7564
- throw new Error(`Invalid TTL format: ${ttl}`);
7665
+ async loadConnections() {
7666
+ const connectionsDir = join2(this.projectDir, "connections");
7667
+ try {
7668
+ await access(connectionsDir);
7669
+ } catch {
7670
+ return;
7671
+ }
7672
+ const files = await readdir(connectionsDir);
7673
+ for (const file of files) {
7674
+ if (extname(file) !== ".yaml" && extname(file) !== ".yml")
7675
+ continue;
7676
+ const filePath = join2(connectionsDir, file);
7677
+ const content = await readFile(filePath, "utf-8");
7678
+ const parsed = parseYaml(content);
7679
+ const result = ConnectionSchema.safeParse(parsed);
7680
+ if (result.success) {
7681
+ this.connections.set(result.data.name, result.data);
7682
+ } else {
7683
+ console.warn(`Invalid connection file ${file}: ${result.error.message}`);
7684
+ }
7685
+ }
7565
7686
  }
7566
- const value = parseInt(match[1] ?? "0", 10);
7567
- const unit = match[2];
7568
- switch (unit) {
7569
- case "ms":
7570
- return value;
7571
- case "s":
7572
- return value * 1e3;
7573
- case "m":
7574
- return value * 60 * 1e3;
7575
- case "h":
7576
- return value * 60 * 60 * 1e3;
7577
- case "d":
7578
- return value * 24 * 60 * 60 * 1e3;
7579
- default:
7580
- throw new Error(`Unknown TTL unit: ${unit}`);
7687
+ async loadCharts() {
7688
+ const chartsDir = join2(this.projectDir, "charts");
7689
+ try {
7690
+ await access(chartsDir);
7691
+ } catch {
7692
+ return;
7693
+ }
7694
+ await this.loadChartsFromDir(chartsDir, chartsDir);
7581
7695
  }
7582
- }
7583
-
7584
- // ../server/dist/services/query-service.js
7585
- function quoteIdentifier(name, dialect) {
7586
- if (dialect === "mysql" || dialect === "bigquery")
7587
- return `\`${name.replace(/`/g, "``")}\``;
7588
- return `"${name.replace(/"/g, '""')}"`;
7589
- }
7590
- var QueryService = class {
7591
- compiler;
7592
- connector;
7593
- cache;
7594
- models;
7595
- refs;
7596
- dialect;
7597
- constructor(config) {
7598
- this.compiler = new QueryCompiler({
7599
- models: config.models,
7600
- refs: config.refs
7601
- });
7602
- this.connector = config.connector;
7603
- this.cache = config.cache;
7604
- this.models = new Map(Object.entries(config.models));
7605
- this.refs = config.refs;
7606
- this.dialect = config.dialect ?? "duckdb";
7696
+ async loadChartsFromDir(dir, baseDir) {
7697
+ const entries = await readdir(dir, { withFileTypes: true });
7698
+ for (const entry of entries) {
7699
+ const fullPath = join2(dir, entry.name);
7700
+ if (entry.isDirectory() && dir === baseDir) {
7701
+ await this.loadChartsFromDir(fullPath, baseDir);
7702
+ } else if (entry.isFile() && (extname(entry.name) === ".yaml" || extname(entry.name) === ".yml")) {
7703
+ const content = await readFile(fullPath, "utf-8");
7704
+ const parsed = parseYaml(content);
7705
+ const result = ChartSchema.safeParse(parsed);
7706
+ if (result.success) {
7707
+ const name = result.data.name;
7708
+ if (this.charts.has(name)) {
7709
+ console.warn(`Duplicate chart name "${name}" in ${relative(baseDir, fullPath)} (already loaded from ${relative(baseDir, this.chartFilePaths.get(name))})`);
7710
+ continue;
7711
+ }
7712
+ this.charts.set(name, result.data);
7713
+ this.chartFilePaths.set(name, fullPath);
7714
+ const relDir = relative(baseDir, dirname(fullPath)).replace(/\\/g, "/");
7715
+ this.chartFolders.set(name, relDir || "");
7716
+ } else {
7717
+ const relPath = relative(baseDir, fullPath);
7718
+ const name = parsed && typeof parsed === "object" && "name" in parsed && typeof parsed.name === "string" ? parsed.name : void 0;
7719
+ const errorMsg = result.error.errors.map((e) => {
7720
+ const path = e.path.length > 0 ? e.path.join(".") : "(root)";
7721
+ return `${path}: ${e.message}`;
7722
+ }).join("; ");
7723
+ console.warn(`Invalid chart file ${relPath}: ${errorMsg}`);
7724
+ if (name) {
7725
+ this.validationErrors.set(name, { file: relPath, message: errorMsg });
7726
+ }
7727
+ }
7728
+ }
7729
+ }
7607
7730
  }
7608
- async executeChart(chart, params, userContext) {
7609
- const compiled = this.compiler.compile(chart, params, userContext);
7610
- const cached = await this.cache.get(compiled.cacheKey);
7611
- if (cached) {
7612
- return {
7613
- columns: cached.columns,
7614
- rows: cached.rows,
7615
- rowCount: cached.rowCount,
7616
- durationMs: cached.durationMs,
7617
- cached: true,
7618
- cacheKey: compiled.cacheKey,
7619
- compiledSql: compiled.sql
7620
- };
7731
+ async loadModels() {
7732
+ const modelsDir = join2(this.projectDir, "models");
7733
+ try {
7734
+ await access(modelsDir);
7735
+ } catch {
7736
+ return;
7621
7737
  }
7622
- const result = await this.connector.execute(compiled.sql);
7623
- const cacheEntry = {
7624
- columns: result.columns,
7625
- rows: result.rows,
7626
- rowCount: result.rowCount,
7627
- durationMs: result.durationMs,
7628
- cachedAt: Date.now()
7629
- };
7630
- await this.cache.set(compiled.cacheKey, cacheEntry);
7631
- return {
7632
- ...result,
7633
- cached: false,
7634
- cacheKey: compiled.cacheKey,
7635
- compiledSql: compiled.sql
7636
- };
7738
+ await this.loadModelsFromDir(modelsDir);
7637
7739
  }
7638
- /**
7639
- * Execute a table chart with server-side pagination.
7640
- * Wraps the compiled SQL with LIMIT/OFFSET and runs a parallel COUNT(*) query.
7641
- * Optionally computes summary aggregations for specified columns.
7642
- */
7643
- async executeChartPaginated(chart, params, options, userContext) {
7644
- const compiled = this.compiler.compile(chart, params, userContext);
7645
- const baseSql = compiled.sql;
7646
- const q = quoteIdentifier;
7647
- const d = this.dialect;
7648
- let orderBy = "";
7649
- if (options.sortField) {
7650
- const cleanField = options.sortField.replace(/[^\w\s]/g, "");
7651
- if (cleanField.length > 0) {
7652
- orderBy = `ORDER BY ${q(cleanField, d)} ${options.sortDirection === "desc" ? "DESC" : "ASC"}`;
7740
+ async loadModelsFromDir(dir) {
7741
+ const entries = await readdir(dir, { withFileTypes: true });
7742
+ for (const entry of entries) {
7743
+ const fullPath = join2(dir, entry.name);
7744
+ if (entry.isDirectory()) {
7745
+ await this.loadModelsFromDir(fullPath);
7746
+ } else if (extname(entry.name) === ".sql") {
7747
+ const content = await readFile(fullPath, "utf-8");
7748
+ try {
7749
+ const parsed = parseModelMetadata(content);
7750
+ this.models.set(parsed.name, {
7751
+ metadata: parsed,
7752
+ sql: parsed.sql,
7753
+ filePath: fullPath
7754
+ });
7755
+ } catch (err) {
7756
+ console.warn(`Invalid model file ${entry.name}: ${err}`);
7757
+ }
7653
7758
  }
7654
7759
  }
7655
- const offset = options.page * options.pageSize;
7656
- const dataSql = `SELECT * FROM (${baseSql}) _t ${orderBy} LIMIT ${options.pageSize} OFFSET ${offset}`;
7657
- const countCacheKey = `${compiled.cacheKey}:count`;
7658
- let totalCount;
7659
- const cachedCount = await this.cache.get(countCacheKey);
7660
- if (cachedCount) {
7661
- totalCount = cachedCount.rowCount;
7662
- } else {
7663
- const countSql = `SELECT COUNT(*) AS _count FROM (${baseSql}) _t`;
7664
- const countResult = await this.connector.execute(countSql);
7665
- totalCount = Number(countResult.rows[0]?._count ?? 0);
7666
- await this.cache.set(countCacheKey, {
7667
- columns: [],
7668
- rows: [],
7669
- rowCount: totalCount,
7670
- durationMs: countResult.durationMs,
7671
- cachedAt: Date.now()
7672
- });
7760
+ }
7761
+ async loadDashboards() {
7762
+ const dashboardsDir = join2(this.projectDir, "dashboards");
7763
+ try {
7764
+ await access(dashboardsDir);
7765
+ } catch {
7766
+ return;
7673
7767
  }
7674
- let summaryValues;
7675
- if (options.summaryColumns && options.summaryColumns.length > 0) {
7676
- const summaryCacheKey = `${compiled.cacheKey}:summary`;
7677
- const cachedSummary = await this.cache.get(summaryCacheKey);
7678
- if (cachedSummary && cachedSummary.rows[0]) {
7679
- summaryValues = cachedSummary.rows[0];
7680
- } else {
7681
- const aggExprs = options.summaryColumns.map(({ field, summary }) => {
7682
- const col = q(field, d);
7683
- switch (summary) {
7684
- case "sum":
7685
- return `SUM(${col}) AS ${q(`_sum_${field}`, d)}`;
7686
- case "avg":
7687
- return `AVG(${col}) AS ${q(`_avg_${field}`, d)}`;
7688
- case "min":
7689
- return `MIN(${col}) AS ${q(`_min_${field}`, d)}`;
7690
- case "max":
7691
- return `MAX(${col}) AS ${q(`_max_${field}`, d)}`;
7692
- case "count":
7693
- return `COUNT(${col}) AS ${q(`_count_${field}`, d)}`;
7694
- default:
7695
- return null;
7768
+ await this.loadDashboardsFromDir(dashboardsDir, dashboardsDir);
7769
+ }
7770
+ async loadDashboardsFromDir(dir, baseDir) {
7771
+ const entries = await readdir(dir, { withFileTypes: true });
7772
+ for (const entry of entries) {
7773
+ const fullPath = join2(dir, entry.name);
7774
+ if (entry.isDirectory() && dir === baseDir) {
7775
+ await this.loadDashboardsFromDir(fullPath, baseDir);
7776
+ } else if (entry.isFile() && (extname(entry.name) === ".yaml" || extname(entry.name) === ".yml")) {
7777
+ const content = await readFile(fullPath, "utf-8");
7778
+ const parsed = parseYaml(content);
7779
+ const result = DashboardSchema.safeParse(parsed);
7780
+ if (result.success) {
7781
+ const name = result.data.name;
7782
+ if (this.dashboards.has(name)) {
7783
+ console.warn(`Duplicate dashboard name "${name}" in ${relative(baseDir, fullPath)} (already loaded from ${relative(baseDir, this.dashboardFilePaths.get(name))})`);
7784
+ continue;
7696
7785
  }
7697
- }).filter(Boolean);
7698
- if (aggExprs.length > 0) {
7699
- const summarySql = `SELECT ${aggExprs.join(", ")} FROM (${baseSql}) _t`;
7700
- const summaryResult = await this.connector.execute(summarySql);
7701
- if (summaryResult.rows[0]) {
7702
- summaryValues = {};
7703
- for (const { field, summary } of options.summaryColumns) {
7704
- const key = `_${summary}_${field}`;
7705
- summaryValues[`${summary}:${field}`] = Number(summaryResult.rows[0][key] ?? 0);
7706
- }
7707
- await this.cache.set(summaryCacheKey, {
7708
- columns: [],
7709
- rows: [summaryValues],
7710
- rowCount: 0,
7711
- durationMs: summaryResult.durationMs,
7712
- cachedAt: Date.now()
7713
- });
7786
+ this.dashboards.set(name, result.data);
7787
+ this.dashboardFilePaths.set(name, fullPath);
7788
+ const relDir = relative(baseDir, dirname(fullPath)).replace(/\\/g, "/");
7789
+ this.dashboardFolders.set(name, relDir || "");
7790
+ } else {
7791
+ const relPath = relative(baseDir, fullPath);
7792
+ const name = parsed && typeof parsed === "object" && "name" in parsed && typeof parsed.name === "string" ? parsed.name : void 0;
7793
+ const errorMsg = result.error.errors.map((e) => {
7794
+ const path = e.path.length > 0 ? e.path.join(".") : "(root)";
7795
+ return `${path}: ${e.message}`;
7796
+ }).join("; ");
7797
+ console.warn(`Invalid dashboard file ${relPath}: ${errorMsg}`);
7798
+ if (name) {
7799
+ this.validationErrors.set(name, { file: relPath, message: errorMsg });
7714
7800
  }
7715
7801
  }
7716
7802
  }
7717
7803
  }
7718
- const result = await this.connector.execute(dataSql);
7719
- return {
7720
- ...result,
7721
- cached: false,
7722
- cacheKey: compiled.cacheKey,
7723
- compiledSql: dataSql,
7724
- totalCount,
7725
- page: options.page,
7726
- pageSize: options.pageSize,
7727
- summaryValues
7728
- };
7729
7804
  }
7730
- /**
7731
- * Execute a chart query for CSV export with a bounded row limit.
7732
- * Returns full QueryResult — caller streams to HTTP response.
7733
- */
7734
- async executeChartForExport(chart, params, maxRows, userContext) {
7735
- const compiled = this.compiler.compile(chart, params, userContext);
7736
- const sql = `SELECT * FROM (${compiled.sql}) _t LIMIT ${maxRows}`;
7737
- const result = await this.connector.execute(sql);
7738
- return {
7739
- ...result,
7740
- cached: false,
7741
- cacheKey: compiled.cacheKey,
7742
- compiledSql: sql
7743
- };
7805
+ async loadSchedules() {
7806
+ const schedulesDir = join2(this.projectDir, "schedules");
7807
+ try {
7808
+ await access(schedulesDir);
7809
+ } catch {
7810
+ return;
7811
+ }
7812
+ const files = await readdir(schedulesDir);
7813
+ for (const file of files) {
7814
+ if (extname(file) !== ".yaml" && extname(file) !== ".yml")
7815
+ continue;
7816
+ const filePath = join2(schedulesDir, file);
7817
+ const content = await readFile(filePath, "utf-8");
7818
+ const parsed = parseYaml(content);
7819
+ const result = ScheduleSchema.safeParse(parsed);
7820
+ if (result.success) {
7821
+ this.schedules.set(result.data.name, result.data);
7822
+ } else {
7823
+ console.warn(`Invalid schedule file ${file}: ${result.error.message}`);
7824
+ }
7825
+ }
7826
+ }
7827
+ async loadEvents() {
7828
+ const eventsPath = join2(this.projectDir, "events.yaml");
7829
+ try {
7830
+ await access(eventsPath);
7831
+ } catch {
7832
+ return;
7833
+ }
7834
+ const content = await readFile(eventsPath, "utf-8");
7835
+ const parsed = parseYaml(content);
7836
+ const result = EventsFileSchema.safeParse(parsed);
7837
+ if (result.success) {
7838
+ this.events = result.data.events;
7839
+ } else {
7840
+ console.warn(`Invalid events.yaml: ${result.error.message}`);
7841
+ }
7842
+ }
7843
+ async loadAgents() {
7844
+ const agentsDir = join2(this.projectDir, "agents");
7845
+ this.agentRegistry.clearProjectAgents();
7846
+ try {
7847
+ const files = await readdir(agentsDir);
7848
+ for (const file of files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"))) {
7849
+ const content = await readFile(join2(agentsDir, file), "utf-8");
7850
+ this.agentRegistry.loadFromYaml(content, `agents/${file}`, (msg) => {
7851
+ console.warn(msg);
7852
+ });
7853
+ }
7854
+ } catch {
7855
+ }
7856
+ }
7857
+ startWatching() {
7858
+ if (this.watcher)
7859
+ return;
7860
+ const watchPaths = [
7861
+ join2(this.projectDir, "yamchart.yaml"),
7862
+ join2(this.projectDir, "yamchart.local.yaml"),
7863
+ join2(this.projectDir, "connections"),
7864
+ join2(this.projectDir, "charts"),
7865
+ join2(this.projectDir, "models"),
7866
+ join2(this.projectDir, "dashboards"),
7867
+ join2(this.projectDir, "schedules"),
7868
+ join2(this.projectDir, "events.yaml"),
7869
+ join2(this.projectDir, "agents")
7870
+ ];
7871
+ this.watcher = watch(watchPaths, {
7872
+ ignored: /(^|[\/\\])\../,
7873
+ // Ignore dotfiles
7874
+ persistent: true,
7875
+ ignoreInitial: true,
7876
+ depth: 1
7877
+ // Only watch one level deep in subdirectories
7878
+ });
7879
+ this.watcher.on("change", async (path) => {
7880
+ console.log(`File changed: ${path}`);
7881
+ await this.reload();
7882
+ });
7883
+ this.watcher.on("add", async (path) => {
7884
+ console.log(`File added: ${path}`);
7885
+ await this.reload();
7886
+ });
7887
+ this.watcher.on("unlink", async (path) => {
7888
+ console.log(`File removed: ${path}`);
7889
+ await this.reload();
7890
+ });
7891
+ this.watcher.on("error", (err) => {
7892
+ const code = err.code;
7893
+ if (code === "EMFILE") {
7894
+ console.warn(`Warning: Too many open files for file watching. The server will continue running but won't auto-reload on file changes.
7895
+ Fix: run "ulimit -n 10240" before starting yamchart dev, or add it to your shell profile.`);
7896
+ this.watcher?.close();
7897
+ this.watcher = null;
7898
+ } else {
7899
+ console.error("File watcher error:", err);
7900
+ }
7901
+ });
7902
+ }
7903
+ async reload() {
7904
+ try {
7905
+ await this.load();
7906
+ for (const callback of this.onChangeCallbacks) {
7907
+ callback();
7908
+ }
7909
+ } catch (err) {
7910
+ console.error("Error reloading config:", err);
7911
+ }
7912
+ }
7913
+ onChange(callback) {
7914
+ this.onChangeCallbacks.push(callback);
7744
7915
  }
7745
- /**
7746
- * Compile a chart query without executing it.
7747
- * Returns the compiled SQL and cache key.
7748
- */
7749
- compileChart(chart, params, userContext) {
7750
- return this.compiler.compile(chart, params, userContext);
7916
+ async stop() {
7917
+ if (this.watcher) {
7918
+ await this.watcher.close();
7919
+ this.watcher = null;
7920
+ }
7751
7921
  }
7752
- invalidateChart(chartName) {
7753
- this.cache.invalidatePattern(`${chartName}:*`);
7922
+ getProject() {
7923
+ if (!this.project) {
7924
+ throw new Error("Config not loaded");
7925
+ }
7926
+ return this.project;
7754
7927
  }
7755
- invalidateAll() {
7756
- this.cache.clear();
7928
+ getConnections() {
7929
+ return Array.from(this.connections.values());
7757
7930
  }
7758
- /**
7759
- * Update the compiler with new models and refs.
7760
- * Called when config is reloaded.
7761
- */
7762
- updateCompiler(config) {
7763
- this.compiler = new QueryCompiler({
7764
- models: config.models,
7765
- refs: config.refs
7766
- });
7767
- this.models = new Map(Object.entries(config.models));
7768
- this.refs = config.refs;
7931
+ getConnectionByName(name) {
7932
+ return this.connections.get(name);
7769
7933
  }
7770
- getCacheStats() {
7771
- return this.cache.getStats();
7934
+ /** Merge a runtime config layer over the Git-loaded config. Call after load(). */
7935
+ applyRuntimeOverrides(rc) {
7936
+ for (const rconn of rc.connections ?? []) {
7937
+ const { secrets, ...rest } = rconn;
7938
+ this.connections.set(rconn.name, rest);
7939
+ if (secrets)
7940
+ this.connectionSecrets.set(rconn.name, secrets);
7941
+ }
7942
+ this.runtimeKnowledgeUrl = rc.knowledge?.database_url;
7943
+ this.runtimeAi = rc.ai;
7772
7944
  }
7773
- async executeRawSql(sql) {
7774
- return this.connector.execute(sql);
7945
+ getConnectionSecrets(name) {
7946
+ return this.connectionSecrets.get(name);
7775
7947
  }
7776
- async executeModel(modelName, params, userContext, limit2 = 1e3, filters) {
7777
- const model = this.models.get(modelName);
7778
- if (!model) {
7779
- throw new Error(`Unknown model: ${modelName}`);
7780
- }
7781
- const context = createTemplateContext(params, this.refs, userContext);
7782
- let sql = renderTemplate(model.sql, context);
7783
- if (filters && Object.keys(filters).length > 0) {
7784
- const clauses = Object.entries(filters).map(([field, value]) => `${field} = '${value.replace(/'/g, "''")}'`);
7785
- sql = `SELECT * FROM (${sql}) _detail WHERE ${clauses.join(" AND ")} LIMIT ${limit2}`;
7786
- } else {
7787
- sql = `SELECT * FROM (${sql}) _detail LIMIT ${limit2}`;
7788
- }
7789
- return this.connector.execute(sql);
7948
+ /** Knowledge DB URL: runtime override wins, else the named env var. */
7949
+ getKnowledgeDbUrl(envVarName, env) {
7950
+ return this.runtimeKnowledgeUrl ?? env[envVarName];
7790
7951
  }
7791
- async executeParameterOptions(modelName, valueField, labelField) {
7792
- const model = this.models.get(modelName);
7793
- if (!model) {
7794
- throw new Error(`Unknown model: ${modelName}`);
7795
- }
7796
- const context = createTemplateContext({}, this.refs);
7797
- const sql = renderTemplate(model.sql, context);
7798
- const result = await this.connector.execute(sql);
7799
- return result.rows.map((row) => ({
7800
- value: String(row[valueField] ?? ""),
7801
- label: String(row[labelField] ?? row[valueField] ?? "")
7802
- }));
7952
+ /** Effective AI config: Git project.ai with the runtime ai deep-merged over it. */
7953
+ getAiConfig() {
7954
+ const base2 = this.project?.ai;
7955
+ if (!this.runtimeAi)
7956
+ return base2;
7957
+ return deepMerge(base2 ?? {}, this.runtimeAi);
7803
7958
  }
7804
- };
7805
-
7806
- // ../server/dist/services/connector-factory.js
7807
- import { join as join2 } from "path";
7808
- function createConnectorFromConfig(connection, options) {
7809
- const { projectDir } = options;
7810
- switch (connection.type) {
7811
- case "duckdb": {
7812
- const duckdbConfig = connection;
7813
- const dbPath = duckdbConfig.config.path.startsWith("/") ? duckdbConfig.config.path : join2(projectDir, duckdbConfig.config.path);
7814
- return new DuckDBConnector({ path: dbPath });
7815
- }
7816
- case "postgres": {
7817
- const pgConnection = connection;
7818
- const credentials = resolvePostgresAuth(pgConnection);
7819
- return new PostgresConnector({
7820
- host: pgConnection.config.host,
7821
- port: pgConnection.config.port,
7822
- database: pgConnection.config.database,
7823
- schema: pgConnection.config.schema,
7824
- ssl: pgConnection.config.ssl,
7825
- user: credentials.user,
7826
- password: credentials.password,
7827
- min: pgConnection.pool?.min_connections,
7828
- max: pgConnection.pool?.max_connections,
7829
- idleTimeoutMillis: pgConnection.pool?.idle_timeout,
7830
- statementTimeout: pgConnection.query?.timeout
7831
- });
7832
- }
7833
- case "mysql": {
7834
- const mysqlConnection = connection;
7835
- const credentials = resolveMySQLAuth(mysqlConnection);
7836
- return new MySQLConnector({
7837
- host: mysqlConnection.config.host,
7838
- port: mysqlConnection.config.port,
7839
- database: mysqlConnection.config.database,
7840
- ssl: mysqlConnection.config.ssl,
7841
- user: credentials.user,
7842
- password: credentials.password,
7843
- min: mysqlConnection.pool?.min_connections,
7844
- max: mysqlConnection.pool?.max_connections,
7845
- idleTimeoutMillis: mysqlConnection.pool?.idle_timeout,
7846
- statementTimeout: mysqlConnection.query?.timeout
7847
- });
7848
- }
7849
- case "sqlite": {
7850
- const sqliteConnection = connection;
7851
- const dbPath = sqliteConnection.config.path.startsWith("/") ? sqliteConnection.config.path : sqliteConnection.config.path === ":memory:" ? ":memory:" : join2(projectDir, sqliteConnection.config.path);
7852
- return new SQLiteConnector({ path: dbPath });
7853
- }
7854
- case "snowflake": {
7855
- const sfConnection = connection;
7856
- const credentials = resolveSnowflakeAuth(sfConnection);
7857
- return new SnowflakeConnector({
7858
- account: sfConnection.config.account,
7859
- username: credentials.username,
7860
- password: credentials.password,
7861
- privateKey: credentials.privateKey,
7862
- authenticator: credentials.authenticator,
7863
- clientStoreTemporaryCredential: credentials.clientStoreTemporaryCredential,
7864
- warehouse: sfConnection.config.warehouse,
7865
- database: sfConnection.config.database,
7866
- schema: sfConnection.config.schema,
7867
- role: sfConnection.config.role,
7868
- statementTimeout: sfConnection.query?.timeout
7869
- });
7870
- }
7871
- case "bigquery": {
7872
- const bqConnection = connection;
7873
- const creds = resolveBigQueryAuth(bqConnection);
7874
- const keyFilename = creds.keyFilename ? creds.keyFilename.startsWith("/") ? creds.keyFilename : join2(projectDir, creds.keyFilename) : void 0;
7875
- return new BigQueryConnector({
7876
- projectId: bqConnection.config.project_id,
7877
- dataset: bqConnection.config.dataset,
7878
- location: bqConnection.config.location,
7879
- maxBytesBilled: bqConnection.config.max_bytes_billed,
7880
- credentials: creds.credentials,
7881
- keyFilename
7882
- });
7883
- }
7884
- default:
7885
- throw new Error(`Unsupported connection type: ${connection.type}`);
7959
+ getCharts() {
7960
+ return Array.from(this.charts.values());
7886
7961
  }
7887
- }
7888
- function buildConnectionInfo(connection) {
7889
- const info2 = {
7890
- name: connection.name,
7891
- type: connection.type
7892
- };
7893
- switch (connection.type) {
7894
- case "duckdb": {
7895
- const c = connection;
7896
- info2.path = c.config.path;
7897
- break;
7898
- }
7899
- case "postgres": {
7900
- const c = connection;
7901
- info2.host = c.config.host;
7902
- info2.port = c.config.port;
7903
- info2.database = c.config.database;
7904
- info2.schema = c.config.schema;
7905
- break;
7962
+ getChartByName(name) {
7963
+ return this.charts.get(name);
7964
+ }
7965
+ getChartFolder(name) {
7966
+ return this.chartFolders.get(name) ?? "";
7967
+ }
7968
+ getChartFilePath(name) {
7969
+ return this.chartFilePaths.get(name);
7970
+ }
7971
+ getModels() {
7972
+ return Array.from(this.models.values());
7973
+ }
7974
+ getModelByName(name) {
7975
+ return this.models.get(name);
7976
+ }
7977
+ getDefaultConnection() {
7978
+ const envConnection = process.env.YAMCHART_CONNECTION;
7979
+ if (envConnection) {
7980
+ return this.connections.get(envConnection);
7906
7981
  }
7907
- case "mysql": {
7908
- const c = connection;
7909
- info2.host = c.config.host;
7910
- info2.port = c.config.port;
7911
- info2.database = c.config.database;
7912
- break;
7982
+ const defaultName = this.project?.defaults?.connection;
7983
+ if (defaultName) {
7984
+ return this.connections.get(defaultName);
7913
7985
  }
7914
- case "sqlite": {
7915
- const c = connection;
7916
- info2.path = c.config.path;
7917
- break;
7986
+ return this.connections.values().next().value;
7987
+ }
7988
+ getDashboards() {
7989
+ return Array.from(this.dashboards.values());
7990
+ }
7991
+ getDashboardByName(name) {
7992
+ return this.dashboards.get(name);
7993
+ }
7994
+ getDashboardFolder(name) {
7995
+ return this.dashboardFolders.get(name) ?? "";
7996
+ }
7997
+ getDashboardFilePath(name) {
7998
+ return this.dashboardFilePaths.get(name);
7999
+ }
8000
+ getSchedules() {
8001
+ return Array.from(this.schedules.values());
8002
+ }
8003
+ getEvents() {
8004
+ return this.events;
8005
+ }
8006
+ getProjectDir() {
8007
+ return this.projectDir;
8008
+ }
8009
+ getValidationError(name) {
8010
+ return this.validationErrors.get(name);
8011
+ }
8012
+ getValidationErrors() {
8013
+ return this.validationErrors;
8014
+ }
8015
+ getAgentRegistry() {
8016
+ return this.agentRegistry;
8017
+ }
8018
+ };
8019
+
8020
+ // ../server/dist/services/runtime-config-store.js
8021
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
8022
+ import { dirname as dirname3, join as join4, isAbsolute } from "path";
8023
+
8024
+ // ../../packages/crypto/dist/index.js
8025
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
8026
+ var ALGO = "aes-256-gcm";
8027
+ function deriveKeyFromEnv(env) {
8028
+ const b64 = env.YAMCHART_CONFIG_SECRET;
8029
+ if (!b64)
8030
+ return null;
8031
+ const key = Buffer.from(b64, "base64");
8032
+ if (key.length !== 32) {
8033
+ throw new Error("YAMCHART_CONFIG_SECRET must be a base64-encoded 32-byte key");
8034
+ }
8035
+ return key;
8036
+ }
8037
+ function encryptSecret(plain, key) {
8038
+ const iv = randomBytes(12);
8039
+ const cipher = createCipheriv(ALGO, key, iv);
8040
+ const ct = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
8041
+ const tag = cipher.getAuthTag();
8042
+ return `${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
8043
+ }
8044
+ function decryptSecret(enc, key) {
8045
+ const parts = enc.split(":");
8046
+ if (parts.length !== 3)
8047
+ throw new Error("malformed ciphertext");
8048
+ const ivB64 = parts[0];
8049
+ const tagB64 = parts[1];
8050
+ const ctB64 = parts[2];
8051
+ const decipher = createDecipheriv(ALGO, key, Buffer.from(ivB64, "base64"));
8052
+ decipher.setAuthTag(Buffer.from(tagB64, "base64"));
8053
+ return Buffer.concat([decipher.update(Buffer.from(ctB64, "base64")), decipher.final()]).toString("utf8");
8054
+ }
8055
+ function maskSecret(plain) {
8056
+ if (plain.length <= 4)
8057
+ return "\u2022\u2022\u2022\u2022";
8058
+ return "\u2022\u2022\u2022\u2022" + plain.slice(-4);
8059
+ }
8060
+
8061
+ // ../server/dist/services/secret-crypto.js
8062
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
8063
+ import { dirname as dirname2, join as join3 } from "path";
8064
+ import { randomBytes as randomBytes2 } from "crypto";
8065
+ function getConfigKey(dataDir, env, log) {
8066
+ const fromEnv = deriveKeyFromEnv(env);
8067
+ if (fromEnv)
8068
+ return fromEnv;
8069
+ const keyPath = join3(dataDir, ".config-secret");
8070
+ if (existsSync(keyPath)) {
8071
+ const key2 = Buffer.from(readFileSync(keyPath, "utf8").trim(), "base64");
8072
+ if (key2.length !== 32) {
8073
+ throw new Error(`Key file at ${keyPath} does not contain a 32-byte key (got ${key2.length} bytes). Delete it to regenerate.`);
8074
+ }
8075
+ return key2;
8076
+ }
8077
+ mkdirSync(dirname2(keyPath), { recursive: true });
8078
+ const key = randomBytes2(32);
8079
+ writeFileSync(keyPath, key.toString("base64"), { mode: 384 });
8080
+ 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.`);
8081
+ return key;
8082
+ }
8083
+
8084
+ // ../server/dist/services/runtime-config-store.js
8085
+ var RuntimeConfigError = class extends Error {
8086
+ };
8087
+ var SECRET_LEAF_KEYS = /* @__PURE__ */ new Set(["password", "private_key", "credentials_json", "database_url", "api_key"]);
8088
+ function transformSecretLeaves(obj, fn) {
8089
+ if (Array.isArray(obj))
8090
+ return obj.map((v) => transformSecretLeaves(v, fn));
8091
+ if (obj && typeof obj === "object") {
8092
+ const out = {};
8093
+ for (const [k, v] of Object.entries(obj)) {
8094
+ out[k] = SECRET_LEAF_KEYS.has(k) && typeof v === "string" ? fn(v) : transformSecretLeaves(v, fn);
7918
8095
  }
7919
- case "snowflake": {
7920
- const c = connection;
7921
- info2.account = c.config.account;
7922
- info2.warehouse = c.config.warehouse;
7923
- info2.database = c.config.database;
7924
- info2.schema = c.config.schema;
7925
- info2.role = c.config.role;
7926
- break;
8096
+ return out;
8097
+ }
8098
+ return obj;
8099
+ }
8100
+ var RuntimeConfigStore = class {
8101
+ filePath;
8102
+ key;
8103
+ constructor(filePath, key) {
8104
+ this.filePath = filePath;
8105
+ this.key = key;
8106
+ }
8107
+ /** Directory the store (and key file) live in — useful for getConfigKey(). */
8108
+ get dataDir() {
8109
+ return dirname3(this.filePath);
8110
+ }
8111
+ static resolvePath(opts) {
8112
+ if (opts.env.YAMCHART_RUNTIME_CONFIG)
8113
+ return opts.env.YAMCHART_RUNTIME_CONFIG;
8114
+ if (opts.authDbPath && isAbsolute(opts.authDbPath))
8115
+ return join4(dirname3(opts.authDbPath), "runtime-config.json");
8116
+ return join4(opts.projectDir, ".yamchart", "runtime-config.json");
8117
+ }
8118
+ read() {
8119
+ if (!existsSync2(this.filePath))
8120
+ return { version: 1 };
8121
+ let parsed;
8122
+ try {
8123
+ parsed = JSON.parse(readFileSync2(this.filePath, "utf8"));
8124
+ } catch (err) {
8125
+ throw new RuntimeConfigError(`runtime-config.json is not valid JSON: ${err.message}`);
7927
8126
  }
7928
- case "bigquery": {
7929
- const c = connection;
7930
- info2.database = c.config.project_id;
7931
- info2.schema = c.config.dataset;
7932
- break;
8127
+ let decrypted;
8128
+ try {
8129
+ decrypted = transformSecretLeaves(parsed, (s) => decryptSecret(s, this.key));
8130
+ } catch (err) {
8131
+ throw new RuntimeConfigError(`Could not decrypt runtime-config.json (wrong YAMCHART_CONFIG_SECRET?): ${err.message}`);
7933
8132
  }
8133
+ const result = RuntimeConfigSchema.safeParse(decrypted);
8134
+ if (!result.success)
8135
+ throw new RuntimeConfigError(`Invalid runtime-config.json: ${result.error.message}`);
8136
+ return result.data;
7934
8137
  }
7935
- return info2;
8138
+ write(config) {
8139
+ const validated = RuntimeConfigSchema.parse(config);
8140
+ const encrypted = transformSecretLeaves(validated, (s) => encryptSecret(s, this.key));
8141
+ mkdirSync2(dirname3(this.filePath), { recursive: true, mode: 448 });
8142
+ writeFileSync2(this.filePath, JSON.stringify(encrypted, null, 2), { mode: 384 });
8143
+ }
8144
+ };
8145
+
8146
+ // ../server/dist/services/restart.js
8147
+ function requestRestart(env, exit = (c) => process.exit(c), schedule = (fn, ms) => {
8148
+ setTimeout(fn, ms).unref?.();
8149
+ }) {
8150
+ if (env.YAMCHART_DEV_CHILD === "1") {
8151
+ schedule(() => exit(75), 150);
8152
+ return { applying: true, restartRequired: false };
8153
+ }
8154
+ return { applying: false, restartRequired: true };
7936
8155
  }
7937
- async function testConnection(connection, options) {
7938
- const start = performance.now();
7939
- let connector = null;
7940
- try {
7941
- connector = createConnectorFromConfig(connection, options);
7942
- await connector.connect();
7943
- const healthQuery = connection.type === "duckdb" ? "SELECT 1 AS health" : "SELECT 1";
7944
- await connector.execute(healthQuery);
7945
- const latencyMs = performance.now() - start;
7946
- return {
7947
- name: connection.name,
7948
- type: connection.type,
7949
- status: "healthy",
7950
- latencyMs: Math.round(latencyMs)
7951
- };
7952
- } catch (err) {
7953
- const latencyMs = performance.now() - start;
7954
- let errorMessage = "Unknown error";
7955
- if (err instanceof Error) {
7956
- const pgError = err;
7957
- errorMessage = err.message || pgError.code || "Connection failed";
7958
- } else {
7959
- errorMessage = String(err);
7960
- }
7961
- return {
7962
- name: connection.name,
7963
- type: connection.type,
7964
- status: "unhealthy",
7965
- latencyMs: Math.round(latencyMs),
7966
- error: errorMessage
7967
- };
7968
- } finally {
7969
- if (connector) {
7970
- try {
7971
- await connector.disconnect();
7972
- } catch {
7973
- }
7974
- }
8156
+
8157
+ // ../server/dist/services/connection-refs.js
8158
+ function findConnectionRefs(connectionName, input) {
8159
+ const refs = [];
8160
+ const resolves = (explicit) => (explicit ?? input.defaultConnection) === connectionName;
8161
+ for (const c of input.charts) {
8162
+ if (resolves(c.source?.connection))
8163
+ refs.push({ kind: "chart", name: c.name });
7975
8164
  }
8165
+ for (const m of input.models) {
8166
+ if (resolves(m.metadata.connection))
8167
+ refs.push({ kind: "model", name: m.metadata.name });
8168
+ }
8169
+ return refs;
7976
8170
  }
7977
8171
 
7978
8172
  // ../server/dist/routes/config.js
@@ -8004,7 +8198,10 @@ async function configRoutes(fastify, options) {
8004
8198
  });
8005
8199
  fastify.get("/api/connections/status", async () => {
8006
8200
  const connections = configLoader.getConnections();
8007
- const results = await Promise.all(connections.map((conn) => testConnection(conn, { projectDir: options.projectDir })));
8201
+ const results = await Promise.all(connections.map((conn) => testConnection(conn, {
8202
+ projectDir: options.projectDir,
8203
+ secrets: options.getConnectionSecrets?.(conn.name)
8204
+ })));
8008
8205
  return { connections: results };
8009
8206
  });
8010
8207
  fastify.get("/api/connections/:name/status", async (request, reply) => {
@@ -8013,7 +8210,10 @@ async function configRoutes(fastify, options) {
8013
8210
  if (!connection) {
8014
8211
  return reply.status(404).send({ error: `Connection not found: ${name}` });
8015
8212
  }
8016
- const result = await testConnection(connection, { projectDir: options.projectDir });
8213
+ const result = await testConnection(connection, {
8214
+ projectDir: options.projectDir,
8215
+ secrets: options.getConnectionSecrets?.(connection.name)
8216
+ });
8017
8217
  return result;
8018
8218
  });
8019
8219
  fastify.get("/api/cache/stats", async () => {
@@ -8492,10 +8692,10 @@ async function chartRoutes(fastify, options) {
8492
8692
 
8493
8693
  // ../server/dist/routes/chart-events.js
8494
8694
  import { readFile as readFile2, writeFile } from "fs/promises";
8495
- import { join as join3 } from "path";
8695
+ import { join as join5 } from "path";
8496
8696
  import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
8497
8697
  async function readEventsFile(projectDir) {
8498
- const eventsPath = join3(projectDir, "events.yaml");
8698
+ const eventsPath = join5(projectDir, "events.yaml");
8499
8699
  try {
8500
8700
  const content = await readFile2(eventsPath, "utf-8");
8501
8701
  const parsed = parseYaml2(content);
@@ -8506,7 +8706,7 @@ async function readEventsFile(projectDir) {
8506
8706
  }
8507
8707
  }
8508
8708
  async function writeEventsFile(projectDir, events) {
8509
- const eventsPath = join3(projectDir, "events.yaml");
8709
+ const eventsPath = join5(projectDir, "events.yaml");
8510
8710
  await writeFile(eventsPath, stringifyYaml({ events }));
8511
8711
  }
8512
8712
  async function chartEventsRoutes(fastify, options) {
@@ -8555,7 +8755,7 @@ async function chartEventsRoutes(fastify, options) {
8555
8755
 
8556
8756
  // ../server/dist/routes/dashboards.js
8557
8757
  import { writeFile as writeFile2 } from "fs/promises";
8558
- import { join as join4 } from "path";
8758
+ import { join as join6 } from "path";
8559
8759
  import { stringify as stringifyYaml2 } from "yaml";
8560
8760
  function classifyError2(error2) {
8561
8761
  const msg = error2 instanceof Error ? error2.message : String(error2);
@@ -8634,7 +8834,7 @@ async function dashboardRoutes(fastify, options) {
8634
8834
  }
8635
8835
  updated = { ...dashboard, layout };
8636
8836
  }
8637
- const filePath = configLoader.getDashboardFilePath(id) ?? join4(projectDir, "dashboards", `${id}.yaml`);
8837
+ const filePath = configLoader.getDashboardFilePath(id) ?? join6(projectDir, "dashboards", `${id}.yaml`);
8638
8838
  await writeFile2(filePath, stringifyYaml2(updated));
8639
8839
  await configLoader.load();
8640
8840
  const commitMessage = message || `Update ${dashboard.title} layout`;
@@ -8830,11 +9030,11 @@ async function semanticRoutes(fastify, options) {
8830
9030
  errorType: "validation_error"
8831
9031
  });
8832
9032
  }
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");
9033
+ const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync3 } = await import("fs");
9034
+ const { join: join15, dirname: dirname5 } = await import("path");
9035
+ const fullPath = join15(projectDir, filePath);
9036
+ mkdirSync3(dirname5(fullPath), { recursive: true });
9037
+ writeFileSync3(fullPath, sql, "utf-8");
8838
9038
  return { saved: true, path: filePath };
8839
9039
  });
8840
9040
  fastify.post("/api/semantic/rebuild", async () => {
@@ -8849,8 +9049,8 @@ async function semanticRoutes(fastify, options) {
8849
9049
  }
8850
9050
 
8851
9051
  // ../server/dist/services/semantic-service.js
8852
- import { readFileSync, existsSync } from "fs";
8853
- import { join as join5 } from "path";
9052
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
9053
+ import { join as join7 } from "path";
8854
9054
  var SemanticService = class {
8855
9055
  model = null;
8856
9056
  builder;
@@ -8867,10 +9067,10 @@ var SemanticService = class {
8867
9067
  this.model = this.builder.build(input);
8868
9068
  }
8869
9069
  buildFromDisk(models, chartHints) {
8870
- const catalogPath = join5(this.projectDir, ".yamchart", "catalog.json");
9070
+ const catalogPath = join7(this.projectDir, ".yamchart", "catalog.json");
8871
9071
  let catalog;
8872
- if (existsSync(catalogPath)) {
8873
- const raw = readFileSync(catalogPath, "utf-8");
9072
+ if (existsSync3(catalogPath)) {
9073
+ const raw = readFileSync3(catalogPath, "utf-8");
8874
9074
  catalog = JSON.parse(raw);
8875
9075
  }
8876
9076
  this.build({ catalog, models, chartHints });
@@ -8945,7 +9145,7 @@ var SlackNotifier = class {
8945
9145
  let response = await fetch(webhookUrl, options);
8946
9146
  if (!response.ok) {
8947
9147
  if (this.retryDelayMs > 0) {
8948
- await new Promise((resolve3) => setTimeout(resolve3, this.retryDelayMs));
9148
+ await new Promise((resolve4) => setTimeout(resolve4, this.retryDelayMs));
8949
9149
  }
8950
9150
  response = await fetch(webhookUrl, options);
8951
9151
  if (!response.ok) {
@@ -9190,6 +9390,14 @@ var GitService = class {
9190
9390
  }
9191
9391
  };
9192
9392
 
9393
+ // ../server/dist/services/ai-error-redact.js
9394
+ function redactProviderError(msg, apiKey) {
9395
+ let out = msg.replace(/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, "[url redacted]");
9396
+ if (apiKey)
9397
+ out = out.split(apiKey).join("[redacted]");
9398
+ return out;
9399
+ }
9400
+
9193
9401
  // ../../packages/knowledge/dist/memory-store.js
9194
9402
  import { randomUUID } from "crypto";
9195
9403
 
@@ -9253,6 +9461,8 @@ function rowToBlindspot(r) {
9253
9461
  confidence: r.confidence ?? void 0,
9254
9462
  status: r.status,
9255
9463
  answer: r.answer ?? void 0,
9464
+ prUrl: r.pr_url ?? void 0,
9465
+ resolvedByKnowledge: r.resolved_by_knowledge ?? false,
9256
9466
  createdAt: new Date(r.created_at).toISOString(),
9257
9467
  updatedAt: new Date(r.updated_at).toISOString()
9258
9468
  };
@@ -9280,6 +9490,7 @@ function rowToHunch(r) {
9280
9490
  rationale: r.rationale ?? void 0,
9281
9491
  confidence: r.confidence ?? void 0,
9282
9492
  lastRunSample: r.last_run_sample ?? void 0,
9493
+ autoConfirmed: r.auto_confirmed ?? false,
9283
9494
  createdAt: new Date(r.created_at).toISOString(),
9284
9495
  updatedAt: new Date(r.updated_at).toISOString()
9285
9496
  };
@@ -9399,6 +9610,14 @@ var PostgresKnowledgeStore = class {
9399
9610
  params.push(patch.answer);
9400
9611
  sets.push(`answer = $${params.length}`);
9401
9612
  }
9613
+ if (patch.resolvedByKnowledge !== void 0) {
9614
+ params.push(patch.resolvedByKnowledge);
9615
+ sets.push(`resolved_by_knowledge = $${params.length}`);
9616
+ }
9617
+ if (patch.prUrl !== void 0) {
9618
+ params.push(patch.prUrl);
9619
+ sets.push(`pr_url = $${params.length}`);
9620
+ }
9402
9621
  params.push((/* @__PURE__ */ new Date()).toISOString());
9403
9622
  sets.push(`updated_at = $${params.length}`);
9404
9623
  params.push(id);
@@ -9474,6 +9693,65 @@ var PostgresKnowledgeStore = class {
9474
9693
  const r = rows[0];
9475
9694
  return { id: r.id, connection: r.connection, source_kind: r.source_kind, source_id: r.source_id, text: r.text, vector: r.vector };
9476
9695
  }
9696
+ async exportAll() {
9697
+ const profilesRes = await this.pool.query(`SELECT connection, profile FROM source_profiles`);
9698
+ return {
9699
+ profiles: profilesRes.rows.map((r) => ({ connection: r.connection, profile: r.profile })),
9700
+ hunches: await this.listHunches({}),
9701
+ blindspots: await this.listBlindspots({}),
9702
+ knowledgeDocs: await this.listKnowledgeDocs({}),
9703
+ embeddings: await this.listEmbeddings({})
9704
+ };
9705
+ }
9706
+ async importAll(snapshot) {
9707
+ for (const p of snapshot.profiles)
9708
+ await this.saveProfile(p.connection, p.profile);
9709
+ for (const h of snapshot.hunches) {
9710
+ 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)
9711
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT (id) DO NOTHING`, [
9712
+ h.id,
9713
+ h.connection,
9714
+ h.question,
9715
+ h.sql,
9716
+ h.generatedSql,
9717
+ h.status,
9718
+ JSON.stringify(h.tablesUsed ?? []),
9719
+ h.rationale ?? null,
9720
+ h.confidence ?? null,
9721
+ h.lastRunSample ? JSON.stringify(h.lastRunSample) : null,
9722
+ h.createdAt,
9723
+ h.updatedAt
9724
+ ]);
9725
+ }
9726
+ for (const b of snapshot.blindspots) {
9727
+ await this.pool.query(`INSERT INTO blindspots (id, connection, kind, title, detail, subject, suggested_model, confidence, status, answer, resolved_by_knowledge, pr_url, created_at, updated_at)
9728
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT (id) DO NOTHING`, [
9729
+ b.id,
9730
+ b.connection,
9731
+ b.kind,
9732
+ b.title,
9733
+ b.detail,
9734
+ b.subject ?? null,
9735
+ JSON.stringify(b.suggestedModel ?? null),
9736
+ b.confidence ?? null,
9737
+ b.status,
9738
+ b.answer ?? null,
9739
+ b.resolvedByKnowledge ?? false,
9740
+ b.prUrl ?? null,
9741
+ b.createdAt,
9742
+ b.updatedAt
9743
+ ]);
9744
+ }
9745
+ for (const d of snapshot.knowledgeDocs) {
9746
+ await this.pool.query(`INSERT INTO knowledge_docs (id, connection, title, content, source, created_at, updated_at)
9747
+ 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]);
9748
+ }
9749
+ for (const e of snapshot.embeddings)
9750
+ await this.upsertEmbedding(e);
9751
+ }
9752
+ async close() {
9753
+ await this.pool.end();
9754
+ }
9477
9755
  };
9478
9756
 
9479
9757
  // ../../node_modules/.pnpm/tslib@2.8.1/node_modules/tslib/tslib.es6.mjs
@@ -9490,11 +9768,11 @@ function __rest(s, e) {
9490
9768
  }
9491
9769
  function __awaiter(thisArg, _arguments, P, generator) {
9492
9770
  function adopt(value) {
9493
- return value instanceof P ? value : new P(function(resolve3) {
9494
- resolve3(value);
9771
+ return value instanceof P ? value : new P(function(resolve4) {
9772
+ resolve4(value);
9495
9773
  });
9496
9774
  }
9497
- return new (P || (P = Promise))(function(resolve3, reject) {
9775
+ return new (P || (P = Promise))(function(resolve4, reject) {
9498
9776
  function fulfilled(value) {
9499
9777
  try {
9500
9778
  step(generator.next(value));
@@ -9510,7 +9788,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
9510
9788
  }
9511
9789
  }
9512
9790
  function step(result) {
9513
- result.done ? resolve3(result.value) : adopt(result.value).then(fulfilled, rejected);
9791
+ result.done ? resolve4(result.value) : adopt(result.value).then(fulfilled, rejected);
9514
9792
  }
9515
9793
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9516
9794
  });
@@ -12041,15 +12319,15 @@ var RealtimeChannel = class _RealtimeChannel {
12041
12319
  }
12042
12320
  }
12043
12321
  } else {
12044
- return new Promise((resolve3) => {
12322
+ return new Promise((resolve4) => {
12045
12323
  var _a2, _b2, _c;
12046
12324
  const push = this._push(args.type, args, opts.timeout || this.timeout);
12047
12325
  if (args.type === "broadcast" && !((_c = (_b2 = (_a2 = this.params) === null || _a2 === void 0 ? void 0 : _a2.config) === null || _b2 === void 0 ? void 0 : _b2.broadcast) === null || _c === void 0 ? void 0 : _c.ack)) {
12048
- resolve3("ok");
12326
+ resolve4("ok");
12049
12327
  }
12050
- push.receive("ok", () => resolve3("ok"));
12051
- push.receive("error", () => resolve3("error"));
12052
- push.receive("timeout", () => resolve3("timed out"));
12328
+ push.receive("ok", () => resolve4("ok"));
12329
+ push.receive("error", () => resolve4("error"));
12330
+ push.receive("timeout", () => resolve4("timed out"));
12053
12331
  });
12054
12332
  }
12055
12333
  }
@@ -12077,16 +12355,16 @@ var RealtimeChannel = class _RealtimeChannel {
12077
12355
  };
12078
12356
  this.joinPush.destroy();
12079
12357
  let leavePush = null;
12080
- return new Promise((resolve3) => {
12358
+ return new Promise((resolve4) => {
12081
12359
  leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout);
12082
12360
  leavePush.receive("ok", () => {
12083
12361
  onClose();
12084
- resolve3("ok");
12362
+ resolve4("ok");
12085
12363
  }).receive("timeout", () => {
12086
12364
  onClose();
12087
- resolve3("timed out");
12365
+ resolve4("timed out");
12088
12366
  }).receive("error", () => {
12089
- resolve3("error");
12367
+ resolve4("error");
12090
12368
  });
12091
12369
  leavePush.send();
12092
12370
  if (!this._canPush()) {
@@ -12165,8 +12443,8 @@ var RealtimeChannel = class _RealtimeChannel {
12165
12443
  _trigger(type, payload, ref) {
12166
12444
  var _a, _b;
12167
12445
  const typeLower = type.toLocaleLowerCase();
12168
- const { close, error: error2, leave, join: join12 } = CHANNEL_EVENTS;
12169
- const events = [close, error2, leave, join12];
12446
+ const { close, error: error2, leave, join: join15 } = CHANNEL_EVENTS;
12447
+ const events = [close, error2, leave, join15];
12170
12448
  if (ref && events.indexOf(typeLower) >= 0 && ref !== this._joinRef()) {
12171
12449
  return;
12172
12450
  }
@@ -13782,7 +14060,7 @@ var _getRequestParams = (method, options, parameters, body) => {
13782
14060
  return _objectSpread2(_objectSpread2({}, params), parameters);
13783
14061
  };
13784
14062
  async function _handleRequest(fetcher, method, url, options, parameters, body, namespace) {
13785
- return new Promise((resolve3, reject) => {
14063
+ return new Promise((resolve4, reject) => {
13786
14064
  fetcher(url, _getRequestParams(method, options, parameters, body)).then((result) => {
13787
14065
  if (!result.ok) throw result;
13788
14066
  if (options === null || options === void 0 ? void 0 : options.noResolveJson) return result;
@@ -13792,7 +14070,7 @@ async function _handleRequest(fetcher, method, url, options, parameters, body, n
13792
14070
  if (!contentType || !contentType.includes("application/json")) return {};
13793
14071
  }
13794
14072
  return result.json();
13795
- }).then((data) => resolve3(data)).catch((error2) => handleError(error2, reject, options, namespace));
14073
+ }).then((data) => resolve4(data)).catch((error2) => handleError(error2, reject, options, namespace));
13796
14074
  });
13797
14075
  }
13798
14076
  function createFetchApi(namespace = "storage") {
@@ -21598,14 +21876,14 @@ async function publicRoutes(fastify, options) {
21598
21876
  }
21599
21877
 
21600
21878
  // ../server/dist/routes/chat.js
21601
- import { resolve, join as join6 } from "path";
21879
+ import { resolve, join as join8 } from "path";
21602
21880
  import { readFile as readFile3 } from "fs/promises";
21603
21881
  function isChatEnabled(configLoader) {
21604
21882
  const project = configLoader.getProject();
21605
21883
  return !!(project?.features?.chat && process.env.ANTHROPIC_API_KEY);
21606
21884
  }
21607
21885
  async function chatRoutes(fastify, options) {
21608
- const { configLoader, queryService } = options;
21886
+ const { configLoader, queryService, aiConfig } = options;
21609
21887
  fastify.post("/api/chat/generate-text", async (request, reply) => {
21610
21888
  const { prompt, mode, dashboard: dashboardName, existingContent, filters } = request.body ?? {};
21611
21889
  if (!prompt || !mode || !dashboardName) {
@@ -21616,10 +21894,9 @@ async function chatRoutes(fastify, options) {
21616
21894
  if (mode !== "insert" && mode !== "replace") {
21617
21895
  return reply.status(400).send({ error: 'mode must be "insert" or "replace"' });
21618
21896
  }
21619
- const apiKey = process.env.ANTHROPIC_API_KEY;
21620
- if (!apiKey) {
21897
+ if (!aiConfig && !process.env.ANTHROPIC_API_KEY) {
21621
21898
  return reply.status(400).send({
21622
- error: "AI text generation requires an API key. Set ANTHROPIC_API_KEY in your environment."
21899
+ error: "AI text generation requires an API key. Set ANTHROPIC_API_KEY in your environment, or configure an ai: block in yamchart.yaml."
21623
21900
  });
21624
21901
  }
21625
21902
  const dashboard = configLoader.getDashboardByName(dashboardName);
@@ -21726,8 +22003,7 @@ async function chatRoutes(fastify, options) {
21726
22003
  systemParts.push("Output ONLY the new text to insert. Do NOT repeat the existing content.");
21727
22004
  }
21728
22005
  const systemPrompt = systemParts.join("\n");
21729
- const { AnthropicProvider } = await import("./dist-5DQO6L2S.js");
21730
- const provider = new AnthropicProvider(apiKey);
22006
+ const provider = createProviderForTask(aiConfig, "text_generation", process.env);
21731
22007
  const response = await provider.chat({
21732
22008
  system: systemPrompt,
21733
22009
  messages: [{ role: "user", content: prompt }],
@@ -21850,7 +22126,7 @@ IMPORTANT: When using query_model, pass the chart name (e.g. "${chartRefs[0] ??
21850
22126
  The user is viewing the "${chart.title ?? chart.name}" chart (id: "${chart.name}", type: ${chart.chart?.type}).${model}${params}`;
21851
22127
  }
21852
22128
  } else if (chatContext.type === "explore") {
21853
- const catalogPath = join6(options.projectDir, ".yamchart", "catalog.json");
22129
+ const catalogPath = join8(options.projectDir, ".yamchart", "catalog.json");
21854
22130
  const tableLines = [];
21855
22131
  try {
21856
22132
  const catalogData = JSON.parse(await readFile3(catalogPath, "utf-8"));
@@ -22391,7 +22667,7 @@ var responseViaResponseObject = async (res, outgoing, options = {}) => {
22391
22667
  });
22392
22668
  if (!chunk) {
22393
22669
  if (i === 1) {
22394
- await new Promise((resolve3) => setTimeout(resolve3));
22670
+ await new Promise((resolve4) => setTimeout(resolve4));
22395
22671
  maxReadCount = 3;
22396
22672
  continue;
22397
22673
  }
@@ -24425,9 +24701,9 @@ data:
24425
24701
  const initRequest = messages.find((m) => isInitializeRequest(m));
24426
24702
  const clientProtocolVersion = initRequest ? initRequest.params.protocolVersion : req.headers.get("mcp-protocol-version") ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
24427
24703
  if (this._enableJsonResponse) {
24428
- return new Promise((resolve3) => {
24704
+ return new Promise((resolve4) => {
24429
24705
  this._streamMapping.set(streamId, {
24430
- resolveJson: resolve3,
24706
+ resolveJson: resolve4,
24431
24707
  cleanup: () => {
24432
24708
  this._streamMapping.delete(streamId);
24433
24709
  }
@@ -26744,7 +27020,7 @@ var Protocol = class {
26744
27020
  return;
26745
27021
  }
26746
27022
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
26747
- await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
27023
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
26748
27024
  options?.signal?.throwIfAborted();
26749
27025
  }
26750
27026
  } catch (error2) {
@@ -26761,7 +27037,7 @@ var Protocol = class {
26761
27037
  */
26762
27038
  request(request, resultSchema, options) {
26763
27039
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
26764
- return new Promise((resolve3, reject) => {
27040
+ return new Promise((resolve4, reject) => {
26765
27041
  const earlyReject = (error2) => {
26766
27042
  reject(error2);
26767
27043
  };
@@ -26839,7 +27115,7 @@ var Protocol = class {
26839
27115
  if (!parseResult.success) {
26840
27116
  reject(parseResult.error);
26841
27117
  } else {
26842
- resolve3(parseResult.data);
27118
+ resolve4(parseResult.data);
26843
27119
  }
26844
27120
  } catch (error2) {
26845
27121
  reject(error2);
@@ -27100,12 +27376,12 @@ var Protocol = class {
27100
27376
  }
27101
27377
  } catch {
27102
27378
  }
27103
- return new Promise((resolve3, reject) => {
27379
+ return new Promise((resolve4, reject) => {
27104
27380
  if (signal.aborted) {
27105
27381
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
27106
27382
  return;
27107
27383
  }
27108
- const timeoutId = setTimeout(resolve3, interval);
27384
+ const timeoutId = setTimeout(resolve4, interval);
27109
27385
  signal.addEventListener("abort", () => {
27110
27386
  clearTimeout(timeoutId);
27111
27387
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -28206,7 +28482,7 @@ var McpServer = class {
28206
28482
  let task = createTaskResult.task;
28207
28483
  const pollInterval = task.pollInterval ?? 5e3;
28208
28484
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
28209
- await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
28485
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
28210
28486
  const updatedTask = await extra.taskStore.getTask(taskId);
28211
28487
  if (!updatedTask) {
28212
28488
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -28781,13 +29057,20 @@ import { z as z2 } from "zod";
28781
29057
  async function listCharts(deps) {
28782
29058
  const charts = deps.configLoader.getCharts().map((c) => {
28783
29059
  const chart = c;
29060
+ const model = chart.source?.model ? deps.configLoader.getModelByName(chart.source.model) : void 0;
29061
+ const modelParameters = model?.metadata?.params ?? [];
29062
+ const parameterDetails = [
29063
+ ...modelParameters.map((p) => ({ ...p, source: "model" })),
29064
+ ...(chart.parameters ?? []).map((p) => ({ ...p, source: "chart" }))
29065
+ ];
28784
29066
  return {
28785
29067
  name: chart.name,
28786
29068
  title: chart.title,
28787
29069
  ...chart.description ? { description: chart.description } : {},
28788
29070
  type: chart.chart?.type,
28789
29071
  model: chart.source?.model,
28790
- parameters: (chart.parameters ?? []).map((p) => p.name)
29072
+ parameters: (chart.parameters ?? []).map((p) => p.name),
29073
+ parameter_details: parameterDetails
28791
29074
  };
28792
29075
  });
28793
29076
  return JSON.stringify(charts);
@@ -28832,17 +29115,98 @@ async function runSql(deps, args) {
28832
29115
  async function runModel(deps, args) {
28833
29116
  const chart = deps.configLoader.getChartByName(args.name);
28834
29117
  if (!chart) {
29118
+ const model = deps.configLoader.getModelByName(args.name);
29119
+ if (model) {
29120
+ const chartsForModel = deps.configLoader.getCharts().filter((c) => c.source?.model === args.name).map((c) => c.name);
29121
+ return [
29122
+ `Error: "${args.name}" is a model, but run_model currently executes saved charts by chart name.`,
29123
+ chartsForModel.length ? `Use one of these chart names for that model: ${chartsForModel.join(", ")}.` : "Create a chart for this model or use run_sql to query the model SQL directly."
29124
+ ].join(" ");
29125
+ }
28835
29126
  return `Error: Chart "${args.name}" not found. Use list_charts to see available charts.`;
28836
29127
  }
28837
- const result = await deps.queryService.executeChart(chart, args.params ?? {}, buildUserContext(deps.user));
28838
- return JSON.stringify({ columns: result.columns, rows: result.rows });
29128
+ const normalized = normalizeChartParams(deps, chart, args.params ?? {});
29129
+ if (normalized.error)
29130
+ return `Error: ${JSON.stringify(normalized.error)}`;
29131
+ try {
29132
+ const result = await deps.queryService.executeChart(chart, normalized.params, buildUserContext(deps.user));
29133
+ return JSON.stringify({ columns: result.columns, rows: result.rows });
29134
+ } catch (err) {
29135
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
29136
+ }
29137
+ }
29138
+ function normalizeChartParams(deps, chart, input) {
29139
+ const definitions = /* @__PURE__ */ new Map();
29140
+ const modelName = chart.source?.model;
29141
+ const model = modelName ? deps.configLoader.getModelByName(modelName) : void 0;
29142
+ for (const param of model?.metadata?.params ?? []) {
29143
+ definitions.set(param.name, param);
29144
+ }
29145
+ for (const param of chart.parameters ?? []) {
29146
+ definitions.set(param.name, param);
29147
+ }
29148
+ const params = { ...input };
29149
+ for (const [name, value] of Object.entries(input)) {
29150
+ const definition = definitions.get(name);
29151
+ if (!definition)
29152
+ continue;
29153
+ const type = definition.type;
29154
+ if (type === "multi_select" || type === "string[]") {
29155
+ if (isAllValue(value)) {
29156
+ params[name] = [];
29157
+ } else if (!Array.isArray(value)) {
29158
+ params[name] = [String(value)];
29159
+ } else {
29160
+ params[name] = value.map((v) => String(v));
29161
+ }
29162
+ continue;
29163
+ }
29164
+ if (type === "number[]") {
29165
+ const values = Array.isArray(value) ? value : [value];
29166
+ const nums = values.map((v) => Number(v));
29167
+ if (nums.some((v) => !Number.isFinite(v))) {
29168
+ return {
29169
+ params,
29170
+ error: {
29171
+ error_type: "invalid_parameter",
29172
+ parameter: name,
29173
+ expected: "array<number>",
29174
+ received: Array.isArray(value) ? "array" : typeof value
29175
+ }
29176
+ };
29177
+ }
29178
+ params[name] = nums;
29179
+ continue;
29180
+ }
29181
+ if (type === "number" && typeof value === "string") {
29182
+ const num2 = Number(value);
29183
+ if (!Number.isFinite(num2)) {
29184
+ return {
29185
+ params,
29186
+ error: {
29187
+ error_type: "invalid_parameter",
29188
+ parameter: name,
29189
+ expected: "number",
29190
+ received: "string"
29191
+ }
29192
+ };
29193
+ }
29194
+ params[name] = num2;
29195
+ }
29196
+ }
29197
+ return { params };
29198
+ }
29199
+ function isAllValue(value) {
29200
+ if (typeof value === "string")
29201
+ return value.trim().toLowerCase() === "all";
29202
+ return Array.isArray(value) && value.length === 1 && isAllValue(value[0]);
28839
29203
  }
28840
29204
 
28841
29205
  // ../../packages/mcp-core/dist/catalog.js
28842
- import { join as join7 } from "path";
29206
+ import { join as join9 } from "path";
28843
29207
  import { readFile as readFile4 } from "fs/promises";
28844
29208
  async function readCatalogTables(projectDir, include) {
28845
- const catalogPath = join7(projectDir, ".yamchart", "catalog.json");
29209
+ const catalogPath = join9(projectDir, ".yamchart", "catalog.json");
28846
29210
  let parsed;
28847
29211
  try {
28848
29212
  parsed = JSON.parse(await readFile4(catalogPath, "utf-8"));
@@ -28873,22 +29237,38 @@ function includeFilter(deps) {
28873
29237
  }
28874
29238
  async function listTables(deps) {
28875
29239
  const tables = await readCatalogTables(deps.projectDir, includeFilter(deps));
28876
- return JSON.stringify(tables.map((t) => ({
28877
- name: t.name,
28878
- ...t.schema ? { schema: t.schema } : {},
28879
- connection: t.connection,
28880
- columnCount: t.columns.length
28881
- })));
29240
+ if (tables.length === 0) {
29241
+ const live = await discoverLiveTables(deps);
29242
+ return JSON.stringify({
29243
+ tables: live.tables.map(tableSummary),
29244
+ ...live.warning ? { warning: live.warning } : {}
29245
+ });
29246
+ }
29247
+ return JSON.stringify({ tables: tables.map(tableSummary) });
28882
29248
  }
28883
29249
  async function describeTable(deps, args) {
28884
- const normalizedTable = args.schema != null && !args.table.includes(".") ? `${args.schema}.${args.table}` : args.table;
28885
- const parts = normalizedTable.split(".");
29250
+ const identifier = parseTableIdentifier(args.table, args.schema);
29251
+ if (!identifier.valid)
29252
+ return `Error: ${identifier.error}`;
29253
+ const normalizedTable = identifier.normalized;
29254
+ const parts = identifier.parts;
28886
29255
  const tableName = parts[parts.length - 1] ?? normalizedTable;
28887
29256
  const schema = parts.length > 1 ? parts.slice(0, -1).join(".") : void 0;
28888
29257
  const tables = await readCatalogTables(deps.projectDir, includeFilter(deps));
28889
- const match = tables.find((t) => t.name === tableName && (schema === void 0 || t.schema === schema));
29258
+ const match = tables.find((t) => t.name === tableName && (schema === void 0 || schemaMatches(t.schema, schema)));
28890
29259
  if (!match) {
28891
- return `Error: Table "${args.table}" not found in catalog. Use list_tables to see available tables.`;
29260
+ try {
29261
+ const result = await deps.queryService.executeRawSql(`SELECT * FROM ${identifier.sql} LIMIT 0`);
29262
+ return JSON.stringify({
29263
+ name: tableName,
29264
+ ...schema ? { schema } : {},
29265
+ columns: result.columns,
29266
+ warning: "Table was not found in the cached catalog; columns were discovered with a live LIMIT 0 query."
29267
+ });
29268
+ } catch (err) {
29269
+ const detail2 = err instanceof Error ? err.message : String(err);
29270
+ return `Error: Table "${args.table}" not found in catalog, and live discovery failed: ${detail2}`;
29271
+ }
28892
29272
  }
28893
29273
  return JSON.stringify({
28894
29274
  name: match.name,
@@ -28896,24 +29276,111 @@ async function describeTable(deps, args) {
28896
29276
  columns: match.columns
28897
29277
  });
28898
29278
  }
28899
- var TABLE_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
28900
29279
  async function sampleTable(deps, args) {
28901
- if (!TABLE_IDENT.test(args.table)) {
28902
- return `Error: Invalid table identifier "${args.table}".`;
28903
- }
29280
+ const identifier = parseTableIdentifier(args.table, args.schema);
29281
+ if (!identifier.valid)
29282
+ return `Error: ${identifier.error}`;
28904
29283
  const limit2 = Math.min(Math.max(args.limit ?? 5, 1), 100);
28905
- const parts = args.table.split(".");
29284
+ const parts = identifier.parts;
28906
29285
  const tableName = parts[parts.length - 1] ?? args.table;
28907
29286
  const providedSchema = parts.length > 1 ? parts.slice(0, -1).join(".") : void 0;
28908
29287
  const tables = await readCatalogTables(deps.projectDir, includeFilter(deps));
28909
- const match = tables.find((t) => t.name === tableName && (providedSchema === void 0 || t.schema === providedSchema));
28910
- const from = providedSchema === void 0 && match?.schema ? `${match.schema}.${args.table}` : args.table;
29288
+ const match = tables.find((t) => t.name === tableName && (providedSchema === void 0 || schemaMatches(t.schema, providedSchema)));
29289
+ const from = providedSchema === void 0 && match?.schema ? sqlIdentifier(`${match.schema}.${args.table}`) : identifier.sql;
28911
29290
  const result = await deps.queryService.executeRawSql(`SELECT * FROM ${from} LIMIT ${limit2}`);
28912
29291
  return JSON.stringify({ columns: result.columns, rows: result.rows });
28913
29292
  }
29293
+ function tableSummary(t) {
29294
+ return {
29295
+ name: t.name,
29296
+ ...t.schema ? { schema: t.schema } : {},
29297
+ connection: t.connection,
29298
+ columnCount: t.columns.length
29299
+ };
29300
+ }
29301
+ function schemaMatches(catalogSchema, requestedSchema) {
29302
+ if (catalogSchema === requestedSchema)
29303
+ return true;
29304
+ return catalogSchema?.endsWith(`.${requestedSchema}`) ?? false;
29305
+ }
29306
+ function parseTableIdentifier(table, schema) {
29307
+ const normalizedInput = table.trim().replace(/^`(.+)`$/, "$1");
29308
+ const normalized = schema != null && !normalizedInput.includes(".") ? `${schema}.${normalizedInput}` : normalizedInput;
29309
+ const parts = normalized.split(".");
29310
+ if (parts.length === 0 || parts.length > 3 || parts.some((p) => !/^[A-Za-z_][A-Za-z0-9_-]*$/.test(p))) {
29311
+ return {
29312
+ valid: false,
29313
+ error: `Invalid table identifier "${table}". Use table, schema.table, project.schema.table, or a backticked fully qualified name.`
29314
+ };
29315
+ }
29316
+ return { valid: true, normalized, parts, sql: sqlIdentifier(normalized) };
29317
+ }
29318
+ function sqlIdentifier(identifier) {
29319
+ const needsQuoting = identifier.split(".").some((p) => p.includes("-"));
29320
+ return needsQuoting ? `\`${identifier}\`` : identifier;
29321
+ }
29322
+ async function discoverLiveTables(deps) {
29323
+ const include = includeFilter(deps);
29324
+ const connectionType = deps.connectionType?.toLowerCase();
29325
+ try {
29326
+ if (connectionType === "bigquery") {
29327
+ const connection2 = deps.configLoader.getDefaultConnection?.();
29328
+ const config = connection2?.type === "bigquery" ? connection2.config : void 0;
29329
+ const projectId = config?.project_id;
29330
+ if (!projectId) {
29331
+ return {
29332
+ tables: [],
29333
+ warning: "Cached catalog is empty. Live BigQuery discovery needs a default BigQuery connection with config.project_id."
29334
+ };
29335
+ }
29336
+ const region = bigQueryRegion(config.location);
29337
+ const schemas = include?.schemas ?? [];
29338
+ const schemaFilter = schemas.length ? ` AND table_schema IN (${schemas.map(sqlStringLiteral).join(", ")})` : "";
29339
+ const result2 = await deps.queryService.executeRawSql(`SELECT table_schema AS schema, table_name AS name FROM \`${region}\`.INFORMATION_SCHEMA.TABLES WHERE table_catalog = ${sqlStringLiteral(projectId)} AND table_type = 'BASE TABLE'${schemaFilter} ORDER BY table_schema, table_name LIMIT 1000`);
29340
+ return {
29341
+ tables: rowsToTables(result2.rows, connection2?.name ?? "default"),
29342
+ warning: "Cached catalog is empty; returned live BigQuery region-level INFORMATION_SCHEMA results."
29343
+ };
29344
+ }
29345
+ const connection = deps.configLoader.getDefaultConnection?.();
29346
+ if (connection?.type === "postgres" && connection.config.schema) {
29347
+ const result2 = await deps.queryService.executeRawSql(`SELECT table_schema AS schema, table_name AS name FROM information_schema.tables WHERE table_type IN ('BASE TABLE', 'VIEW') AND table_schema = ${sqlStringLiteral(connection.config.schema)} LIMIT 1000`);
29348
+ return {
29349
+ tables: rowsToTables(result2.rows, connection.name),
29350
+ warning: "Cached catalog is empty; returned live INFORMATION_SCHEMA results."
29351
+ };
29352
+ }
29353
+ const result = await deps.queryService.executeRawSql("SELECT table_schema AS schema, table_name AS name FROM information_schema.tables WHERE table_type IN ('BASE TABLE', 'VIEW') LIMIT 1000");
29354
+ return {
29355
+ tables: rowsToTables(result.rows, "default"),
29356
+ warning: "Cached catalog is empty; returned live INFORMATION_SCHEMA results."
29357
+ };
29358
+ } catch (err) {
29359
+ const detail2 = err instanceof Error ? err.message : String(err);
29360
+ return {
29361
+ tables: [],
29362
+ warning: `Cached catalog is empty and live INFORMATION_SCHEMA discovery failed: ${detail2}`
29363
+ };
29364
+ }
29365
+ }
29366
+ function rowsToTables(rows, connection) {
29367
+ return rows.map((r) => ({
29368
+ name: String(r["name"] ?? r["table_name"] ?? ""),
29369
+ schema: String(r["schema"] ?? r["table_schema"] ?? ""),
29370
+ connection,
29371
+ columns: []
29372
+ })).filter((t) => t.name.length > 0);
29373
+ }
29374
+ function sqlStringLiteral(value) {
29375
+ return `'${value.replace(/'/g, "''")}'`;
29376
+ }
29377
+ function bigQueryRegion(location) {
29378
+ const loc = (location ?? "US").trim().toLowerCase().replace(/_/g, "-");
29379
+ return loc.startsWith("region-") ? loc : `region-${loc}`;
29380
+ }
28914
29381
 
28915
29382
  // ../../packages/mcp-core/dist/render/optionBuilder.js
28916
- var SUPPORTED = /* @__PURE__ */ new Set(["line", "bar", "area", "pie", "donut", "scatter"]);
29383
+ var SUPPORTED = /* @__PURE__ */ new Set(["line", "bar", "area", "pie", "donut", "scatter", "kpi", "table"]);
28917
29384
  function num(v) {
28918
29385
  if (v === null || v === void 0 || v === "")
28919
29386
  return null;
@@ -28933,6 +29400,64 @@ function buildEChartsOption(chart, rows) {
28933
29400
  left: "center"
28934
29401
  };
28935
29402
  const grid = { left: "3%", right: "4%", bottom: "8%", top: 60, containLabel: true };
29403
+ if (type === "kpi") {
29404
+ const valueField = block.value?.field ?? "";
29405
+ const value = rows[0]?.[valueField];
29406
+ return {
29407
+ title,
29408
+ graphic: [
29409
+ {
29410
+ type: "text",
29411
+ left: "center",
29412
+ top: "42%",
29413
+ style: {
29414
+ text: value === null || value === void 0 ? "No data" : String(value),
29415
+ fontSize: 56,
29416
+ fontWeight: 700,
29417
+ fill: "#111827",
29418
+ textAlign: "center"
29419
+ }
29420
+ },
29421
+ {
29422
+ type: "text",
29423
+ left: "center",
29424
+ top: "58%",
29425
+ style: {
29426
+ text: block.value?.label ?? valueField,
29427
+ fontSize: 18,
29428
+ fill: "#6b7280",
29429
+ textAlign: "center"
29430
+ }
29431
+ }
29432
+ ]
29433
+ };
29434
+ }
29435
+ if (type === "table") {
29436
+ const columns = block.columns?.length ? block.columns : Object.keys(rows[0] ?? {}).map((field) => ({ field, label: field }));
29437
+ return {
29438
+ title,
29439
+ tooltip: { trigger: "item" },
29440
+ grid,
29441
+ xAxis: {
29442
+ type: "category",
29443
+ data: columns.map((c) => c.label ?? c.field),
29444
+ axisLabel: { rotate: 30 }
29445
+ },
29446
+ yAxis: { type: "category", data: rows.slice(0, 10).map((_, i) => `Row ${i + 1}`) },
29447
+ series: [
29448
+ {
29449
+ type: "scatter",
29450
+ symbolSize: 1,
29451
+ label: {
29452
+ show: true,
29453
+ formatter: "{@[2]}",
29454
+ color: "#111827"
29455
+ },
29456
+ data: rows.slice(0, 10).flatMap((row, rowIndex) => columns.map((column, colIndex) => [colIndex, rowIndex, row[column.field] ?? ""]))
29457
+ }
29458
+ ]
29459
+ };
29460
+ }
28936
29461
  if (type === "pie" || type === "donut") {
28937
29462
  return {
28938
29463
  title,
@@ -28957,6 +29482,47 @@ function buildEChartsOption(chart, rows) {
28957
29482
  };
28958
29483
  }
28959
29484
  const echartType = type === "bar" ? "bar" : "line";
29485
+ if (block.series?.columns?.length) {
29486
+ return {
29487
+ title,
29488
+ legend: { bottom: 0 },
29489
+ grid,
29490
+ tooltip: { trigger: "axis" },
29491
+ xAxis: { type: "category", data: rows.map((r) => String(r[xField] ?? "")), name: block.x?.label ?? xField },
29492
+ yAxis: { type: "value", name: block.y?.label ?? block.series.columns.map((c) => c.name ?? c.field).join(", ") },
29493
+ series: block.series.columns.map((column) => {
29494
+ const seriesType = column.type === "bar" ? "bar" : "line";
29495
+ return {
29496
+ name: column.name ?? column.field,
29497
+ type: type === "bar" ? "bar" : seriesType,
29498
+ data: rows.map((r) => num(r[column.field])),
29499
+ ...type === "area" || column.type === "area" ? { areaStyle: {} } : {}
29500
+ };
29501
+ })
29502
+ };
29503
+ }
29504
+ if (block.series?.field) {
29505
+ const seriesField = block.series.field;
29506
+ const groups = [...new Set(rows.map((r) => String(r[seriesField] ?? "")))];
29507
+ const xValues = [...new Set(rows.map((r) => String(r[xField] ?? "")))];
29508
+ return {
29509
+ title,
29510
+ legend: { bottom: 0 },
29511
+ grid,
29512
+ tooltip: { trigger: "axis" },
29513
+ xAxis: { type: "category", data: xValues, name: block.x?.label ?? xField },
29514
+ yAxis: { type: "value", name: block.y?.label ?? yField },
29515
+ series: groups.map((group) => ({
29516
+ name: group,
29517
+ type: echartType,
29518
+ data: xValues.map((x) => {
29519
+ const row = rows.find((r) => String(r[xField] ?? "") === x && String(r[seriesField] ?? "") === group);
29520
+ return row ? num(row[yField]) : null;
29521
+ }),
29522
+ ...type === "area" ? { areaStyle: {} } : {}
29523
+ }))
29524
+ };
29525
+ }
28960
29526
  return {
28961
29527
  title,
28962
29528
  grid,
@@ -29040,7 +29606,7 @@ async function visualize(_deps, args) {
29040
29606
 
29041
29607
  // ../../packages/mcp-core/dist/tools/save.js
29042
29608
  import { mkdir, writeFile as writeFile3 } from "fs/promises";
29043
- import { join as join8 } from "path";
29609
+ import { join as join10 } from "path";
29044
29610
  import { stringify as stringifyYaml3 } from "yaml";
29045
29611
  var SAFE_NAME = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
29046
29612
  async function saveChart(deps, args) {
@@ -29058,9 +29624,9 @@ async function saveChart(deps, args) {
29058
29624
  if (name.length > 200) {
29059
29625
  return `Error: Chart name is too long (${name.length} chars). Maximum is 200.`;
29060
29626
  }
29061
- const chartsDir = join8(deps.projectDir, "charts");
29627
+ const chartsDir = join10(deps.projectDir, "charts");
29062
29628
  await mkdir(chartsDir, { recursive: true });
29063
- await writeFile3(join8(chartsDir, `${name}.yaml`), stringifyYaml3(parsed.data), "utf-8");
29629
+ await writeFile3(join10(chartsDir, `${name}.yaml`), stringifyYaml3(parsed.data), "utf-8");
29064
29630
  await deps.configLoader.load();
29065
29631
  return `Chart "${name}" saved to charts/${name}.yaml`;
29066
29632
  }
@@ -29169,6 +29735,18 @@ var EXAMPLES = {
29169
29735
  y: { field: "invoiced_revenue", type: "quantitative", format: "$,.0f" }
29170
29736
  }
29171
29737
  },
29738
+ bar_multi_series_long: {
29739
+ name: "jobs_by_month",
29740
+ title: "Jobs by Month",
29741
+ source: { model: "jobs_by_month" },
29742
+ chart: {
29743
+ type: "bar",
29744
+ x: { field: "month", type: "temporal" },
29745
+ y: { field: "job_count", type: "quantitative", label: "Jobs" },
29746
+ series: { field: "job_type" },
29747
+ legend: { show: true, position: "bottom" }
29748
+ }
29749
+ },
29172
29750
  line: {
29173
29751
  name: "revenue_trend",
29174
29752
  title: "Revenue Trend",
@@ -29179,6 +29757,22 @@ var EXAMPLES = {
29179
29757
  y: { field: "invoiced_revenue", type: "quantitative" }
29180
29758
  }
29181
29759
  },
29760
+ line_multi_series_wide: {
29761
+ name: "jobs_completed_vs_scheduled",
29762
+ title: "Completed vs Scheduled Jobs",
29763
+ source: { model: "jobs_by_month" },
29764
+ chart: {
29765
+ type: "line",
29766
+ x: { field: "month", type: "temporal" },
29767
+ series: {
29768
+ columns: [
29769
+ { field: "completed_jobs", name: "Completed" },
29770
+ { field: "scheduled_jobs", name: "Scheduled", style: "dashed" }
29771
+ ]
29772
+ },
29773
+ legend: { show: true, position: "bottom" }
29774
+ }
29775
+ },
29182
29776
  area: {
29183
29777
  name: "revenue_area",
29184
29778
  title: "Revenue Area",
@@ -29235,7 +29829,7 @@ var EXAMPLES = {
29235
29829
  function getChartSchema(args) {
29236
29830
  const type = args.type != null && EXAMPLES[args.type] != null ? args.type : "bar";
29237
29831
  return JSON.stringify({
29238
- note: "Axes are objects: { field, type: temporal|quantitative|ordinal|nominal }. source is { model } or { sql }.",
29832
+ note: "Axes are objects: { field, type: temporal|quantitative|ordinal|nominal }. source is { model } or { sql }. For long-format multi-series charts use chart.series.field; for wide-format multi-series charts use chart.series.columns. chart.color is a field-name string or conditional color config, not an axis object.",
29239
29833
  available_examples: Object.keys(EXAMPLES),
29240
29834
  example: EXAMPLES[type]
29241
29835
  }, null, 2);
@@ -29367,15 +29961,15 @@ async function scanBlindspots(input) {
29367
29961
  }
29368
29962
 
29369
29963
  // ../../packages/learning/dist/context.js
29370
- import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync } from "fs";
29371
- import { join as join9 } from "path";
29964
+ import { readFileSync as readFileSync4, existsSync as existsSync4, readdirSync } from "fs";
29965
+ import { join as join11 } from "path";
29372
29966
  var MAX_DOC = 6e3;
29373
29967
  var MAX_TOTAL = 16e3;
29374
29968
  function loadKnowledgeDocs(projectDir) {
29375
- const dir = join9(projectDir, "knowledge");
29376
- if (!existsSync2(dir))
29969
+ const dir = join11(projectDir, "knowledge");
29970
+ if (!existsSync4(dir))
29377
29971
  return [];
29378
- return readdirSync(dir).filter((f) => /\.md$/i.test(f)).map((f) => readFileSync2(join9(dir, f), "utf8"));
29972
+ return readdirSync(dir).filter((f) => /\.md$/i.test(f)).map((f) => readFileSync4(join11(dir, f), "utf8"));
29379
29973
  }
29380
29974
  function buildContextBlock(input) {
29381
29975
  const parts = [];
@@ -29489,24 +30083,88 @@ ${context}` : "",
29489
30083
  } catch (e) {
29490
30084
  return { question, mode: "generated", sql, groundedOn, message: `Query failed: ${e.message}` };
29491
30085
  }
29492
- }
29493
- function limit(sql) {
29494
- return /\blimit\b/i.test(sql) ? sql : `${sql.replace(/;\s*$/, "")} LIMIT 1000`;
30086
+ }
30087
+ function limit(sql) {
30088
+ return /\blimit\b/i.test(sql) ? sql : `${sql.replace(/;\s*$/, "")} LIMIT 1000`;
30089
+ }
30090
+
30091
+ // ../../packages/learning/dist/reindex.js
30092
+ async function reindexEmbeddings(input) {
30093
+ const { store, embedder, connection } = input;
30094
+ const corpus = [];
30095
+ for (const h of await store.listHunches({ connection, status: "confirmed" })) {
30096
+ corpus.push({ kind: "hunch", id: h.id, connection: h.connection, text: h.question });
30097
+ }
30098
+ for (const b of await store.listBlindspots({ connection, status: "answered" })) {
30099
+ if (b.answer)
30100
+ corpus.push({ kind: "blindspot", id: b.id, connection: b.connection, text: `${b.title}: ${b.answer}` });
30101
+ }
30102
+ for (const d of await store.listKnowledgeDocs({ connection })) {
30103
+ corpus.push({ kind: "knowledge", id: d.id, connection: d.connection, text: `${d.title}
30104
+ ${d.content}` });
30105
+ }
30106
+ if (corpus.length === 0)
30107
+ return 0;
30108
+ const vectors = await embedder.embed(corpus.map((c) => c.text));
30109
+ if (vectors.length !== corpus.length) {
30110
+ throw new Error(`Embedder returned ${vectors.length} vectors for ${corpus.length} texts`);
30111
+ }
30112
+ for (let i = 0; i < corpus.length; i++) {
30113
+ const item = corpus[i];
30114
+ await store.upsertEmbedding({ connection: item.connection, source_kind: item.kind, source_id: item.id, text: item.text, vector: vectors[i] });
30115
+ }
30116
+ return corpus.length;
29495
30117
  }
29496
30118
 
29497
30119
  // ../../packages/mcp-core/dist/tools/answer.js
29498
30120
  async function answerQuestionTool(deps, args) {
29499
30121
  const connection = args.connection ?? deps.defaultConnectionName ?? "default";
29500
- const result = await answerQuestion({
29501
- question: args.question,
29502
- connection,
29503
- store: deps.knowledgeStore,
29504
- embedder: deps.embedder,
29505
- provider: deps.provider,
29506
- executeRawSql: (sql) => deps.queryService.executeRawSql(sql),
29507
- connectionType: deps.connectionType
29508
- });
29509
- return JSON.stringify(result, null, 2);
30122
+ try {
30123
+ const result = await answerQuestion({
30124
+ question: args.question,
30125
+ connection,
30126
+ store: deps.knowledgeStore,
30127
+ embedder: deps.embedder,
30128
+ provider: deps.provider,
30129
+ executeRawSql: (sql) => deps.queryService.executeRawSql(sql),
30130
+ connectionType: deps.connectionType
30131
+ });
30132
+ return JSON.stringify(result, null, 2);
30133
+ } catch (err) {
30134
+ const message = err instanceof Error ? err.message : String(err);
30135
+ const lower = message.toLowerCase();
30136
+ const isEmbeddingProvider = lower.includes("embedding") || lower.includes("embeddings") || lower.includes("generativelanguage.googleapis.com") || lower.includes("premature close");
30137
+ return JSON.stringify({
30138
+ error_type: isEmbeddingProvider ? "embedding_provider_unavailable" : "answer_question_failed",
30139
+ retryable: isEmbeddingProvider,
30140
+ fallback_available: true,
30141
+ fallback_attempted: false,
30142
+ ...suggestFallback(deps, args.question),
30143
+ message: isEmbeddingProvider ? "Natural-language retrieval failed, but governed metric, model, chart, and SQL tools are available." : "Natural-language answering failed, but governed metric, model, chart, and SQL tools are available.",
30144
+ detail: message
30145
+ }, null, 2);
30146
+ }
30147
+ }
30148
+ function suggestFallback(deps, question) {
30149
+ const charts = deps.configLoader?.getCharts?.() ?? [];
30150
+ const models = deps.configLoader?.getModels?.() ?? [];
30151
+ const terms = question.toLowerCase().split(/[^a-z0-9_]+/).filter((t) => t.length >= 3);
30152
+ const score = (text2) => terms.reduce((total, term) => total + (text2.includes(term) ? 1 : 0), 0);
30153
+ const rankedChart = charts.map((chart) => ({
30154
+ chart,
30155
+ score: score(`${chart.name} ${chart.title ?? ""} ${chart.source?.model ?? ""}`.toLowerCase())
30156
+ })).sort((a, b) => b.score - a.score)[0];
30157
+ if (rankedChart && rankedChart.score > 0) {
30158
+ return { suggested_tool: "run_model", suggested_args: { name: rankedChart.chart.name } };
30159
+ }
30160
+ const rankedModel = models.map((model) => ({
30161
+ model,
30162
+ score: score(`${model.metadata.name} ${model.metadata.description ?? ""}`.toLowerCase())
30163
+ })).sort((a, b) => b.score - a.score)[0];
30164
+ if (rankedModel && rankedModel.score > 0) {
30165
+ return { suggested_tool: "run_sql", suggested_args: { sql: rankedModel.model.sql } };
30166
+ }
30167
+ return {};
29510
30168
  }
29511
30169
 
29512
30170
  // ../../packages/mcp-core/dist/render/embedResource.js
@@ -29599,31 +30257,162 @@ var text = (s) => ({
29599
30257
  content: [{ type: "text", text: s }],
29600
30258
  ...s.startsWith("Error:") ? { isError: true } : {}
29601
30259
  });
30260
+ var textOut = (s) => ({
30261
+ content: [{ type: "text", text: s }],
30262
+ structuredContent: { result: s },
30263
+ ...s.startsWith("Error:") ? { isError: true } : {}
30264
+ });
30265
+ var TABULAR_OUTPUT = {
30266
+ rowCount: z2.number(),
30267
+ columns: z2.array(z2.string()),
30268
+ rows: z2.array(z2.record(z2.unknown())),
30269
+ preview: z2.array(z2.record(z2.unknown()))
30270
+ };
30271
+ var TEXT_OUTPUT = { result: z2.string() };
30272
+ var READ_ONLY = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
30273
+ var WRITE_SAFE = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
30274
+ function buildResourceMeta(baseUrl) {
30275
+ const RESOURCE_DOMAINS = ["https://cdn.jsdelivr.net", "https://persistent.oaistatic.com"];
30276
+ const csp = { connectDomains: [baseUrl], resourceDomains: RESOURCE_DOMAINS };
30277
+ return {
30278
+ // OpenAI Apps SDK (ChatGPT submission) — clears "Widget CSP / domain not set".
30279
+ "openai/widgetCSP": { connect_domains: [baseUrl], resource_domains: RESOURCE_DOMAINS },
30280
+ "openai/widgetDomain": baseUrl,
30281
+ "openai/widgetDescription": "Interactive yamchart chart renderer",
30282
+ // Standard MCP-UI camelCase form (kept for forward-compat).
30283
+ ui: { domain: baseUrl, csp }
30284
+ };
30285
+ }
29602
30286
  function createMcpServer(deps) {
29603
30287
  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 () => {
30288
+ const baseUrl = deps.baseUrl ?? process.env["BASE_URL"] ?? "https://yamchart.example.com";
30289
+ server.registerResource("chart-renderer", TEMPLATE_URI, {
30290
+ mimeType: "text/html;profile=mcp-app",
30291
+ description: "Interactive yamchart chart renderer",
30292
+ _meta: buildResourceMeta(baseUrl)
30293
+ }, async () => {
29605
30294
  const tpl = await buildTemplateResource();
29606
- return { contents: [{ uri: TEMPLATE_URI, mimeType: tpl.resource.mimeType, text: tpl.resource.text ?? "" }] };
30295
+ return {
30296
+ contents: [
30297
+ {
30298
+ uri: TEMPLATE_URI,
30299
+ mimeType: tpl.resource.mimeType,
30300
+ text: tpl.resource.text ?? "",
30301
+ _meta: buildResourceMeta(baseUrl)
30302
+ }
30303
+ ]
30304
+ };
30305
+ });
30306
+ server.registerTool("list_charts", {
30307
+ description: "List governed yamchart charts (name, title, type, model, parameters).",
30308
+ inputSchema: {},
30309
+ outputSchema: { charts: z2.array(z2.unknown()) },
30310
+ annotations: READ_ONLY
30311
+ }, async () => {
30312
+ const raw = await listCharts(deps);
30313
+ const charts = JSON.parse(raw);
30314
+ return {
30315
+ content: [{ type: "text", text: raw }],
30316
+ structuredContent: { charts }
30317
+ };
30318
+ });
30319
+ server.registerTool("list_models", {
30320
+ description: "List governed SQL models (name, description).",
30321
+ inputSchema: {},
30322
+ outputSchema: { models: z2.array(z2.unknown()) },
30323
+ annotations: READ_ONLY
30324
+ }, async () => {
30325
+ const raw = await listModels(deps);
30326
+ const models = JSON.parse(raw);
30327
+ return {
30328
+ content: [{ type: "text", text: raw }],
30329
+ structuredContent: { models }
30330
+ };
29607
30331
  });
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)));
29610
30332
  server.registerTool("run_model", {
29611
- 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)));
30333
+ description: "Execute a saved governed chart by its chart name (from list_charts) with parameters and return its data. Note: this runs CHARTS, not models \u2014 passing a model name returns an actionable error listing the chart names defined for that model.",
30334
+ inputSchema: { name: z2.string(), params: z2.record(z2.unknown()).optional() },
30335
+ outputSchema: TABULAR_OUTPUT,
30336
+ annotations: READ_ONLY
30337
+ }, async (args) => {
30338
+ const raw = await runModel(deps, args);
30339
+ if (raw.startsWith("Error:"))
30340
+ return { content: [{ type: "text", text: raw }], isError: true };
30341
+ const parsed = JSON.parse(raw);
30342
+ return {
30343
+ content: [{ type: "text", text: raw }],
30344
+ structuredContent: {
30345
+ rowCount: parsed.rows.length,
30346
+ columns: parsed.columns.map((c) => c.name),
30347
+ rows: parsed.rows,
30348
+ preview: parsed.rows.slice(0, 5)
30349
+ }
30350
+ };
30351
+ });
29614
30352
  server.registerTool("run_sql", {
29615
30353
  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)));
30354
+ inputSchema: { sql: z2.string(), limit: z2.number().optional() },
30355
+ outputSchema: TABULAR_OUTPUT,
30356
+ annotations: READ_ONLY
30357
+ }, async (args) => {
30358
+ const raw = await runSql(deps, args);
30359
+ if (raw.startsWith("Error:"))
30360
+ return { content: [{ type: "text", text: raw }], isError: true };
30361
+ const parsed = JSON.parse(raw);
30362
+ return {
30363
+ content: [{ type: "text", text: raw }],
30364
+ structuredContent: {
30365
+ rowCount: parsed.rows.length,
30366
+ columns: parsed.columns.map((c) => c.name),
30367
+ rows: parsed.rows,
30368
+ preview: parsed.rows.slice(0, 5)
30369
+ }
30370
+ };
30371
+ });
30372
+ server.registerTool("list_tables", {
30373
+ description: "List warehouse tables from the cached catalog (name, schema, connection, column count).",
30374
+ inputSchema: {},
30375
+ outputSchema: { tables: z2.array(z2.unknown()), warning: z2.string().optional() },
30376
+ annotations: READ_ONLY
30377
+ }, async () => {
30378
+ const raw = await listTables(deps);
30379
+ const parsed = JSON.parse(raw);
30380
+ return {
30381
+ content: [{ type: "text", text: raw }],
30382
+ structuredContent: parsed
30383
+ };
30384
+ });
29619
30385
  server.registerTool("describe_table", {
29620
30386
  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() }
29622
- }, 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)));
30387
+ inputSchema: { table: z2.string(), schema: z2.string().optional() },
30388
+ outputSchema: TEXT_OUTPUT,
30389
+ annotations: READ_ONLY
30390
+ }, async (args) => textOut(await describeTable(deps, args)));
30391
+ server.registerTool("sample_table", {
30392
+ description: "Return a few sample rows from a table.",
30393
+ inputSchema: { table: z2.string(), schema: z2.string().optional(), limit: z2.number().optional() },
30394
+ outputSchema: TABULAR_OUTPUT,
30395
+ annotations: READ_ONLY
30396
+ }, async (args) => {
30397
+ const raw = await sampleTable(deps, args);
30398
+ if (raw.startsWith("Error:"))
30399
+ return { content: [{ type: "text", text: raw }], isError: true };
30400
+ const parsed = JSON.parse(raw);
30401
+ return {
30402
+ content: [{ type: "text", text: raw }],
30403
+ structuredContent: {
30404
+ rowCount: parsed.rows.length,
30405
+ columns: parsed.columns.map((c) => c.name),
30406
+ rows: parsed.rows,
30407
+ preview: parsed.rows.slice(0, 5)
30408
+ }
30409
+ };
30410
+ });
29624
30411
  server.registerTool("visualize", {
29625
30412
  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
30413
  inputSchema: { spec: z2.record(z2.unknown()), data: z2.array(z2.record(z2.unknown())) },
30414
+ outputSchema: { spec: z2.record(z2.unknown()), option: z2.record(z2.unknown()) },
30415
+ annotations: READ_ONLY,
29627
30416
  _meta: {
29628
30417
  ui: { resourceUri: TEMPLATE_URI },
29629
30418
  "openai/outputTemplate": TEMPLATE_URI
@@ -29643,20 +30432,62 @@ function createMcpServer(deps) {
29643
30432
  structuredContent: { spec: result.spec, option: result.option }
29644
30433
  };
29645
30434
  });
30435
+ server.registerTool("get_permissions", {
30436
+ description: "Return the current MCP user role and permissions, including whether save_chart is allowed.",
30437
+ inputSchema: {},
30438
+ outputSchema: {
30439
+ role: z2.string(),
30440
+ permissions: z2.record(z2.boolean()),
30441
+ required_role: z2.object({ save_chart: z2.string() })
30442
+ },
30443
+ annotations: READ_ONLY
30444
+ }, async () => {
30445
+ const role = deps.user?.role ?? "anonymous";
30446
+ const permissions = {
30447
+ can_view_charts: true,
30448
+ can_run_sql: true,
30449
+ can_validate_charts: true,
30450
+ can_visualize: true,
30451
+ can_save_charts: canWrite(deps.user)
30452
+ };
30453
+ const result = {
30454
+ role,
30455
+ permissions,
30456
+ required_role: { save_chart: "editor_or_admin" }
30457
+ };
30458
+ const raw = JSON.stringify(result);
30459
+ return {
30460
+ content: [{ type: "text", text: raw }],
30461
+ structuredContent: result
30462
+ };
30463
+ });
29646
30464
  server.registerTool("save_chart", {
29647
30465
  description: "Save a yamchart chart spec to charts/<name>.yaml (requires editor/admin).",
29648
- inputSchema: { spec: z2.record(z2.unknown()) }
29649
- }, async (args) => text(await saveChart(deps, args)));
30466
+ inputSchema: { spec: z2.record(z2.unknown()) },
30467
+ outputSchema: TEXT_OUTPUT,
30468
+ annotations: WRITE_SAFE
30469
+ }, async (args) => textOut(await saveChart(deps, args)));
29650
30470
  if (deps.semanticLayer) {
29651
30471
  const semanticLayer = deps.semanticLayer;
29652
30472
  server.registerTool("list_metrics", {
29653
30473
  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 })));
30474
+ inputSchema: {},
30475
+ outputSchema: { metrics: z2.array(z2.unknown()) },
30476
+ annotations: READ_ONLY
30477
+ }, async () => {
30478
+ const raw = listMetrics({ semanticLayer });
30479
+ const metrics = JSON.parse(raw);
30480
+ return {
30481
+ content: [{ type: "text", text: raw }],
30482
+ structuredContent: { metrics }
30483
+ };
30484
+ });
29656
30485
  server.registerTool("get_metric", {
29657
30486
  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() }
29659
- }, async (args) => text(getMetric({ semanticLayer }, args)));
30487
+ inputSchema: { name: z2.string() },
30488
+ outputSchema: TEXT_OUTPUT,
30489
+ annotations: READ_ONLY
30490
+ }, async (args) => textOut(getMetric({ semanticLayer }, args)));
29660
30491
  server.registerTool("query_metric", {
29661
30492
  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.",
29662
30493
  inputSchema: {
@@ -29670,10 +30501,34 @@ function createMcpServer(deps) {
29670
30501
  filters: z2.array(z2.object({ dimension: z2.string(), operator: z2.string(), value: z2.any() })).optional(),
29671
30502
  order_by: z2.object({ field: z2.string(), direction: z2.enum(["asc", "desc"]).optional() }).optional(),
29672
30503
  limit: z2.number().int().positive().max(1e4).optional()
29673
- }
30504
+ },
30505
+ outputSchema: {
30506
+ rowCount: z2.number(),
30507
+ columns: z2.array(z2.string()),
30508
+ rows: z2.array(z2.record(z2.unknown())),
30509
+ preview: z2.array(z2.record(z2.unknown())),
30510
+ sql: z2.string(),
30511
+ suggested_chart: z2.unknown()
30512
+ },
30513
+ annotations: READ_ONLY
29674
30514
  }, async (args) => {
29675
30515
  try {
29676
- return text(await queryMetric({ semanticLayer, queryService: deps.queryService, connectionType: deps.connectionType }, args));
30516
+ const raw = await queryMetric({ semanticLayer, queryService: deps.queryService, connectionType: deps.connectionType }, args);
30517
+ if (raw.startsWith("Error:") || raw.startsWith("{") && JSON.parse(raw).error) {
30518
+ return { content: [{ type: "text", text: raw }], isError: true };
30519
+ }
30520
+ const parsed = JSON.parse(raw);
30521
+ return {
30522
+ content: [{ type: "text", text: raw }],
30523
+ structuredContent: {
30524
+ rowCount: parsed.rows.length,
30525
+ columns: parsed.columns.map((c) => c.name),
30526
+ rows: parsed.rows,
30527
+ preview: parsed.rows.slice(0, 5),
30528
+ sql: parsed.sql,
30529
+ suggested_chart: parsed.suggested_chart
30530
+ }
30531
+ };
29677
30532
  } catch (err) {
29678
30533
  return text(`Error: ${err instanceof Error ? err.message : String(err)}`);
29679
30534
  }
@@ -29685,28 +30540,65 @@ function createMcpServer(deps) {
29685
30540
  const answerProvider = deps.answerProvider;
29686
30541
  server.registerTool("answer_question", {
29687
30542
  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)));
30543
+ inputSchema: { question: z2.string(), connection: z2.string().optional() },
30544
+ outputSchema: {
30545
+ rowCount: z2.number(),
30546
+ columns: z2.array(z2.string()),
30547
+ rows: z2.array(z2.record(z2.unknown())),
30548
+ preview: z2.array(z2.record(z2.unknown())),
30549
+ sql: z2.unknown().optional(),
30550
+ answer: z2.unknown().optional()
30551
+ },
30552
+ annotations: READ_ONLY
30553
+ }, async (args) => {
30554
+ const raw = await answerQuestionTool({
30555
+ knowledgeStore,
30556
+ embedder,
30557
+ provider: answerProvider,
30558
+ configLoader: deps.configLoader,
30559
+ queryService: deps.queryService,
30560
+ connectionType: deps.connectionType,
30561
+ defaultConnectionName: deps.aiConnectionName
30562
+ }, args);
30563
+ if (raw.startsWith("Error:"))
30564
+ return { content: [{ type: "text", text: raw }], isError: true };
30565
+ const parsed = JSON.parse(raw);
30566
+ if (parsed["error_type"]) {
30567
+ return { content: [{ type: "text", text: raw }], isError: true };
30568
+ }
30569
+ const rows = parsed.rows ?? [];
30570
+ const columns = parsed.columns ?? [];
30571
+ return {
30572
+ content: [{ type: "text", text: raw }],
30573
+ structuredContent: {
30574
+ rowCount: rows.length,
30575
+ columns: columns.map((c) => c.name),
30576
+ rows,
30577
+ preview: rows.slice(0, 5),
30578
+ sql: parsed.sql,
30579
+ answer: parsed.answer
30580
+ }
30581
+ };
30582
+ });
29697
30583
  }
29698
30584
  server.registerTool("get_chart_schema", {
29699
30585
  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() }
29701
- }, async (args) => text(getChartSchema(args)));
30586
+ inputSchema: { type: z2.string().optional() },
30587
+ outputSchema: TEXT_OUTPUT,
30588
+ annotations: READ_ONLY
30589
+ }, async (args) => textOut(getChartSchema(args)));
29702
30590
  server.registerTool("validate_chart", {
29703
30591
  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()) }
29705
- }, async (args) => text(validateChart(args)));
30592
+ inputSchema: { spec: z2.record(z2.unknown()) },
30593
+ outputSchema: TEXT_OUTPUT,
30594
+ annotations: READ_ONLY
30595
+ }, async (args) => textOut(validateChart(args)));
29706
30596
  server.registerTool("get_chart_spec", {
29707
30597
  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() }
29709
- }, async (args) => text(getChartSpec({ configLoader: deps.configLoader }, args)));
30598
+ inputSchema: { name: z2.string() },
30599
+ outputSchema: TEXT_OUTPUT,
30600
+ annotations: READ_ONLY
30601
+ }, async (args) => textOut(getChartSpec({ configLoader: deps.configLoader }, args)));
29710
30602
  return server;
29711
30603
  }
29712
30604
 
@@ -29730,7 +30622,8 @@ async function mcpRoutes(fastify, options) {
29730
30622
  knowledgeStore,
29731
30623
  embedder,
29732
30624
  answerProvider,
29733
- aiConnectionName
30625
+ aiConnectionName,
30626
+ baseUrl: process.env["BASE_URL"]
29734
30627
  });
29735
30628
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
29736
30629
  reply.raw.on("close", () => {
@@ -29754,7 +30647,7 @@ async function mcpRoutes(fastify, options) {
29754
30647
  }
29755
30648
 
29756
30649
  // ../server/dist/routes/catalog.js
29757
- import { join as join10 } from "path";
30650
+ import { join as join12 } from "path";
29758
30651
  import { readFile as readFile5 } from "fs/promises";
29759
30652
  async function catalogRoutes(fastify, options) {
29760
30653
  const { configLoader, projectDir } = options;
@@ -29767,7 +30660,7 @@ async function catalogRoutes(fastify, options) {
29767
30660
  name: m.metadata.name,
29768
30661
  description: m.metadata.description
29769
30662
  }));
29770
- const catalogPath = join10(projectDir, ".yamchart", "catalog.json");
30663
+ const catalogPath = join12(projectDir, ".yamchart", "catalog.json");
29771
30664
  let tables = [];
29772
30665
  try {
29773
30666
  const catalogData = JSON.parse(await readFile5(catalogPath, "utf-8"));
@@ -29803,6 +30696,22 @@ async function catalogRoutes(fastify, options) {
29803
30696
  });
29804
30697
  }
29805
30698
 
30699
+ // ../server/dist/services/dbt-path.js
30700
+ import { readFile as readFile6 } from "fs/promises";
30701
+ import { join as join13, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
30702
+ import { parse as parseYaml3 } from "yaml";
30703
+ async function resolveDbtProjectPath(projectDir) {
30704
+ try {
30705
+ const raw = await readFile6(join13(projectDir, ".yamchart", "dbt-source.yaml"), "utf-8");
30706
+ const parsed = parseYaml3(raw);
30707
+ if (!parsed?.path)
30708
+ return null;
30709
+ return isAbsolute2(parsed.path) ? parsed.path : resolve2(projectDir, parsed.path);
30710
+ } catch {
30711
+ return null;
30712
+ }
30713
+ }
30714
+
29806
30715
  // ../server/dist/routes/learning.js
29807
30716
  var ReadOnlySqlError = class extends Error {
29808
30717
  constructor() {
@@ -29826,10 +30735,14 @@ async function runHunch(deps, id) {
29826
30735
  if (!isReadOnlySql(hunch.sql))
29827
30736
  return { error: "Only read-only SELECT queries can be run." };
29828
30737
  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 };
30738
+ try {
30739
+ const res = await deps.queryService.executeRawSql(limited);
30740
+ const sample = { columns: res.columns, rows: res.rows, ranAt: (/* @__PURE__ */ new Date()).toISOString() };
30741
+ await deps.store.updateHunch(id, { lastRunSample: sample });
30742
+ return { sample };
30743
+ } catch (e) {
30744
+ return { error: e.message };
30745
+ }
29833
30746
  }
29834
30747
  async function answerBlindspot(deps, id, body) {
29835
30748
  return deps.store.updateBlindspot(id, { status: "answered", answer: body.answer });
@@ -29837,6 +30750,51 @@ async function answerBlindspot(deps, id, body) {
29837
30750
  async function dismissBlindspot(deps, id) {
29838
30751
  return deps.store.updateBlindspot(id, { status: "dismissed" });
29839
30752
  }
30753
+ async function acceptBlindspot(deps, id) {
30754
+ const bs = await deps.store.getBlindspot(id);
30755
+ if (!bs || bs.kind !== "opportunity" || !bs.suggestedModel) {
30756
+ return { status: 400, body: { error: "Not an opportunity blindspot with a suggested model." } };
30757
+ }
30758
+ if (!/^[a-zA-Z0-9_]+$/.test(bs.suggestedModel.name)) {
30759
+ return {
30760
+ status: 400,
30761
+ body: { error: "Invalid suggested model name \u2014 dbt model names may contain only letters, digits, and underscores." }
30762
+ };
30763
+ }
30764
+ const dbtPath = await resolveDbtProjectPath(deps.projectDir);
30765
+ if (!dbtPath) {
30766
+ return {
30767
+ status: 400,
30768
+ body: { error: 'No connected dbt project. Run "yamchart sync-dbt" to link one.' }
30769
+ };
30770
+ }
30771
+ try {
30772
+ const sm = bs.suggestedModel;
30773
+ const conventions = await detectConventions(dbtPath);
30774
+ const modelPath = buildModelPath(sm.name, void 0, conventions);
30775
+ const layer = modelPath.split("/")[1] ?? "marts";
30776
+ const materialization = conventions.commonMaterializations[layer] ?? "view";
30777
+ const sql = formatModelSql({
30778
+ materialization,
30779
+ sql: scaffoldModelSql(sm),
30780
+ description: sm.description
30781
+ });
30782
+ await writeDbtModel(dbtPath, modelPath, sql);
30783
+ const schemaPath = modelPath.replace(/[^/]+\.sql$/, "schema.yml");
30784
+ await updateSchemaYml(dbtPath, schemaPath, {
30785
+ name: sm.name,
30786
+ description: sm.description,
30787
+ columns: sm.columns
30788
+ });
30789
+ const blindspot = await deps.store.updateBlindspot(id, {
30790
+ status: "answered",
30791
+ answer: `Scaffolded dbt model: ${modelPath}`
30792
+ });
30793
+ return { status: 200, body: { modelPath, schemaPath, blindspot } };
30794
+ } catch (e) {
30795
+ return { status: 500, body: { error: e.message } };
30796
+ }
30797
+ }
29840
30798
  async function loadContextBlock(opts, store, connection) {
29841
30799
  const fileDocs = loadKnowledgeDocs(opts.projectDir);
29842
30800
  const dbDocs = (await store.listKnowledgeDocs({ connection })).map((d) => `${d.title}
@@ -29895,7 +30853,17 @@ async function learningRoutes(fastify, opts) {
29895
30853
  contextBlock
29896
30854
  });
29897
30855
  const created = await store.createHunches(candidates);
29898
- send("done", { hunches: created });
30856
+ const threshold = opts.getConnectionThreshold?.(connection);
30857
+ let autoConfirmed = 0;
30858
+ if (threshold != null) {
30859
+ for (const h of created) {
30860
+ if ((h.confidence ?? 0) >= threshold) {
30861
+ await store.updateHunch(h.id, { status: "confirmed" });
30862
+ autoConfirmed++;
30863
+ }
30864
+ }
30865
+ }
30866
+ send("done", { hunches: created, autoConfirmed });
29899
30867
  } catch (e) {
29900
30868
  send("error", { message: e.message });
29901
30869
  }
@@ -29940,7 +30908,12 @@ async function learningRoutes(fastify, opts) {
29940
30908
  reply.code(403).send({ error: "Requires editor or admin role." });
29941
30909
  return;
29942
30910
  }
29943
- return runHunch({ store, queryService }, request.params.id);
30911
+ const result = await runHunch({ store, queryService }, request.params.id);
30912
+ if (result.error) {
30913
+ reply.code(422).send({ error: result.error });
30914
+ return;
30915
+ }
30916
+ return result;
29944
30917
  });
29945
30918
  fastify.post("/api/blindspots/scan", async (request, reply) => {
29946
30919
  if (!requireStore(reply))
@@ -30017,6 +30990,46 @@ async function learningRoutes(fastify, opts) {
30017
30990
  }
30018
30991
  return dismissBlindspot({ store }, request.params.id);
30019
30992
  });
30993
+ fastify.post("/api/blindspots/:id/accept", async (request, reply) => {
30994
+ if (!requireStore(reply))
30995
+ return;
30996
+ if (!opts.canWrite(request)) {
30997
+ reply.code(403).send({ error: "Requires editor or admin role." });
30998
+ return;
30999
+ }
31000
+ const result = await acceptBlindspot({ store, projectDir: opts.projectDir }, request.params.id);
31001
+ reply.code(result.status).send(result.body);
31002
+ });
31003
+ fastify.get("/api/coverage", async (request, reply) => {
31004
+ if (!requireStore(reply))
31005
+ return;
31006
+ const q = request.query;
31007
+ const connection = q.connection ?? opts.defaultConnectionName;
31008
+ const [hunches, blindspots] = await Promise.all([
31009
+ store.listHunches({ connection }),
31010
+ store.listBlindspots({ connection })
31011
+ ]);
31012
+ const profile = await store.getProfile(connection);
31013
+ const profileTables = profile?.tables?.map((t) => t.table) ?? [];
31014
+ const tableCounts = {};
31015
+ const ensure = (t) => {
31016
+ tableCounts[t] ??= { hunches: 0, blindspots: 0 };
31017
+ };
31018
+ profileTables.forEach(ensure);
31019
+ for (const h of hunches) {
31020
+ for (const t of h.tablesUsed ?? []) {
31021
+ ensure(t);
31022
+ tableCounts[t].hunches++;
31023
+ }
31024
+ }
31025
+ for (const b of blindspots) {
31026
+ if (b.subject) {
31027
+ ensure(b.subject);
31028
+ tableCounts[b.subject].blindspots++;
31029
+ }
31030
+ }
31031
+ return Object.entries(tableCounts).map(([name, counts]) => ({ name, ...counts })).sort((a, b) => b.hunches + b.blindspots - (a.hunches + a.blindspots));
31032
+ });
30020
31033
  }
30021
31034
 
30022
31035
  // ../server/dist/routes/data.js
@@ -30105,6 +31118,257 @@ async function dataRoutes(fastify, opts) {
30105
31118
  });
30106
31119
  }
30107
31120
 
31121
+ // ../server/dist/routes/admin-config.js
31122
+ function redactUrls(msg) {
31123
+ return msg.replace(/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, "[url redacted]");
31124
+ }
31125
+ function maskSecrets(secrets) {
31126
+ if (!secrets)
31127
+ return void 0;
31128
+ const out = {};
31129
+ for (const [k, v] of Object.entries(secrets))
31130
+ if (typeof v === "string")
31131
+ out[k] = maskSecret(v);
31132
+ return out;
31133
+ }
31134
+ function maskProviderKey(p) {
31135
+ if (p && typeof p.api_key === "string")
31136
+ return { ...p, api_key: maskSecret(p.api_key) };
31137
+ return p;
31138
+ }
31139
+ function maskAiConfig(ai) {
31140
+ if (!ai)
31141
+ return null;
31142
+ const providers = ai.providers ? Object.fromEntries(Object.entries(ai.providers).map(([n, p]) => [n, maskProviderKey(p)])) : void 0;
31143
+ return { ...ai, providers, embedding: maskProviderKey(ai.embedding) };
31144
+ }
31145
+ function mergeAiSecrets(incoming, stored) {
31146
+ const keep = (inc, st) => {
31147
+ if (!inc)
31148
+ return inc;
31149
+ if (inc.api_key === void 0 || inc.api_key === "" || inc.api_key.startsWith("\u2022\u2022\u2022\u2022")) {
31150
+ return { ...inc, api_key: st?.api_key };
31151
+ }
31152
+ return inc;
31153
+ };
31154
+ const providers = incoming.providers ? Object.fromEntries(Object.entries(incoming.providers).map(([n, p]) => [n, keep(p, stored?.providers?.[n])])) : void 0;
31155
+ return { ...incoming, providers, embedding: keep(incoming.embedding, stored?.embedding) };
31156
+ }
31157
+ async function adminConfigRoutes(fastify, opts) {
31158
+ const guard = (req, reply) => {
31159
+ if (!opts.canAdmin(req)) {
31160
+ reply.code(403).send({ error: "Requires admin role." });
31161
+ return false;
31162
+ }
31163
+ return true;
31164
+ };
31165
+ fastify.get("/api/admin/config", async (req, reply) => {
31166
+ if (!guard(req, reply))
31167
+ return;
31168
+ const runtime = opts.runtimeStore.read();
31169
+ const runtimeByName = new Map((runtime.connections ?? []).map((c) => [c.name, c]));
31170
+ const git = opts.gitConnections();
31171
+ const names = /* @__PURE__ */ new Set([...git.map((g) => g.name), ...runtimeByName.keys()]);
31172
+ const connections = [...names].map((name) => {
31173
+ const rt = runtimeByName.get(name);
31174
+ if (rt) {
31175
+ return { name, type: rt.type, source: "runtime", config: rt.config, secrets: maskSecrets(rt.secrets) };
31176
+ }
31177
+ const g = git.find((x) => x.name === name);
31178
+ return { name, type: g.type, source: "git" };
31179
+ });
31180
+ return {
31181
+ connections,
31182
+ knowledge: { configured: opts.knowledgeDbConfigured, urlSet: !!runtime.knowledge?.database_url },
31183
+ ai: maskAiConfig(opts.aiConfig)
31184
+ };
31185
+ });
31186
+ fastify.post("/api/admin/restart", async (req, reply) => {
31187
+ if (!guard(req, reply))
31188
+ return;
31189
+ return opts.requestRestart();
31190
+ });
31191
+ const mergeStoredSecrets = (conn, cfg) => {
31192
+ const existing = cfg.connections?.find((c) => c.name === conn.name);
31193
+ const incoming = Object.fromEntries(
31194
+ // Drop blank AND masked-sentinel ("••••…") values so neither wipes nor overwrites the
31195
+ // stored secret (the existing value is kept via the spread below).
31196
+ Object.entries(conn.secrets ?? {}).filter(([, v]) => typeof v === "string" && v !== "" && !v.startsWith("\u2022\u2022\u2022\u2022"))
31197
+ );
31198
+ const merged = { ...existing?.secrets ?? {}, ...incoming };
31199
+ const secrets = Object.keys(merged).length > 0 ? merged : void 0;
31200
+ return { ...conn, secrets };
31201
+ };
31202
+ const upsertConnection = (conn) => {
31203
+ const cfg = opts.runtimeStore.read();
31204
+ const merged = mergeStoredSecrets(conn, cfg);
31205
+ const list = (cfg.connections ?? []).filter((c) => c.name !== merged.name);
31206
+ list.push(merged);
31207
+ opts.runtimeStore.write({ ...cfg, connections: list });
31208
+ };
31209
+ const createOrUpdate = async (req, reply) => {
31210
+ if (!guard(req, reply))
31211
+ return;
31212
+ const parsed = RuntimeConnectionSchema.safeParse(req.body);
31213
+ if (!parsed.success) {
31214
+ reply.code(400).send({ error: "Invalid connection", details: parsed.error.flatten() });
31215
+ return;
31216
+ }
31217
+ upsertConnection(parsed.data);
31218
+ return opts.requestRestart();
31219
+ };
31220
+ fastify.post("/api/admin/connections", createOrUpdate);
31221
+ fastify.put("/api/admin/connections/:name", async (req, reply) => {
31222
+ if (!guard(req, reply))
31223
+ return;
31224
+ const parsed = RuntimeConnectionSchema.safeParse(req.body);
31225
+ if (!parsed.success) {
31226
+ reply.code(400).send({ error: "Invalid connection", details: parsed.error.flatten() });
31227
+ return;
31228
+ }
31229
+ const { name } = req.params;
31230
+ if (parsed.data.name !== name) {
31231
+ reply.code(400).send({ error: `Connection name mismatch: URL "${name}" vs body "${parsed.data.name}". Renaming is not supported.` });
31232
+ return;
31233
+ }
31234
+ upsertConnection(parsed.data);
31235
+ return opts.requestRestart();
31236
+ });
31237
+ fastify.delete("/api/admin/connections/:name", async (req, reply) => {
31238
+ if (!guard(req, reply))
31239
+ return;
31240
+ const { name } = req.params;
31241
+ const refs = opts.findConnectionRefs(name);
31242
+ if (refs.length > 0) {
31243
+ reply.code(409).send({ error: "Connection is in use", references: refs });
31244
+ return;
31245
+ }
31246
+ const cfg = opts.runtimeStore.read();
31247
+ opts.runtimeStore.write({ ...cfg, connections: (cfg.connections ?? []).filter((c) => c.name !== name) });
31248
+ return opts.requestRestart();
31249
+ });
31250
+ fastify.post("/api/admin/connections/test", async (req, reply) => {
31251
+ if (!guard(req, reply))
31252
+ return;
31253
+ const parsed = RuntimeConnectionSchema.safeParse(req.body);
31254
+ if (!parsed.success) {
31255
+ reply.code(400).send({ error: "Invalid connection", details: parsed.error.flatten() });
31256
+ return;
31257
+ }
31258
+ const cfg = opts.runtimeStore.read();
31259
+ const merged = mergeStoredSecrets(parsed.data, cfg);
31260
+ const { secrets, ...connLike } = merged;
31261
+ return testConnection(connLike, { projectDir: opts.projectDir, secrets });
31262
+ });
31263
+ fastify.post("/api/admin/knowledge-db/test", async (req, reply) => {
31264
+ if (!guard(req, reply))
31265
+ return;
31266
+ const url = req.body?.url;
31267
+ if (!url || url.trim() === "") {
31268
+ reply.code(400).send({ error: "A database URL is required." });
31269
+ return;
31270
+ }
31271
+ const target = opts.createKnowledgeStore(url);
31272
+ try {
31273
+ await target.init();
31274
+ return { ok: true };
31275
+ } catch (err) {
31276
+ return { ok: false, error: redactUrls(err instanceof Error ? err.message : "Connection failed") };
31277
+ } finally {
31278
+ try {
31279
+ await target.close();
31280
+ } catch {
31281
+ }
31282
+ }
31283
+ });
31284
+ fastify.put("/api/admin/knowledge-db", async (req, reply) => {
31285
+ if (!guard(req, reply))
31286
+ return;
31287
+ const url = req.body?.url;
31288
+ if (!url || url.trim() === "") {
31289
+ reply.code(400).send({ error: "A non-empty database URL is required." });
31290
+ return;
31291
+ }
31292
+ const cfg = opts.runtimeStore.read();
31293
+ opts.runtimeStore.write({ ...cfg, knowledge: { database_url: url } });
31294
+ return opts.requestRestart();
31295
+ });
31296
+ fastify.post("/api/admin/knowledge-db/migrate", async (req, reply) => {
31297
+ if (!guard(req, reply))
31298
+ return;
31299
+ const url = req.body?.url;
31300
+ if (!url) {
31301
+ reply.code(400).send({ error: "A target database URL is required." });
31302
+ return;
31303
+ }
31304
+ if (!opts.knowledgeStore) {
31305
+ reply.code(503).send({ error: "No current knowledge database to copy from." });
31306
+ return;
31307
+ }
31308
+ const target = opts.createKnowledgeStore(url);
31309
+ try {
31310
+ await target.init();
31311
+ const snapshot = await opts.knowledgeStore.exportAll();
31312
+ await target.importAll(snapshot);
31313
+ return {
31314
+ migrated: {
31315
+ hunches: snapshot.hunches.length,
31316
+ blindspots: snapshot.blindspots.length,
31317
+ knowledgeDocs: snapshot.knowledgeDocs.length,
31318
+ embeddings: snapshot.embeddings.length,
31319
+ profiles: snapshot.profiles.length
31320
+ }
31321
+ };
31322
+ } catch (err) {
31323
+ reply.code(500).send({ error: redactUrls(err instanceof Error ? err.message : "Migration failed") });
31324
+ } finally {
31325
+ try {
31326
+ await target.close();
31327
+ } catch {
31328
+ }
31329
+ }
31330
+ });
31331
+ fastify.put("/api/admin/ai", async (req, reply) => {
31332
+ if (!guard(req, reply))
31333
+ return;
31334
+ const parsed = AiConfigSchema.safeParse(req.body);
31335
+ if (!parsed.success) {
31336
+ reply.code(400).send({ error: "Invalid AI config", details: parsed.error.flatten() });
31337
+ return;
31338
+ }
31339
+ const cfg = opts.runtimeStore.read();
31340
+ const merged = mergeAiSecrets(parsed.data, cfg.ai);
31341
+ opts.runtimeStore.write({ ...cfg, ai: merged });
31342
+ return opts.requestRestart();
31343
+ });
31344
+ fastify.post("/api/admin/ai/test", async (req, reply) => {
31345
+ if (!guard(req, reply))
31346
+ return;
31347
+ const body = req.body;
31348
+ const parsed = ProviderConfigSchema.safeParse(body?.provider);
31349
+ if (!parsed.success) {
31350
+ reply.code(400).send({ error: "Invalid provider config", details: parsed.error.flatten() });
31351
+ return;
31352
+ }
31353
+ const kind = body?.kind === "embedding" ? "embedding" : "llm";
31354
+ return opts.testAiProvider(parsed.data, kind);
31355
+ });
31356
+ fastify.post("/api/admin/knowledge-db/reembed", async (req, reply) => {
31357
+ if (!guard(req, reply))
31358
+ return;
31359
+ if (!opts.knowledgeStore || !opts.embedder) {
31360
+ reply.code(503).send({ error: "Reindex requires a knowledge database and a configured embedder." });
31361
+ return;
31362
+ }
31363
+ try {
31364
+ const embedded = await reindexEmbeddings({ store: opts.knowledgeStore, embedder: opts.embedder });
31365
+ return { embedded };
31366
+ } catch (err) {
31367
+ reply.code(500).send({ error: redactUrls(err instanceof Error ? err.message : "Reindex failed") });
31368
+ }
31369
+ });
31370
+ }
31371
+
30108
31372
  // ../server/dist/oauth/service.js
30109
31373
  import { randomUUID as randomUUID3, timingSafeEqual as timingSafeEqual2 } from "crypto";
30110
31374
 
@@ -30631,18 +31895,18 @@ data: ${data}
30631
31895
  }
30632
31896
 
30633
31897
  // ../server/dist/server.js
30634
- import { join as join11, dirname as dirname2 } from "path";
31898
+ import { join as join14, dirname as dirname4 } from "path";
30635
31899
  import { fileURLToPath } from "url";
30636
- import { access as access2, readFile as readFile6 } from "fs/promises";
31900
+ import { access as access2, readFile as readFile7 } from "fs/promises";
30637
31901
  import { watch as fsWatch } from "fs";
30638
- var __dirname = dirname2(fileURLToPath(import.meta.url));
30639
- var packageJsonPath = join11(__dirname, "..", "package.json");
30640
- var packageJson = JSON.parse(await readFile6(packageJsonPath, "utf-8"));
31902
+ var __dirname = dirname4(fileURLToPath(import.meta.url));
31903
+ var packageJsonPath = join14(__dirname, "..", "package.json");
31904
+ var packageJson = JSON.parse(await readFile7(packageJsonPath, "utf-8"));
30641
31905
  var VERSION = packageJson.version;
30642
31906
  async function enrichRefsFromCatalog(projectDir, refs) {
30643
31907
  try {
30644
- const catalogPath = join11(projectDir, ".yamchart", "catalog.json");
30645
- const catalogData = JSON.parse(await readFile6(catalogPath, "utf-8"));
31908
+ const catalogPath = join14(projectDir, ".yamchart", "catalog.json");
31909
+ const catalogData = JSON.parse(await readFile7(catalogPath, "utf-8"));
30646
31910
  for (const model of catalogData.models ?? []) {
30647
31911
  if (model.name && model.table) {
30648
31912
  refs[model.name] = model.table;
@@ -30652,7 +31916,7 @@ async function enrichRefsFromCatalog(projectDir, refs) {
30652
31916
  }
30653
31917
  }
30654
31918
  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;
31919
+ const { projectDir, port = 3001, host = "0.0.0.0", watch: watch2 = false, serveStatic = process.env.NODE_ENV === "production", staticDir = join14(__dirname, "public"), auth } = options;
30656
31920
  if (auth) {
30657
31921
  initAuthServer(auth.supabaseUrl, auth.supabaseServiceKey);
30658
31922
  }
@@ -30682,12 +31946,35 @@ async function createServer(options) {
30682
31946
  }
30683
31947
  const configLoader = new ConfigLoader(projectDir, options.env);
30684
31948
  await configLoader.load();
31949
+ const runtimeAuthDbPath = options.localAuth?.dbPath;
31950
+ const runtimeConfigPath = RuntimeConfigStore.resolvePath({
31951
+ env: process.env,
31952
+ projectDir,
31953
+ authDbPath: runtimeAuthDbPath
31954
+ });
31955
+ const configKey = getConfigKey(dirname4(runtimeConfigPath), process.env, (m) => fastify.log.warn(m));
31956
+ const runtimeStore = new RuntimeConfigStore(runtimeConfigPath, configKey);
31957
+ const applyRuntime = () => {
31958
+ try {
31959
+ configLoader.applyRuntimeOverrides(runtimeStore.read());
31960
+ } catch (err) {
31961
+ if (err instanceof RuntimeConfigError) {
31962
+ fastify.log.error(`Runtime config ignored (Git-only fallback): ${err.message}`);
31963
+ } else
31964
+ throw err;
31965
+ }
31966
+ };
31967
+ applyRuntime();
31968
+ const effectiveAi = configLoader.getAiConfig();
30685
31969
  const gitService = new GitService(projectDir);
30686
31970
  const defaultConnection = configLoader.getDefaultConnection();
30687
31971
  if (!defaultConnection) {
30688
31972
  throw new Error("No connection configured");
30689
31973
  }
30690
- const rawConnector = createConnectorFromConfig(defaultConnection, { projectDir });
31974
+ const rawConnector = createConnectorFromConfig(defaultConnection, {
31975
+ projectDir,
31976
+ secrets: configLoader.getConnectionSecrets(defaultConnection.name)
31977
+ });
30691
31978
  const connector = new ReconnectingConnector(rawConnector);
30692
31979
  await connector.connect();
30693
31980
  const project = configLoader.getProject();
@@ -30697,7 +31984,7 @@ async function createServer(options) {
30697
31984
  defaultTtlMs: cacheTtl
30698
31985
  });
30699
31986
  const knowledgeUrlVar = project.knowledge?.database_url_var ?? "KNOWLEDGE_DATABASE_URL";
30700
- const knowledgeDbUrl = process.env[knowledgeUrlVar];
31987
+ const knowledgeDbUrl = configLoader.getKnowledgeDbUrl(knowledgeUrlVar, process.env);
30701
31988
  let knowledgeStore;
30702
31989
  if (knowledgeDbUrl) {
30703
31990
  const store = new PostgresKnowledgeStore(knowledgeDbUrl);
@@ -30751,20 +32038,6 @@ async function createServer(options) {
30751
32038
  authType: defaultConnection.type === "snowflake" && defaultConnection.auth ? defaultConnection.auth.type : void 0
30752
32039
  };
30753
32040
  });
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
32041
  await fastify.register(semanticRoutes, { semanticService, queryService, projectDir });
30769
32042
  if (authDb) {
30770
32043
  const secureCookies = process.env.NODE_ENV === "production";
@@ -30827,31 +32100,31 @@ async function createServer(options) {
30827
32100
  await fastify.register(async (protectedRoutes) => {
30828
32101
  protectedRoutes.addHook("preHandler", authMiddleware);
30829
32102
  protectedRoutes.addHook("preHandler", orgMiddleware);
30830
- await protectedRoutes.register(configRoutes, { configLoader, projectDir, queryService });
32103
+ await protectedRoutes.register(configRoutes, { configLoader, projectDir, queryService, getConnectionSecrets: (name) => configLoader.getConnectionSecrets(name) });
30831
32104
  await protectedRoutes.register(chartRoutes, { configLoader, queryService });
30832
32105
  await protectedRoutes.register(chartEventsRoutes, { configLoader, projectDir });
30833
32106
  await protectedRoutes.register(dashboardRoutes, { configLoader, gitService, projectDir, queryService });
30834
- await protectedRoutes.register(chatRoutes, { configLoader, queryService, projectDir });
32107
+ await protectedRoutes.register(chatRoutes, { configLoader, queryService, projectDir, aiConfig: effectiveAi });
30835
32108
  await protectedRoutes.register(catalogRoutes, { configLoader, projectDir });
30836
32109
  });
30837
32110
  } else if (authDb) {
30838
32111
  const localMiddleware = createLocalAuthMiddleware(authDb);
30839
32112
  await fastify.register(async (protectedRoutes) => {
30840
32113
  protectedRoutes.addHook("preHandler", localMiddleware);
30841
- await protectedRoutes.register(configRoutes, { configLoader, projectDir, queryService });
32114
+ await protectedRoutes.register(configRoutes, { configLoader, projectDir, queryService, getConnectionSecrets: (name) => configLoader.getConnectionSecrets(name) });
30842
32115
  await protectedRoutes.register(chartRoutes, { configLoader, queryService });
30843
32116
  await protectedRoutes.register(chartEventsRoutes, { configLoader, projectDir });
30844
32117
  await protectedRoutes.register(dashboardRoutes, { configLoader, gitService, projectDir, queryService });
30845
- await protectedRoutes.register(chatRoutes, { configLoader, queryService, projectDir });
32118
+ await protectedRoutes.register(chatRoutes, { configLoader, queryService, projectDir, aiConfig: effectiveAi });
30846
32119
  await protectedRoutes.register(catalogRoutes, { configLoader, projectDir });
30847
32120
  await protectedRoutes.register(sharesRoutes, { authDb });
30848
32121
  });
30849
32122
  } else {
30850
- await fastify.register(configRoutes, { configLoader, projectDir, queryService });
32123
+ await fastify.register(configRoutes, { configLoader, projectDir, queryService, getConnectionSecrets: (name) => configLoader.getConnectionSecrets(name) });
30851
32124
  await fastify.register(chartRoutes, { configLoader, queryService });
30852
32125
  await fastify.register(chartEventsRoutes, { configLoader, projectDir });
30853
32126
  await fastify.register(dashboardRoutes, { configLoader, gitService, projectDir, queryService });
30854
- await fastify.register(chatRoutes, { configLoader, queryService, projectDir });
32127
+ await fastify.register(chatRoutes, { configLoader, queryService, projectDir, aiConfig: effectiveAi });
30855
32128
  await fastify.register(catalogRoutes, { configLoader, projectDir });
30856
32129
  }
30857
32130
  const mcpBaseUrl = process.env.BASE_URL || project.defaults?.base_url || `http://localhost:${port}`;
@@ -30868,12 +32141,12 @@ async function createServer(options) {
30868
32141
  let embedder;
30869
32142
  let answerProvider;
30870
32143
  try {
30871
- embedder = createEmbedder(project.ai?.embedding, process.env);
32144
+ embedder = createEmbedder(effectiveAi?.embedding, process.env);
30872
32145
  } catch (err) {
30873
32146
  fastify.log.warn({ err }, "Failed to build embedder; answer_question tool disabled");
30874
32147
  }
30875
32148
  try {
30876
- answerProvider = createProviderForTask(project.ai, "answer", process.env);
32149
+ answerProvider = createProviderForTask(effectiveAi, "answer", process.env);
30877
32150
  } catch (err) {
30878
32151
  fastify.log.warn({ err }, "Failed to build answer provider; answer_question tool disabled");
30879
32152
  }
@@ -30906,9 +32179,9 @@ async function createServer(options) {
30906
32179
  dialect: defaultConnection?.type ?? "duckdb",
30907
32180
  metricNames: semanticLayer?.listMetrics().map((m) => m.name) ?? [],
30908
32181
  getConnectionTables: async (connection) => {
30909
- const catalogPath = join11(projectDir, ".yamchart", "catalog.json");
32182
+ const catalogPath = join14(projectDir, ".yamchart", "catalog.json");
30910
32183
  try {
30911
- const catalogData = JSON.parse(await readFile6(catalogPath, "utf-8"));
32184
+ const catalogData = JSON.parse(await readFile7(catalogPath, "utf-8"));
30912
32185
  return (catalogData.models ?? []).filter((m) => !m.connection || m.connection === connection).map((m) => ({
30913
32186
  table: m.table ?? m.name ?? "",
30914
32187
  columns: (m.columns ?? []).map((c) => ({ name: c.name, type: c.data_type ?? "string" }))
@@ -30925,7 +32198,11 @@ async function createServer(options) {
30925
32198
  return true;
30926
32199
  return user.role !== "viewer";
30927
32200
  },
30928
- aiConfig: project.ai
32201
+ aiConfig: effectiveAi,
32202
+ getConnectionThreshold: (name) => {
32203
+ const conn = configLoader.getConnections().find((c) => c.name === name);
32204
+ return conn?.hunch_confidence_threshold;
32205
+ }
30929
32206
  };
30930
32207
  const dataOptions = {
30931
32208
  store: knowledgeStore,
@@ -30933,12 +32210,76 @@ async function createServer(options) {
30933
32210
  getConnectionTables: learningOptions.getConnectionTables,
30934
32211
  canWrite: learningOptions.canWrite
30935
32212
  };
32213
+ const adminConfigOptions = {
32214
+ runtimeStore,
32215
+ gitConnections: () => configLoader.getConnections().map((c) => ({ name: c.name, type: c.type })),
32216
+ knowledgeDbConfigured: !!knowledgeStore,
32217
+ aiConfig: effectiveAi,
32218
+ projectDir,
32219
+ findConnectionRefs: (name) => findConnectionRefs(name, {
32220
+ charts: configLoader.getCharts().map((c) => ({
32221
+ name: c.name,
32222
+ source: { connection: c.source.connection }
32223
+ })),
32224
+ models: configLoader.getModels().map((m) => ({
32225
+ metadata: {
32226
+ name: m.metadata.name,
32227
+ connection: m.metadata.connection
32228
+ }
32229
+ })),
32230
+ defaultConnection: configLoader.getDefaultConnection()?.name
32231
+ }),
32232
+ requestRestart: () => requestRestart(process.env),
32233
+ // Admin-only: when auth disabled (no req.user) allow (dev); else require admin role.
32234
+ canAdmin: (req) => {
32235
+ const user = req.user;
32236
+ if (!user)
32237
+ return true;
32238
+ return user.role === "admin";
32239
+ },
32240
+ knowledgeStore,
32241
+ embedder,
32242
+ createKnowledgeStore: (url) => new PostgresKnowledgeStore(url),
32243
+ testAiProvider: async (cfg, kind) => {
32244
+ try {
32245
+ if (kind === "embedding") {
32246
+ const e = createEmbedder(cfg, process.env);
32247
+ if (!e)
32248
+ return { ok: false, error: "Could not build embedder for this config." };
32249
+ await e.embed(["ping"]);
32250
+ } else {
32251
+ const provider = buildProvider(cfg, process.env);
32252
+ await provider.chat({ system: "", messages: [{ role: "user", content: "ping" }], tools: [], maxTokens: 1 });
32253
+ }
32254
+ return { ok: true };
32255
+ } catch (err) {
32256
+ const msg = err instanceof Error ? err.message : "Test failed";
32257
+ return { ok: false, error: redactProviderError(msg, cfg.api_key) };
32258
+ }
32259
+ }
32260
+ };
32261
+ const registerReconnect = async (scope) => {
32262
+ scope.post("/api/connections/reconnect", async (_request2, reply) => {
32263
+ try {
32264
+ try {
32265
+ await connector.disconnect();
32266
+ } catch {
32267
+ }
32268
+ await connector.connect();
32269
+ return { success: true };
32270
+ } catch (err) {
32271
+ return reply.status(500).send({ error: err instanceof Error ? err.message : "Reconnection failed" });
32272
+ }
32273
+ });
32274
+ };
30936
32275
  if (auth) {
30937
32276
  await fastify.register(async (protectedRoutes) => {
30938
32277
  protectedRoutes.addHook("preHandler", authMiddleware);
30939
32278
  protectedRoutes.addHook("preHandler", orgMiddleware);
30940
32279
  await protectedRoutes.register(learningRoutes, learningOptions);
30941
32280
  await protectedRoutes.register(dataRoutes, dataOptions);
32281
+ await protectedRoutes.register(adminConfigRoutes, adminConfigOptions);
32282
+ await registerReconnect(protectedRoutes);
30942
32283
  });
30943
32284
  } else if (authDb) {
30944
32285
  const localMiddleware = createLocalAuthMiddleware(authDb);
@@ -30946,12 +32287,16 @@ async function createServer(options) {
30946
32287
  protectedRoutes.addHook("preHandler", localMiddleware);
30947
32288
  await protectedRoutes.register(learningRoutes, learningOptions);
30948
32289
  await protectedRoutes.register(dataRoutes, dataOptions);
32290
+ await protectedRoutes.register(adminConfigRoutes, adminConfigOptions);
32291
+ await registerReconnect(protectedRoutes);
30949
32292
  });
30950
32293
  } else {
30951
32294
  await fastify.register(learningRoutes, learningOptions);
30952
32295
  await fastify.register(dataRoutes, dataOptions);
32296
+ await fastify.register(adminConfigRoutes, adminConfigOptions);
32297
+ await registerReconnect(fastify);
30953
32298
  }
30954
- const assetsDir = join11(projectDir, "assets");
32299
+ const assetsDir = join14(projectDir, "assets");
30955
32300
  try {
30956
32301
  await access2(assetsDir);
30957
32302
  await fastify.register(fastifyStatic, {
@@ -30962,10 +32307,10 @@ async function createServer(options) {
30962
32307
  } catch {
30963
32308
  }
30964
32309
  if (project.theme?.customCss) {
30965
- const cssPath = join11(projectDir, project.theme.customCss);
32310
+ const cssPath = join14(projectDir, project.theme.customCss);
30966
32311
  fastify.get("/api/theme/custom.css", async (_request2, reply) => {
30967
32312
  try {
30968
- const content = await readFile6(cssPath, "utf-8");
32313
+ const content = await readFile7(cssPath, "utf-8");
30969
32314
  return reply.type("text/css").send(content);
30970
32315
  } catch {
30971
32316
  return reply.status(404).send("");
@@ -31009,6 +32354,7 @@ async function createServer(options) {
31009
32354
  configLoader.startWatching();
31010
32355
  configLoader.onChange(async () => {
31011
32356
  fastify.log.info("Config reloaded");
32357
+ applyRuntime();
31012
32358
  const { models: newModels, refs: newRefs } = await buildModelsAndRefs();
31013
32359
  queryService.updateCompiler({ models: newModels, refs: newRefs });
31014
32360
  queryService.invalidateAll();
@@ -31021,7 +32367,7 @@ async function createServer(options) {
31021
32367
  fastify.log.info(`Semantic model rebuilt: ${semanticService.getModel()?.entities.length ?? 0} entities`);
31022
32368
  broadcastConfigChange();
31023
32369
  });
31024
- const catalogPath = join11(projectDir, ".yamchart", "catalog.json");
32370
+ const catalogPath = join14(projectDir, ".yamchart", "catalog.json");
31025
32371
  try {
31026
32372
  catalogWatcher = fsWatch(catalogPath, () => {
31027
32373
  semanticService.buildFromDisk();
@@ -31085,15 +32431,15 @@ async function runDevServer(projectDir, options) {
31085
32431
  let usesBrowserAuth = false;
31086
32432
  let localAuth;
31087
32433
  try {
31088
- const { readFileSync: readFileSync3 } = await import("fs");
32434
+ const { readFileSync: readFileSync5 } = await import("fs");
31089
32435
  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");
32436
+ const { resolveProjectConfig: resolveProjectConfig2, deepMerge: deepMerge3 } = await import("./dist-F6TVLP6C.js");
32437
+ const raw = readFileSync5(resolve3(projectDir, "yamchart.yaml"), "utf-8");
31092
32438
  const rawConfig = parse2(raw);
31093
32439
  const resolvedEnv = options.env || process.env.YAMCHART_ENV || void 0;
31094
32440
  let projectConfig = resolveProjectConfig2(rawConfig, resolvedEnv);
31095
32441
  try {
31096
- const localRaw = readFileSync3(resolve2(projectDir, "yamchart.local.yaml"), "utf-8");
32442
+ const localRaw = readFileSync5(resolve3(projectDir, "yamchart.local.yaml"), "utf-8");
31097
32443
  const localOverrides = parse2(localRaw);
31098
32444
  if (localOverrides && typeof localOverrides === "object") {
31099
32445
  projectConfig = deepMerge3(projectConfig, localOverrides);
@@ -31103,15 +32449,15 @@ async function runDevServer(projectDir, options) {
31103
32449
  if (projectConfig?.auth?.enabled) {
31104
32450
  localAuth = {
31105
32451
  enabled: true,
31106
- dbPath: projectConfig.auth.db_path ? resolve2(projectConfig.auth.db_path.replace(/^~/, homedir())) : resolve2(homedir(), ".yamchart", "auth.db"),
32452
+ dbPath: projectConfig.auth.db_path ? resolve3(projectConfig.auth.db_path.replace(/^~/, homedir())) : resolve3(homedir(), ".yamchart", "auth.db"),
31107
32453
  sessionTtlMs: projectConfig.auth.session_ttl ? parseTtl(projectConfig.auth.session_ttl) : 30 * 24 * 60 * 60 * 1e3
31108
32454
  };
31109
32455
  }
31110
- const connectionDir = resolve2(projectDir, "connections");
32456
+ const connectionDir = resolve3(projectDir, "connections");
31111
32457
  const targetConn = process.env.YAMCHART_CONNECTION || projectConfig?.defaults?.connection;
31112
32458
  if (targetConn) {
31113
32459
  try {
31114
- const connRaw = readFileSync3(resolve2(connectionDir, `${targetConn}.yaml`), "utf-8");
32460
+ const connRaw = readFileSync5(resolve3(connectionDir, `${targetConn}.yaml`), "utf-8");
31115
32461
  const connConfig = parse2(connRaw);
31116
32462
  if (connConfig?.auth?.type === "externalbrowser") {
31117
32463
  usesBrowserAuth = true;
@@ -31191,4 +32537,4 @@ async function runDevServer(projectDir, options) {
31191
32537
  export {
31192
32538
  runDevServer
31193
32539
  };
31194
- //# sourceMappingURL=dev-H6SJZ5UA.js.map
32540
+ //# sourceMappingURL=dev-PHFYLJPJ.js.map