yamchart 0.8.7 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/{advisor-54JBE2EV.js → advisor-Z7TKPPBR.js} +10 -10
  2. package/dist/agent-KWKPAYT2.js +8 -0
  3. package/dist/{chunk-NCPWAWIM.js → chunk-AMHCOB4D.js} +4 -4
  4. package/dist/{chunk-5N3FYFBV.js → chunk-CWAWATL4.js} +34 -7
  5. package/dist/chunk-CWAWATL4.js.map +1 -0
  6. package/dist/{chunk-C7A7TKSY.js → chunk-E2QN2M7S.js} +77 -7
  7. package/dist/chunk-E2QN2M7S.js.map +1 -0
  8. package/dist/{chunk-YMQ4PWVJ.js → chunk-FZFBBB7K.js} +2 -2
  9. package/dist/{chunk-GLCEDWGH.js → chunk-G57J2WQM.js} +4 -4
  10. package/dist/chunk-ZA6AOQVZ.js +677 -0
  11. package/dist/chunk-ZA6AOQVZ.js.map +1 -0
  12. package/dist/{connection-utils-MXEF6X7K.js → connection-utils-CTPN7PV3.js} +4 -4
  13. package/dist/{describe-XKLBZEWG.js → describe-4NME6RCB.js} +5 -5
  14. package/dist/{dev-Z2R2DBWO.js → dev-6QGAB4ZH.js} +845 -70
  15. package/dist/dev-6QGAB4ZH.js.map +1 -0
  16. package/dist/{dist-LJR7TAW4.js → dist-4GUE24QV.js} +4 -2
  17. package/dist/{dist-GVNWQXFR.js → dist-7CRX2GIR.js} +2 -2
  18. package/dist/{dist-E2PVGIPT.js → dist-VNX77VV5.js} +4 -2
  19. package/dist/index.js +21 -21
  20. package/dist/public/assets/{EventManagement-DtPDwZ-w.js → EventManagement-MMsAkJKj.js} +2 -2
  21. package/dist/public/assets/ExplorePage-BSkSNgLT.js +1 -0
  22. package/dist/public/assets/{LoginPage-DBq1qDOK.js → LoginPage-vaI1dnyL.js} +1 -1
  23. package/dist/public/assets/PublicViewer-B-OKj2cg.js +1 -0
  24. package/dist/public/assets/{SetupWizard-hgd12cdr.js → SetupWizard-DvlVX2O6.js} +1 -1
  25. package/dist/public/assets/{ShareManagement-D82oEJJg.js → ShareManagement-ulvPrOAQ.js} +1 -1
  26. package/dist/public/assets/{UserManagement-CZsxY9aP.js → UserManagement-CvmpNy3o.js} +1 -1
  27. package/dist/public/assets/{index-B_fusLA_.css → index-CfyF2Wf-.css} +1 -1
  28. package/dist/public/assets/index-DD59fsOk.js +195 -0
  29. package/dist/public/assets/{index.es-BmKO-vE1.js → index.es-BeTaRWIv.js} +1 -1
  30. package/dist/public/assets/{jspdf.es.min-DMVrmE3G.js → jspdf.es.min-9haD1GSE.js} +3 -3
  31. package/dist/public/index.html +2 -2
  32. package/dist/{query-MXMFI5TB.js → query-Z75RKTHV.js} +4 -4
  33. package/dist/{sample-HDPYNAKS.js → sample-OIJNXQNC.js} +4 -4
  34. package/dist/{search-6CPEPJTI.js → search-YDCPIDZX.js} +5 -5
  35. package/dist/{source-resolver-PCASPRSD.js → source-resolver-4SUWXUGW.js} +5 -5
  36. package/dist/source-resolver-4SUWXUGW.js.map +1 -0
  37. package/dist/{sync-warehouse-XC7YYZKC.js → sync-warehouse-NZFDS6WK.js} +4 -4
  38. package/dist/{tables-26PNVZIC.js → tables-WJS2VI4L.js} +5 -5
  39. package/dist/templates/default/CLAUDE.md +1 -1
  40. package/dist/templates/default/docs/yamchart-reference.md +164 -2
  41. package/dist/templates/default/yamchart.yaml +3 -0
  42. package/dist/{test-APA44AIF.js → test-I4XOF7TZ.js} +5 -17
  43. package/dist/test-I4XOF7TZ.js.map +1 -0
  44. package/package.json +3 -2
  45. package/dist/chunk-5N3FYFBV.js.map +0 -1
  46. package/dist/chunk-C7A7TKSY.js.map +0 -1
  47. package/dist/dev-Z2R2DBWO.js.map +0 -1
  48. package/dist/public/assets/PublicViewer-Da8Cu1C1.js +0 -1
  49. package/dist/public/assets/index-DVSm0iiw.js +0 -187
  50. package/dist/test-APA44AIF.js.map +0 -1
  51. /package/dist/{advisor-54JBE2EV.js.map → advisor-Z7TKPPBR.js.map} +0 -0
  52. /package/dist/{connection-utils-MXEF6X7K.js.map → agent-KWKPAYT2.js.map} +0 -0
  53. /package/dist/{chunk-NCPWAWIM.js.map → chunk-AMHCOB4D.js.map} +0 -0
  54. /package/dist/{chunk-YMQ4PWVJ.js.map → chunk-FZFBBB7K.js.map} +0 -0
  55. /package/dist/{chunk-GLCEDWGH.js.map → chunk-G57J2WQM.js.map} +0 -0
  56. /package/dist/{dist-E2PVGIPT.js.map → connection-utils-CTPN7PV3.js.map} +0 -0
  57. /package/dist/{describe-XKLBZEWG.js.map → describe-4NME6RCB.js.map} +0 -0
  58. /package/dist/{dist-LJR7TAW4.js.map → dist-4GUE24QV.js.map} +0 -0
  59. /package/dist/{dist-GVNWQXFR.js.map → dist-7CRX2GIR.js.map} +0 -0
  60. /package/dist/{source-resolver-PCASPRSD.js.map → dist-VNX77VV5.js.map} +0 -0
  61. /package/dist/{query-MXMFI5TB.js.map → query-Z75RKTHV.js.map} +0 -0
  62. /package/dist/{sample-HDPYNAKS.js.map → sample-OIJNXQNC.js.map} +0 -0
  63. /package/dist/{search-6CPEPJTI.js.map → search-YDCPIDZX.js.map} +0 -0
  64. /package/dist/{sync-warehouse-XC7YYZKC.js.map → sync-warehouse-NZFDS6WK.js.map} +0 -0
  65. /package/dist/{tables-26PNVZIC.js.map → tables-WJS2VI4L.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  loadEnvFile,
3
3
  validateProject
4
- } from "./chunk-GLCEDWGH.js";
4
+ } from "./chunk-G57J2WQM.js";
5
5
  import {
6
6
  box,
7
7
  detail,
@@ -23,7 +23,11 @@ import {
23
23
  SemanticQuerySchema,
24
24
  deepMerge,
25
25
  resolveProjectConfig
26
- } from "./chunk-C7A7TKSY.js";
26
+ } from "./chunk-E2QN2M7S.js";
27
+ import {
28
+ StreamingChatAgent,
29
+ filterCatalogEntries
30
+ } from "./chunk-ZA6AOQVZ.js";
27
31
  import {
28
32
  AuthDatabase,
29
33
  generateSessionToken,
@@ -53,7 +57,7 @@ import {
53
57
  resolveMySQLAuth,
54
58
  resolvePostgresAuth,
55
59
  resolveSnowflakeAuth
56
- } from "./chunk-5N3FYFBV.js";
60
+ } from "./chunk-CWAWATL4.js";
57
61
  import {
58
62
  SemanticModelBuilder,
59
63
  SemanticQueryCompiler,
@@ -63,7 +67,7 @@ import {
63
67
  import "./chunk-DGUM43GV.js";
64
68
 
65
69
  // src/commands/dev.ts
66
- import { resolve } from "path";
70
+ import { resolve as resolve2 } from "path";
67
71
  import { homedir } from "os";
68
72
 
69
73
  // ../server/dist/server.js
@@ -76,6 +80,257 @@ import { readFile, readdir, access } from "fs/promises";
76
80
  import { join, extname, relative, dirname } from "path";
77
81
  import { parse as parseYaml } from "yaml";
78
82
  import { watch } from "chokidar";
83
+
84
+ // ../../packages/chat/dist/registry.js
85
+ import { parse } from "yaml";
86
+
87
+ // ../../packages/chat/dist/agents/dashboard-analyst.js
88
+ var DASHBOARD_ANALYST = {
89
+ name: "dashboard-analyst",
90
+ title: "Dashboard Analyst",
91
+ description: "Summarizes dashboards, explains trends, answers data questions, and can apply filters to the dashboard.",
92
+ systemPrompt: `You are an AI dashboard analyst for a business intelligence platform.
93
+
94
+ Your job is to help users understand their data by:
95
+ - Summarizing what a dashboard or chart shows
96
+ - Explaining trends, outliers, and patterns in the data
97
+ - Answering questions about metrics and KPIs
98
+ - Suggesting and applying filters to explore data
99
+ - Running ad-hoc SQL to explore data beyond existing charts
100
+
101
+ ## Rules
102
+ - Be concise. Dashboard users want quick answers, not essays.
103
+ - Use numbers and percentages when discussing data.
104
+ - When you query data, summarize the key findings \u2014 don't dump raw rows.
105
+ - If asked about a trend, explain the direction, magnitude, and possible causes.
106
+ - When suggesting filters, explain what you expect to see.
107
+ - Use the apply_filter tool when the user asks to filter or drill down.
108
+ - If the existing charts don't have the data needed, use run_sql to query the database directly.
109
+ - Use call_agent to delegate to specialized agents when you need expertise you don't have.
110
+ - Use suggest_actions to offer follow-up exploration or save options after answering.
111
+ - If you don't have enough data to answer, say so and suggest what to query.
112
+
113
+ ## Data Visualization in Responses
114
+ When you have tabular data to share, use these markdown code fences:
115
+
116
+ For charts (line, bar, pie, scatter):
117
+ \`\`\`chart
118
+ {"type":"line","data":[{"date":"2025-01-01","value":100},...],"x":"date","y":"value","title":"Optional Title"}
119
+ \`\`\`
120
+
121
+ For data tables:
122
+ \`\`\`data
123
+ {"columns":[{"name":"date","type":"date"},{"name":"value","type":"number"}],"rows":[{"date":"2025-01-01","value":100},...]}
124
+ \`\`\`
125
+
126
+ Choose charts for trends and comparisons, tables for detailed breakdowns. Keep data concise \u2014 summarize large result sets.`,
127
+ 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"],
128
+ builtIn: true
129
+ };
130
+
131
+ // ../../packages/chat/dist/agents/sql-explorer.js
132
+ var SQL_EXPLORER = {
133
+ name: "sql-explorer",
134
+ title: "SQL Explorer",
135
+ description: "Runs ad-hoc SQL queries against the connected database for deep data exploration and period comparisons.",
136
+ systemPrompt: `You are a SQL expert helping users explore their data warehouse.
137
+
138
+ Your job is to:
139
+ - Write and execute SQL queries to answer user questions
140
+ - Compare data across time periods
141
+ - Find specific records or aggregations
142
+ - Explain query results clearly
143
+
144
+ ## Rules
145
+ - Always LIMIT your queries (default: 50 rows) to avoid large payloads.
146
+ - Use clear column aliases so results are readable.
147
+ - When comparing periods, use the compare_periods tool for models, or write explicit SQL for ad-hoc comparisons.
148
+ - Explain what your query does before showing results.
149
+ - If a query fails, explain the error and try a corrected version.
150
+ - Never run destructive queries (INSERT, UPDATE, DELETE, DROP, etc.).
151
+
152
+ ## Data Visualization in Responses
153
+ When you have tabular data to share, use these markdown code fences:
154
+
155
+ For charts (line, bar, pie, scatter):
156
+ \`\`\`chart
157
+ {"type":"line","data":[{"date":"2025-01-01","value":100},...],"x":"date","y":"value","title":"Optional Title"}
158
+ \`\`\`
159
+
160
+ For data tables:
161
+ \`\`\`data
162
+ {"columns":[{"name":"date","type":"date"},{"name":"value","type":"number"}],"rows":[{"date":"2025-01-01","value":100},...]}
163
+ \`\`\`
164
+
165
+ Choose charts for trends and comparisons, tables for detailed breakdowns. Keep data concise \u2014 summarize large result sets.`,
166
+ tools: ["query_model", "run_sql", "compare_periods", "get_chart_config", "call_agent", "get_catalog_tables", "suggest_actions", "save_model", "save_chart"],
167
+ builtIn: true
168
+ };
169
+
170
+ // ../../packages/chat/dist/agents/data-explorer.js
171
+ var DATA_EXPLORER = {
172
+ name: "data-explorer",
173
+ title: "Data Explorer",
174
+ description: "Open-ended data exploration, visualization, and dashboard building.",
175
+ systemPrompt: `You are an AI data explorer for a business intelligence platform.
176
+
177
+ 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.
178
+
179
+ ## Discovery First
180
+ - Always start with get_catalog_tables to understand what data is available before writing SQL.
181
+ - Prefer querying named models (query_model) over raw SQL when a model already exists.
182
+ - When no model exists, use run_sql to explore \u2014 then offer to save the query as a model.
183
+
184
+ ## Rules
185
+ - All SQL must be read-only (SELECT only). Never run INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, or CREATE.
186
+ - Always LIMIT queries to 50 rows by default. Use LIMIT explicitly.
187
+ - Use clear column aliases so results are readable.
188
+ - When a query fails, explain the error and try a corrected version.
189
+ - Explain what you're querying and why before showing results.
190
+
191
+ ## Proactive Visualization
192
+ After retrieving data, always visualize it \u2014 don't just dump raw rows. Choose the right form:
193
+ - Trends over time \u2192 line chart
194
+ - Comparisons across categories \u2192 bar chart
195
+ - Part-to-whole \u2192 pie chart
196
+ - Single key metric \u2192 show as bold text or KPI
197
+ - Detailed breakdowns \u2192 data table
198
+
199
+ For charts (line, bar, pie, scatter):
200
+ \`\`\`chart
201
+ {"type":"line","data":[{"date":"2025-01-01","value":100},...],"x":"date","y":"value","title":"Optional Title"}
202
+ \`\`\`
203
+
204
+ For data tables:
205
+ \`\`\`data
206
+ {"columns":[{"name":"date","type":"date"},{"name":"value","type":"number"}],"rows":[{"date":"2025-01-01","value":100},...]}
207
+ \`\`\`
208
+
209
+ Keep visualizations concise \u2014 summarize large result sets rather than showing every row.
210
+
211
+ ## Saving Work
212
+ When a user wants to keep a query or chart:
213
+ 1. Use save_model to create a reusable SQL model file.
214
+ 2. Use save_chart to create a chart YAML referencing that model.
215
+ 3. Use create_dashboard to assemble saved charts into a new dashboard.
216
+ 4. After saving, use suggest_actions to offer navigation or further exploration.
217
+
218
+ ## Suggesting Next Steps
219
+ Always use suggest_actions after answering a question to offer natural follow-up options:
220
+ - Explore related dimensions or time periods
221
+ - Save the current query as a model
222
+ - Create a chart from the data
223
+ - Add the chart to a dashboard
224
+
225
+ ## Collaboration
226
+ Use call_agent to delegate to specialized agents when needed:
227
+ - Dashboard Analyst for questions about existing dashboards
228
+ - SQL Explorer for complex multi-step SQL investigations`,
229
+ tools: ["run_sql", "query_model", "get_catalog_tables", "compare_periods", "save_model", "save_chart", "create_dashboard", "suggest_actions", "call_agent"],
230
+ builtIn: true
231
+ };
232
+
233
+ // ../../packages/chat/dist/registry.js
234
+ var AgentRegistry = class {
235
+ agents = /* @__PURE__ */ new Map();
236
+ constructor() {
237
+ this.agents.set(DASHBOARD_ANALYST.name, DASHBOARD_ANALYST);
238
+ this.agents.set(SQL_EXPLORER.name, SQL_EXPLORER);
239
+ this.agents.set(DATA_EXPLORER.name, DATA_EXPLORER);
240
+ }
241
+ getAgent(name) {
242
+ return this.agents.get(name);
243
+ }
244
+ listAgents() {
245
+ return Array.from(this.agents.values());
246
+ }
247
+ registerAgent(config) {
248
+ this.agents.set(config.name, config);
249
+ }
250
+ loadFromYaml(yamlContent, filePath, onWarning) {
251
+ try {
252
+ const raw = parse(yamlContent);
253
+ if (!raw || typeof raw !== "object") {
254
+ onWarning?.(`Skipping ${filePath}: not a valid YAML object`);
255
+ return;
256
+ }
257
+ if (!raw.name || typeof raw.name !== "string") {
258
+ onWarning?.(`Skipping ${filePath}: missing required "name" field`);
259
+ return;
260
+ }
261
+ if (!raw.description || typeof raw.description !== "string") {
262
+ onWarning?.(`Skipping ${filePath}: missing required "description" field`);
263
+ return;
264
+ }
265
+ if (!raw.system_prompt || typeof raw.system_prompt !== "string") {
266
+ onWarning?.(`Skipping ${filePath}: missing required "system_prompt" field`);
267
+ return;
268
+ }
269
+ const config = {
270
+ name: raw.name,
271
+ title: raw.title ?? raw.name,
272
+ description: raw.description,
273
+ systemPrompt: raw.system_prompt,
274
+ tools: Array.isArray(raw.tools) ? raw.tools.filter((t) => typeof t === "string") : [],
275
+ builtIn: false,
276
+ model: typeof raw.model === "string" ? raw.model : void 0,
277
+ models: Array.isArray(raw.models) ? raw.models.filter((m) => typeof m === "string") : void 0
278
+ };
279
+ this.agents.set(config.name, config);
280
+ } catch (err) {
281
+ onWarning?.(`Skipping ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
282
+ }
283
+ }
284
+ clearProjectAgents() {
285
+ for (const [name, config] of this.agents) {
286
+ if (!config.builtIn)
287
+ this.agents.delete(name);
288
+ }
289
+ }
290
+ };
291
+
292
+ // ../../packages/chat/dist/router.js
293
+ var SQL_PATTERNS = [
294
+ /\bselect\b.*\bfrom\b/i,
295
+ /\brun\s+(a\s+)?query\b/i,
296
+ /\bwrite\s+(a\s+)?query\b/i,
297
+ /\bshow\s+(me\s+)?the\s+sql\b/i,
298
+ /\bexecute\s+sql\b/i,
299
+ /\bjoin\b.*\btable/i
300
+ ];
301
+ var DEFAULT_AGENT = "dashboard-analyst";
302
+ var RouterAgent = class {
303
+ registry;
304
+ constructor(registry) {
305
+ this.registry = registry;
306
+ }
307
+ selectAgent(message, context, explicitAgent) {
308
+ if (explicitAgent) {
309
+ const agent = this.registry.getAgent(explicitAgent);
310
+ if (agent)
311
+ return agent;
312
+ }
313
+ if (context?.type === "explore") {
314
+ return this.registry.getAgent("data-explorer") || this.registry.getAgent(DEFAULT_AGENT);
315
+ }
316
+ if (SQL_PATTERNS.some((pattern) => pattern.test(message))) {
317
+ const sqlExplorer = this.registry.getAgent("sql-explorer");
318
+ if (sqlExplorer)
319
+ return sqlExplorer;
320
+ }
321
+ return this.registry.getAgent(DEFAULT_AGENT);
322
+ }
323
+ };
324
+
325
+ // ../../packages/chat/dist/stream.js
326
+ function formatSSEEvent(event) {
327
+ return `event: ${event.type}
328
+ data: ${JSON.stringify(event.data)}
329
+
330
+ `;
331
+ }
332
+
333
+ // ../server/dist/services/config-loader.js
79
334
  var ConfigLoader = class {
80
335
  projectDir;
81
336
  project = null;
@@ -92,6 +347,7 @@ var ConfigLoader = class {
92
347
  validationErrors = /* @__PURE__ */ new Map();
93
348
  watcher = null;
94
349
  onChangeCallbacks = [];
350
+ agentRegistry = new AgentRegistry();
95
351
  env;
96
352
  constructor(projectDir, env) {
97
353
  this.projectDir = projectDir;
@@ -116,6 +372,7 @@ var ConfigLoader = class {
116
372
  await this.loadDashboards();
117
373
  await this.loadSchedules();
118
374
  await this.loadEvents();
375
+ await this.loadAgents();
119
376
  }
120
377
  async loadProject() {
121
378
  const projectPath = join(this.projectDir, "yamchart.yaml");
@@ -321,6 +578,20 @@ var ConfigLoader = class {
321
578
  console.warn(`Invalid events.yaml: ${result.error.message}`);
322
579
  }
323
580
  }
581
+ async loadAgents() {
582
+ const agentsDir = join(this.projectDir, "agents");
583
+ this.agentRegistry.clearProjectAgents();
584
+ try {
585
+ const files = await readdir(agentsDir);
586
+ for (const file of files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"))) {
587
+ const content = await readFile(join(agentsDir, file), "utf-8");
588
+ this.agentRegistry.loadFromYaml(content, `agents/${file}`, (msg) => {
589
+ console.warn(msg);
590
+ });
591
+ }
592
+ } catch {
593
+ }
594
+ }
324
595
  startWatching() {
325
596
  if (this.watcher)
326
597
  return;
@@ -332,7 +603,8 @@ var ConfigLoader = class {
332
603
  join(this.projectDir, "models"),
333
604
  join(this.projectDir, "dashboards"),
334
605
  join(this.projectDir, "schedules"),
335
- join(this.projectDir, "events.yaml")
606
+ join(this.projectDir, "events.yaml"),
607
+ join(this.projectDir, "agents")
336
608
  ];
337
609
  this.watcher = watch(watchPaths, {
338
610
  ignored: /(^|[\/\\])\../,
@@ -453,6 +725,9 @@ Fix: run "ulimit -n 10240" before starting yamchart dev, or add it to your shell
453
725
  getValidationErrors() {
454
726
  return this.validationErrors;
455
727
  }
728
+ getAgentRegistry() {
729
+ return this.agentRegistry;
730
+ }
456
731
  };
457
732
 
458
733
  // ../server/dist/services/cache.js
@@ -529,12 +804,18 @@ function parseTtl2(ttl) {
529
804
  }
530
805
 
531
806
  // ../server/dist/services/query-service.js
807
+ function quoteIdentifier(name, dialect) {
808
+ if (dialect === "mysql")
809
+ return `\`${name.replace(/`/g, "``")}\``;
810
+ return `"${name.replace(/"/g, '""')}"`;
811
+ }
532
812
  var QueryService = class {
533
813
  compiler;
534
814
  connector;
535
815
  cache;
536
816
  models;
537
817
  refs;
818
+ dialect;
538
819
  constructor(config) {
539
820
  this.compiler = new QueryCompiler({
540
821
  models: config.models,
@@ -544,6 +825,7 @@ var QueryService = class {
544
825
  this.cache = config.cache;
545
826
  this.models = new Map(Object.entries(config.models));
546
827
  this.refs = config.refs;
828
+ this.dialect = config.dialect ?? "duckdb";
547
829
  }
548
830
  async executeChart(chart, params, userContext) {
549
831
  const compiled = this.compiler.compile(chart, params, userContext);
@@ -575,6 +857,120 @@ var QueryService = class {
575
857
  compiledSql: compiled.sql
576
858
  };
577
859
  }
860
+ /**
861
+ * Execute a table chart with server-side pagination.
862
+ * Wraps the compiled SQL with LIMIT/OFFSET and runs a parallel COUNT(*) query.
863
+ * Optionally computes summary aggregations for specified columns.
864
+ */
865
+ async executeChartPaginated(chart, params, options, userContext) {
866
+ const compiled = this.compiler.compile(chart, params, userContext);
867
+ const baseSql = compiled.sql;
868
+ const q = quoteIdentifier;
869
+ const d = this.dialect;
870
+ let orderBy = "";
871
+ if (options.sortField) {
872
+ const cleanField = options.sortField.replace(/[^\w\s]/g, "");
873
+ if (cleanField.length > 0) {
874
+ orderBy = `ORDER BY ${q(cleanField, d)} ${options.sortDirection === "desc" ? "DESC" : "ASC"}`;
875
+ }
876
+ }
877
+ const offset = options.page * options.pageSize;
878
+ const dataSql = `SELECT * FROM (${baseSql}) _t ${orderBy} LIMIT ${options.pageSize} OFFSET ${offset}`;
879
+ const countCacheKey = `${compiled.cacheKey}:count`;
880
+ let totalCount;
881
+ const cachedCount = await this.cache.get(countCacheKey);
882
+ if (cachedCount) {
883
+ totalCount = cachedCount.rowCount;
884
+ } else {
885
+ const countSql = `SELECT COUNT(*) AS _count FROM (${baseSql}) _t`;
886
+ const countResult = await this.connector.execute(countSql);
887
+ totalCount = Number(countResult.rows[0]?._count ?? 0);
888
+ await this.cache.set(countCacheKey, {
889
+ columns: [],
890
+ rows: [],
891
+ rowCount: totalCount,
892
+ durationMs: countResult.durationMs,
893
+ cachedAt: Date.now()
894
+ });
895
+ }
896
+ let summaryValues;
897
+ if (options.summaryColumns && options.summaryColumns.length > 0) {
898
+ const summaryCacheKey = `${compiled.cacheKey}:summary`;
899
+ const cachedSummary = await this.cache.get(summaryCacheKey);
900
+ if (cachedSummary && cachedSummary.rows[0]) {
901
+ summaryValues = cachedSummary.rows[0];
902
+ } else {
903
+ const aggExprs = options.summaryColumns.map(({ field, summary }) => {
904
+ const col = q(field, d);
905
+ switch (summary) {
906
+ case "sum":
907
+ return `SUM(${col}) AS ${q(`_sum_${field}`, d)}`;
908
+ case "avg":
909
+ return `AVG(${col}) AS ${q(`_avg_${field}`, d)}`;
910
+ case "min":
911
+ return `MIN(${col}) AS ${q(`_min_${field}`, d)}`;
912
+ case "max":
913
+ return `MAX(${col}) AS ${q(`_max_${field}`, d)}`;
914
+ case "count":
915
+ return `COUNT(${col}) AS ${q(`_count_${field}`, d)}`;
916
+ default:
917
+ return null;
918
+ }
919
+ }).filter(Boolean);
920
+ if (aggExprs.length > 0) {
921
+ const summarySql = `SELECT ${aggExprs.join(", ")} FROM (${baseSql}) _t`;
922
+ const summaryResult = await this.connector.execute(summarySql);
923
+ if (summaryResult.rows[0]) {
924
+ summaryValues = {};
925
+ for (const { field, summary } of options.summaryColumns) {
926
+ const key = `_${summary}_${field}`;
927
+ summaryValues[`${summary}:${field}`] = Number(summaryResult.rows[0][key] ?? 0);
928
+ }
929
+ await this.cache.set(summaryCacheKey, {
930
+ columns: [],
931
+ rows: [summaryValues],
932
+ rowCount: 0,
933
+ durationMs: summaryResult.durationMs,
934
+ cachedAt: Date.now()
935
+ });
936
+ }
937
+ }
938
+ }
939
+ }
940
+ const result = await this.connector.execute(dataSql);
941
+ return {
942
+ ...result,
943
+ cached: false,
944
+ cacheKey: compiled.cacheKey,
945
+ compiledSql: dataSql,
946
+ totalCount,
947
+ page: options.page,
948
+ pageSize: options.pageSize,
949
+ summaryValues
950
+ };
951
+ }
952
+ /**
953
+ * Execute a chart query for CSV export with a bounded row limit.
954
+ * Returns full QueryResult — caller streams to HTTP response.
955
+ */
956
+ async executeChartForExport(chart, params, maxRows, userContext) {
957
+ const compiled = this.compiler.compile(chart, params, userContext);
958
+ const sql = `SELECT * FROM (${compiled.sql}) _t LIMIT ${maxRows}`;
959
+ const result = await this.connector.execute(sql);
960
+ return {
961
+ ...result,
962
+ cached: false,
963
+ cacheKey: compiled.cacheKey,
964
+ compiledSql: sql
965
+ };
966
+ }
967
+ /**
968
+ * Compile a chart query without executing it.
969
+ * Returns the compiled SQL and cache key.
970
+ */
971
+ compileChart(chart, params, userContext) {
972
+ return this.compiler.compile(chart, params, userContext);
973
+ }
578
974
  invalidateChart(chartName) {
579
975
  this.cache.invalidatePattern(`${chartName}:*`);
580
976
  }
@@ -857,6 +1253,12 @@ function classifyError(error2) {
857
1253
  }
858
1254
  return "query_error";
859
1255
  }
1256
+ function escapeCSVField(value) {
1257
+ if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
1258
+ return `"${value.replace(/"/g, '""')}"`;
1259
+ }
1260
+ return value;
1261
+ }
860
1262
  function getUserContext(request) {
861
1263
  const user = request.user;
862
1264
  if (!user)
@@ -921,15 +1323,78 @@ async function chartRoutes(fastify, options) {
921
1323
  });
922
1324
  fastify.post("/api/charts/:name/query", async (request, reply) => {
923
1325
  const { name } = request.params;
924
- const params = request.body ?? {};
1326
+ const rawParams = request.body ?? {};
925
1327
  const includeConfig = request.query.includeConfig === "true";
926
1328
  const chart = configLoader.getChartByName(name);
927
1329
  if (!chart) {
928
1330
  const resp = chartNotFoundResponse(configLoader, name);
929
1331
  return reply.status(resp.status).send(resp.body);
930
1332
  }
1333
+ const { _page, _page_size, _sort_field, _sort_dir, _server_paginated, ...params } = rawParams;
931
1334
  try {
932
- const result = await queryService.executeChart(chart, params, getUserContext(request));
1335
+ const isTable = chart.chart?.type === "table";
1336
+ const project = configLoader.getProject();
1337
+ const chartMaxRows = chart.chart?.max_rows;
1338
+ const maxDisplayRows = chartMaxRows ?? project?.defaults?.table?.max_display_rows ?? 1e4;
1339
+ const forceServerSide = chart.chart?.pagination && chart.chart.pagination?.server_side === true;
1340
+ if (isTable && _server_paginated) {
1341
+ const page = Number(_page) || 0;
1342
+ const pageSize = Number(_page_size) || 50;
1343
+ const columns = chart.chart?.columns ?? [];
1344
+ const summaryColumns = columns.filter((c) => c.summary).map((c) => ({ field: c.field, summary: c.summary }));
1345
+ const paginatedResult = await queryService.executeChartPaginated(chart, params, { page, pageSize, sortField: _sort_field, sortDirection: _sort_dir, summaryColumns }, getUserContext(request));
1346
+ reply.header("X-Cache", "MISS");
1347
+ reply.header("X-Query-Duration-Ms", paginatedResult.durationMs.toFixed(0));
1348
+ const response2 = {
1349
+ columns: paginatedResult.columns,
1350
+ rows: paginatedResult.rows,
1351
+ meta: {
1352
+ cached: paginatedResult.cached,
1353
+ durationMs: paginatedResult.durationMs,
1354
+ rowCount: paginatedResult.rowCount,
1355
+ cacheKey: paginatedResult.cacheKey,
1356
+ compiledSql: paginatedResult.compiledSql,
1357
+ serverPaginated: true,
1358
+ totalCount: paginatedResult.totalCount,
1359
+ page,
1360
+ pageSize
1361
+ },
1362
+ ...paginatedResult.summaryValues ? { summaryValues: paginatedResult.summaryValues } : {}
1363
+ };
1364
+ if (includeConfig) {
1365
+ response2.config = {
1366
+ name: chart.name,
1367
+ title: chart.title,
1368
+ description: chart.description,
1369
+ parameters: chart.parameters ?? [],
1370
+ chart: chart.chart,
1371
+ drillDown: chart.drillDown
1372
+ };
1373
+ }
1374
+ return response2;
1375
+ }
1376
+ let result;
1377
+ let serverPaginated = false;
1378
+ let totalCount;
1379
+ if (isTable) {
1380
+ const probeResult = await queryService.executeChartForExport(chart, params, maxDisplayRows + 1, getUserContext(request));
1381
+ if (probeResult.rowCount > maxDisplayRows || forceServerSide) {
1382
+ serverPaginated = true;
1383
+ if (probeResult.rowCount > maxDisplayRows) {
1384
+ const compiled = queryService.compileChart(chart, params, getUserContext(request));
1385
+ const countResult = await queryService.executeRawSql(`SELECT COUNT(*) AS _count FROM (${compiled.sql}) _t`);
1386
+ totalCount = Number(countResult.rows[0]?._count ?? 0);
1387
+ result = { ...probeResult, rows: probeResult.rows.slice(0, maxDisplayRows), rowCount: maxDisplayRows };
1388
+ } else {
1389
+ totalCount = probeResult.rowCount;
1390
+ result = probeResult;
1391
+ }
1392
+ } else {
1393
+ result = probeResult;
1394
+ }
1395
+ } else {
1396
+ result = await queryService.executeChart(chart, params, getUserContext(request));
1397
+ }
933
1398
  let comparison = void 0;
934
1399
  if (chart.chart?.type === "kpi" && chart.chart?.comparison?.period === "previous") {
935
1400
  let startDate = params.start_date;
@@ -972,6 +1437,54 @@ async function chartRoutes(fastify, options) {
972
1437
  }
973
1438
  }
974
1439
  }
1440
+ const compareConfig = chart.chart?.compare;
1441
+ let compareData;
1442
+ if (compareConfig && compareConfig.length > 0) {
1443
+ let startDate = params.start_date;
1444
+ let endDate = params.end_date;
1445
+ const dateRange = params.date_range;
1446
+ if (!startDate || !endDate) {
1447
+ if (isRelativeDateRange(params.date_range)) {
1448
+ const resolved = expandRelativeDateRange(params.date_range);
1449
+ startDate = resolved.start_date;
1450
+ endDate = resolved.end_date;
1451
+ } else if (isCustomDateRange(params.date_range)) {
1452
+ const resolved = expandCustomDateRange(params.date_range);
1453
+ startDate = resolved.start_date;
1454
+ endDate = resolved.end_date;
1455
+ } else if (dateRange && isDatePreset(dateRange)) {
1456
+ const resolved = expandDatePreset(dateRange);
1457
+ if (resolved) {
1458
+ startDate = resolved.start_date;
1459
+ endDate = resolved.end_date;
1460
+ }
1461
+ }
1462
+ }
1463
+ if (startDate && endDate) {
1464
+ compareData = [];
1465
+ for (const comp of compareConfig) {
1466
+ try {
1467
+ const periodMap = {
1468
+ previous_week: "last_7_days",
1469
+ previous_month: "last_30_days",
1470
+ previous_quarter: "last_90_days",
1471
+ previous_year: "last_365_days"
1472
+ };
1473
+ const prev = computePreviousPeriod(startDate, endDate, periodMap[comp.period] ?? comp.period);
1474
+ const prevLabel = comp.label ?? comp.period.replace(/_/g, " ");
1475
+ const { date_range: _, ...restParams } = params;
1476
+ const prevParams = { ...restParams, start_date: prev.start_date, end_date: prev.end_date };
1477
+ const prevResult = await queryService.executeChart(chart, prevParams, getUserContext(request));
1478
+ compareData.push({
1479
+ period: comp.period,
1480
+ label: prevLabel,
1481
+ rows: prevResult.rows
1482
+ });
1483
+ } catch {
1484
+ }
1485
+ }
1486
+ }
1487
+ }
975
1488
  reply.header("X-Cache", result.cached ? "HIT" : "MISS");
976
1489
  reply.header("X-Query-Duration-Ms", result.durationMs.toFixed(0));
977
1490
  const meta = {
@@ -979,7 +1492,8 @@ async function chartRoutes(fastify, options) {
979
1492
  durationMs: result.durationMs,
980
1493
  rowCount: result.rowCount,
981
1494
  cacheKey: result.cacheKey,
982
- compiledSql: result.compiledSql
1495
+ compiledSql: result.compiledSql,
1496
+ ...serverPaginated ? { serverPaginated: true, totalCount } : {}
983
1497
  };
984
1498
  const response = {
985
1499
  columns: result.columns,
@@ -999,6 +1513,9 @@ async function chartRoutes(fastify, options) {
999
1513
  if (comparison) {
1000
1514
  response.comparison = comparison;
1001
1515
  }
1516
+ if (compareData && compareData.length > 0) {
1517
+ response.compareData = compareData;
1518
+ }
1002
1519
  let goalData = void 0;
1003
1520
  const goals = chart.chart?.goals;
1004
1521
  const modelGoal = goals?.find((g) => g.type === "model" && g.source?.model);
@@ -1056,6 +1573,38 @@ async function chartRoutes(fastify, options) {
1056
1573
  return reply.status(500).send({ error: message, errorType: classifyError(error2) });
1057
1574
  }
1058
1575
  });
1576
+ fastify.get("/api/charts/:name/export", async (request, reply) => {
1577
+ const { name } = request.params;
1578
+ const params = { ...request.query };
1579
+ const chart = configLoader.getChartByName(name);
1580
+ if (!chart) {
1581
+ const resp = chartNotFoundResponse(configLoader, name);
1582
+ return reply.status(resp.status).send(resp.body);
1583
+ }
1584
+ const project = configLoader.getProject();
1585
+ const chartExportConfig = chart.chart?.export;
1586
+ if (chartExportConfig?.enabled === false) {
1587
+ return reply.status(403).send({ error: "Export is disabled for this chart", errorType: "validation_error" });
1588
+ }
1589
+ const maxExportRows = chartExportConfig?.max_rows ?? project?.defaults?.table?.max_export_rows ?? 5e5;
1590
+ try {
1591
+ const result = await queryService.executeChartForExport(chart, params, maxExportRows, getUserContext(request));
1592
+ reply.header("Content-Type", "text/csv; charset=utf-8");
1593
+ reply.header("Content-Disposition", `attachment; filename="${name}.csv"`);
1594
+ reply.header("X-Total-Rows", String(result.rowCount));
1595
+ const columns = result.columns.map((c) => c.name);
1596
+ const headerLine = columns.map(escapeCSVField).join(",");
1597
+ const lines = [headerLine];
1598
+ for (const row of result.rows) {
1599
+ const line = columns.map((col) => escapeCSVField(String(row[col] ?? ""))).join(",");
1600
+ lines.push(line);
1601
+ }
1602
+ return reply.send(lines.join("\n"));
1603
+ } catch (error2) {
1604
+ const message = error2 instanceof Error ? error2.message : "Export failed";
1605
+ return reply.status(500).send({ error: message, errorType: classifyError(error2) });
1606
+ }
1607
+ });
1059
1608
  fastify.post("/api/charts/batch", async (request, reply) => {
1060
1609
  const { charts, includeConfig } = request.body;
1061
1610
  if (!charts || !Array.isArray(charts)) {
@@ -1485,8 +2034,8 @@ async function semanticRoutes(fastify, options) {
1485
2034
  });
1486
2035
  }
1487
2036
  const { writeFileSync, mkdirSync } = await import("fs");
1488
- const { join: join7, dirname: dirname3 } = await import("path");
1489
- const fullPath = join7(projectDir, filePath);
2037
+ const { join: join9, dirname: dirname3 } = await import("path");
2038
+ const fullPath = join9(projectDir, filePath);
1490
2039
  mkdirSync(dirname3(fullPath), { recursive: true });
1491
2040
  writeFileSync(fullPath, sql, "utf-8");
1492
2041
  return { saved: true, path: filePath };
@@ -1599,7 +2148,7 @@ var SlackNotifier = class {
1599
2148
  let response = await fetch(webhookUrl, options);
1600
2149
  if (!response.ok) {
1601
2150
  if (this.retryDelayMs > 0) {
1602
- await new Promise((resolve2) => setTimeout(resolve2, this.retryDelayMs));
2151
+ await new Promise((resolve3) => setTimeout(resolve3, this.retryDelayMs));
1603
2152
  }
1604
2153
  response = await fetch(webhookUrl, options);
1605
2154
  if (!response.ok) {
@@ -1858,11 +2407,11 @@ function __rest(s, e) {
1858
2407
  }
1859
2408
  function __awaiter(thisArg, _arguments, P, generator) {
1860
2409
  function adopt(value) {
1861
- return value instanceof P ? value : new P(function(resolve2) {
1862
- resolve2(value);
2410
+ return value instanceof P ? value : new P(function(resolve3) {
2411
+ resolve3(value);
1863
2412
  });
1864
2413
  }
1865
- return new (P || (P = Promise))(function(resolve2, reject) {
2414
+ return new (P || (P = Promise))(function(resolve3, reject) {
1866
2415
  function fulfilled(value) {
1867
2416
  try {
1868
2417
  step(generator.next(value));
@@ -1878,7 +2427,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
1878
2427
  }
1879
2428
  }
1880
2429
  function step(result) {
1881
- result.done ? resolve2(result.value) : adopt(result.value).then(fulfilled, rejected);
2430
+ result.done ? resolve3(result.value) : adopt(result.value).then(fulfilled, rejected);
1882
2431
  }
1883
2432
  step((generator = generator.apply(thisArg, _arguments || [])).next());
1884
2433
  });
@@ -4409,15 +4958,15 @@ var RealtimeChannel = class _RealtimeChannel {
4409
4958
  }
4410
4959
  }
4411
4960
  } else {
4412
- return new Promise((resolve2) => {
4961
+ return new Promise((resolve3) => {
4413
4962
  var _a2, _b2, _c;
4414
4963
  const push = this._push(args.type, args, opts.timeout || this.timeout);
4415
4964
  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)) {
4416
- resolve2("ok");
4965
+ resolve3("ok");
4417
4966
  }
4418
- push.receive("ok", () => resolve2("ok"));
4419
- push.receive("error", () => resolve2("error"));
4420
- push.receive("timeout", () => resolve2("timed out"));
4967
+ push.receive("ok", () => resolve3("ok"));
4968
+ push.receive("error", () => resolve3("error"));
4969
+ push.receive("timeout", () => resolve3("timed out"));
4421
4970
  });
4422
4971
  }
4423
4972
  }
@@ -4445,16 +4994,16 @@ var RealtimeChannel = class _RealtimeChannel {
4445
4994
  };
4446
4995
  this.joinPush.destroy();
4447
4996
  let leavePush = null;
4448
- return new Promise((resolve2) => {
4997
+ return new Promise((resolve3) => {
4449
4998
  leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout);
4450
4999
  leavePush.receive("ok", () => {
4451
5000
  onClose();
4452
- resolve2("ok");
5001
+ resolve3("ok");
4453
5002
  }).receive("timeout", () => {
4454
5003
  onClose();
4455
- resolve2("timed out");
5004
+ resolve3("timed out");
4456
5005
  }).receive("error", () => {
4457
- resolve2("error");
5006
+ resolve3("error");
4458
5007
  });
4459
5008
  leavePush.send();
4460
5009
  if (!this._canPush()) {
@@ -4533,8 +5082,8 @@ var RealtimeChannel = class _RealtimeChannel {
4533
5082
  _trigger(type, payload, ref) {
4534
5083
  var _a, _b;
4535
5084
  const typeLower = type.toLocaleLowerCase();
4536
- const { close, error: error2, leave, join: join7 } = CHANNEL_EVENTS;
4537
- const events = [close, error2, leave, join7];
5085
+ const { close, error: error2, leave, join: join9 } = CHANNEL_EVENTS;
5086
+ const events = [close, error2, leave, join9];
4538
5087
  if (ref && events.indexOf(typeLower) >= 0 && ref !== this._joinRef()) {
4539
5088
  return;
4540
5089
  }
@@ -6150,7 +6699,7 @@ var _getRequestParams = (method, options, parameters, body) => {
6150
6699
  return _objectSpread2(_objectSpread2({}, params), parameters);
6151
6700
  };
6152
6701
  async function _handleRequest(fetcher, method, url, options, parameters, body, namespace) {
6153
- return new Promise((resolve2, reject) => {
6702
+ return new Promise((resolve3, reject) => {
6154
6703
  fetcher(url, _getRequestParams(method, options, parameters, body)).then((result) => {
6155
6704
  if (!result.ok) throw result;
6156
6705
  if (options === null || options === void 0 ? void 0 : options.noResolveJson) return result;
@@ -6160,7 +6709,7 @@ async function _handleRequest(fetcher, method, url, options, parameters, body, n
6160
6709
  if (!contentType || !contentType.includes("application/json")) return {};
6161
6710
  }
6162
6711
  return result.json();
6163
- }).then((data) => resolve2(data)).catch((error2) => handleError(error2, reject, options, namespace));
6712
+ }).then((data) => resolve3(data)).catch((error2) => handleError(error2, reject, options, namespace));
6164
6713
  });
6165
6714
  }
6166
6715
  function createFetchApi(namespace = "storage") {
@@ -13914,6 +14463,12 @@ async function publicRoutes(fastify, options) {
13914
14463
  }
13915
14464
 
13916
14465
  // ../server/dist/routes/chat.js
14466
+ import { resolve, join as join6 } from "path";
14467
+ import { readFile as readFile3 } from "fs/promises";
14468
+ function isChatEnabled(configLoader) {
14469
+ const project = configLoader.getProject();
14470
+ return !!(project?.features?.chat && process.env.ANTHROPIC_API_KEY);
14471
+ }
13917
14472
  async function chatRoutes(fastify, options) {
13918
14473
  const { configLoader, queryService } = options;
13919
14474
  fastify.post("/api/chat/generate-text", async (request, reply) => {
@@ -14036,7 +14591,7 @@ async function chatRoutes(fastify, options) {
14036
14591
  systemParts.push("Output ONLY the new text to insert. Do NOT repeat the existing content.");
14037
14592
  }
14038
14593
  const systemPrompt = systemParts.join("\n");
14039
- const { AnthropicProvider } = await import("./dist-GVNWQXFR.js");
14594
+ const { AnthropicProvider } = await import("./dist-7CRX2GIR.js");
14040
14595
  const provider = new AnthropicProvider(apiKey);
14041
14596
  const response = await provider.chat({
14042
14597
  system: systemPrompt,
@@ -14052,6 +14607,210 @@ async function chatRoutes(fastify, options) {
14052
14607
  return reply.status(500).send({ error: message });
14053
14608
  }
14054
14609
  });
14610
+ fastify.get("/api/chat/agents", async (request, reply) => {
14611
+ if (!isChatEnabled(configLoader)) {
14612
+ return reply.code(404).send({ error: "Chat not enabled" });
14613
+ }
14614
+ const registry = configLoader.getAgentRegistry();
14615
+ const agents = registry.listAgents().map((a) => ({
14616
+ name: a.name,
14617
+ title: a.title,
14618
+ description: a.description,
14619
+ builtIn: a.builtIn
14620
+ }));
14621
+ return agents;
14622
+ });
14623
+ fastify.post("/api/chat/stream", async (request, reply) => {
14624
+ if (!isChatEnabled(configLoader)) {
14625
+ return reply.code(404).send({ error: "Chat not enabled" });
14626
+ }
14627
+ const body = request.body;
14628
+ if (!body.message) {
14629
+ return reply.code(400).send({ error: "message is required" });
14630
+ }
14631
+ const apiKey = process.env.ANTHROPIC_API_KEY;
14632
+ const registry = configLoader.getAgentRegistry();
14633
+ const router = new RouterAgent(registry);
14634
+ const chatContext = body.context ?? { type: "general" };
14635
+ const agentConfig = router.selectAgent(body.message, chatContext, body.agent);
14636
+ reply.raw.writeHead(200, {
14637
+ "Content-Type": "text/event-stream",
14638
+ "Cache-Control": "no-cache",
14639
+ Connection: "keep-alive"
14640
+ });
14641
+ const emitEvent = (event) => {
14642
+ reply.raw.write(formatSSEEvent(event));
14643
+ };
14644
+ emitEvent({ type: "agent_selected", data: { name: agentConfig.name, title: agentConfig.title } });
14645
+ const project = configLoader.getProject();
14646
+ const toolContext = {
14647
+ configLoader: {
14648
+ getChartByName: (name) => configLoader.getChartByName(name),
14649
+ getDashboardByName: (name) => configLoader.getDashboardByName(name),
14650
+ getDashboards: () => configLoader.getDashboards(),
14651
+ getCharts: () => configLoader.getCharts(),
14652
+ getModels: () => configLoader.getModels?.() ?? [],
14653
+ getProject: () => configLoader.getProject(),
14654
+ load: () => configLoader.load()
14655
+ },
14656
+ queryService: {
14657
+ executeChart: (chart, params, userCtx) => queryService.executeChart(chart, params, userCtx),
14658
+ executeRawSql: (sql) => queryService.executeRawSql(sql)
14659
+ },
14660
+ projectDir: options.projectDir,
14661
+ catalogPath: resolve(options.projectDir, ".yamchart", "catalog.json"),
14662
+ agentRegistry: configLoader.getAgentRegistry(),
14663
+ userContext: request.user ? { id: request.user.id, email: request.user.email, role: request.user.role } : void 0,
14664
+ emitEvent,
14665
+ chatContext,
14666
+ catalogFilter: project?.catalog?.include
14667
+ };
14668
+ let systemPrompt = agentConfig.systemPrompt;
14669
+ if (chatContext.type === "dashboard" && chatContext.name) {
14670
+ const dashboard = configLoader.getDashboardByName(chatContext.name);
14671
+ if (dashboard) {
14672
+ const chartRefs = [];
14673
+ const extractRefs = (layout) => {
14674
+ for (const row of layout.rows ?? []) {
14675
+ for (const widget of row.widgets ?? []) {
14676
+ if (widget.type === "chart" && widget.ref)
14677
+ chartRefs.push(widget.ref);
14678
+ }
14679
+ }
14680
+ };
14681
+ if (dashboard.layout)
14682
+ extractRefs(dashboard.layout);
14683
+ if (dashboard.tabs) {
14684
+ for (const tab of dashboard.tabs) {
14685
+ if (tab.layout)
14686
+ extractRefs(tab.layout);
14687
+ }
14688
+ }
14689
+ const chartDetails = [];
14690
+ for (const ref of chartRefs) {
14691
+ const chart = configLoader.getChartByName(ref);
14692
+ if (!chart)
14693
+ continue;
14694
+ const model = chart.source?.model ? `, model: "${chart.source.model}"` : "";
14695
+ const params = chart.parameters?.length ? `, params: [${chart.parameters.map((p) => `${p.name}(${p.type})`).join(", ")}]` : "";
14696
+ chartDetails.push(` - "${ref}" (${chart.chart?.type ?? "unknown"}${model}${params}): ${chart.title ?? ref}`);
14697
+ }
14698
+ const filters = dashboard.filters?.length ? `
14699
+ Dashboard filters: ${dashboard.filters.map((f) => `${f.name}(${f.type}, default: ${f.default ?? "none"})`).join(", ")}` : "";
14700
+ systemPrompt += `
14701
+
14702
+ The user is viewing the "${dashboard.title ?? dashboard.name}" dashboard (id: "${dashboard.name}").${filters}
14703
+ Charts on this dashboard:
14704
+ ${chartDetails.join("\n")}
14705
+
14706
+ IMPORTANT: When using query_model, pass the chart name (e.g. "${chartRefs[0] ?? "chart-name"}") \u2014 NOT the model name. The chart name is what appears before the parentheses above.`;
14707
+ }
14708
+ } else if (chatContext.type === "chart" && chatContext.name) {
14709
+ const chart = configLoader.getChartByName(chatContext.name);
14710
+ if (chart) {
14711
+ const model = chart.source?.model ? ` Model: "${chart.source.model}".` : "";
14712
+ const params = chart.parameters?.length ? ` Parameters: ${chart.parameters.map((p) => `${p.name}(${p.type})`).join(", ")}.` : "";
14713
+ systemPrompt += `
14714
+
14715
+ The user is viewing the "${chart.title ?? chart.name}" chart (id: "${chart.name}", type: ${chart.chart?.type}).${model}${params}`;
14716
+ }
14717
+ } else if (chatContext.type === "explore") {
14718
+ const catalogPath = join6(options.projectDir, ".yamchart", "catalog.json");
14719
+ const tableLines = [];
14720
+ try {
14721
+ const catalogData = JSON.parse(await readFile3(catalogPath, "utf-8"));
14722
+ const filteredModels = filterCatalogEntries(catalogData.models ?? [], project?.catalog?.include);
14723
+ for (const entry of filteredModels) {
14724
+ const fullName = entry.table ?? entry.name;
14725
+ const cols = (entry.columns ?? []).map((c) => c.name).join(", ");
14726
+ const colSuffix = cols ? ` (${entry.columns?.length} columns: ${cols})` : "";
14727
+ tableLines.push(`- ${fullName}${colSuffix}`);
14728
+ }
14729
+ } catch {
14730
+ }
14731
+ const configModels = configLoader.getModels?.() ?? [];
14732
+ const modelLines = configModels.map((m) => m.metadata.description ? `- ${m.metadata.name}: ${m.metadata.description}` : `- ${m.metadata.name}`);
14733
+ if (tableLines.length === 0 && modelLines.length === 0) {
14734
+ systemPrompt += "\n\nNo catalog data available. Use get_catalog_tables or run_sql to discover tables.";
14735
+ } else {
14736
+ const parts = ["\n"];
14737
+ if (tableLines.length > 0) {
14738
+ parts.push("## Available Tables", ...tableLines);
14739
+ }
14740
+ if (modelLines.length > 0) {
14741
+ if (parts.length > 1)
14742
+ parts.push("");
14743
+ parts.push("## Available Models", ...modelLines);
14744
+ }
14745
+ systemPrompt += parts.join("\n");
14746
+ }
14747
+ }
14748
+ const messages = [];
14749
+ for (const msg of body.history ?? []) {
14750
+ messages.push({ role: msg.role, content: msg.content });
14751
+ }
14752
+ messages.push({ role: "user", content: body.message });
14753
+ try {
14754
+ const agent = new StreamingChatAgent(agentConfig, apiKey);
14755
+ await agent.run(systemPrompt, messages, toolContext, emitEvent);
14756
+ } catch (err) {
14757
+ emitEvent({
14758
+ type: "error",
14759
+ data: { message: err instanceof Error ? err.message : "Stream failed" }
14760
+ });
14761
+ }
14762
+ reply.raw.end();
14763
+ });
14764
+ }
14765
+
14766
+ // ../server/dist/routes/catalog.js
14767
+ import { join as join7 } from "path";
14768
+ import { readFile as readFile4 } from "fs/promises";
14769
+ async function catalogRoutes(fastify, options) {
14770
+ const { configLoader, projectDir } = options;
14771
+ fastify.get("/api/catalog/summary", async (_request2, reply) => {
14772
+ const connections = configLoader.getConnections().map((c) => ({
14773
+ name: c.name,
14774
+ type: c.type
14775
+ }));
14776
+ const configModels = configLoader.getModels().map((m) => ({
14777
+ name: m.metadata.name,
14778
+ description: m.metadata.description
14779
+ }));
14780
+ const catalogPath = join7(projectDir, ".yamchart", "catalog.json");
14781
+ let tables = [];
14782
+ try {
14783
+ const catalogData = JSON.parse(await readFile4(catalogPath, "utf-8"));
14784
+ const allCatalogModels = catalogData.models ?? [];
14785
+ const project = configLoader.getProject();
14786
+ const catalogModels = filterCatalogEntries(allCatalogModels, project?.catalog?.include);
14787
+ tables = catalogModels.map((m) => {
14788
+ const fullName = m.table ?? m.name ?? "";
14789
+ const parts = fullName.split(".");
14790
+ const tableName = parts[parts.length - 1] ?? fullName;
14791
+ const schema = parts.length > 1 ? parts.slice(0, -1).join(".") : void 0;
14792
+ const columns = (m.columns ?? []).map((c) => ({
14793
+ name: c.name,
14794
+ type: c.data_type
14795
+ }));
14796
+ return {
14797
+ name: tableName,
14798
+ ...schema ? { schema } : {},
14799
+ connection: m.connection ?? (connections[0]?.name ?? "default"),
14800
+ columnCount: columns.length,
14801
+ columns
14802
+ };
14803
+ });
14804
+ } catch {
14805
+ }
14806
+ return reply.send({
14807
+ connections,
14808
+ tables,
14809
+ models: configModels,
14810
+ tableCount: tables.length,
14811
+ modelCount: configModels.length
14812
+ });
14813
+ });
14055
14814
  }
14056
14815
 
14057
14816
  // ../server/dist/routes/events.js
@@ -14093,18 +14852,18 @@ data: ${data}
14093
14852
  }
14094
14853
 
14095
14854
  // ../server/dist/server.js
14096
- import { join as join6, dirname as dirname2 } from "path";
14855
+ import { join as join8, dirname as dirname2 } from "path";
14097
14856
  import { fileURLToPath } from "url";
14098
- import { access as access2, readFile as readFile3 } from "fs/promises";
14857
+ import { access as access2, readFile as readFile5 } from "fs/promises";
14099
14858
  import { watch as fsWatch } from "fs";
14100
14859
  var __dirname = dirname2(fileURLToPath(import.meta.url));
14101
- var packageJsonPath = join6(__dirname, "..", "package.json");
14102
- var packageJson = JSON.parse(await readFile3(packageJsonPath, "utf-8"));
14860
+ var packageJsonPath = join8(__dirname, "..", "package.json");
14861
+ var packageJson = JSON.parse(await readFile5(packageJsonPath, "utf-8"));
14103
14862
  var VERSION = packageJson.version;
14104
14863
  async function enrichRefsFromCatalog(projectDir, refs) {
14105
14864
  try {
14106
- const catalogPath = join6(projectDir, ".yamchart", "catalog.json");
14107
- const catalogData = JSON.parse(await readFile3(catalogPath, "utf-8"));
14865
+ const catalogPath = join8(projectDir, ".yamchart", "catalog.json");
14866
+ const catalogData = JSON.parse(await readFile5(catalogPath, "utf-8"));
14108
14867
  for (const model of catalogData.models ?? []) {
14109
14868
  if (model.name && model.table) {
14110
14869
  refs[model.name] = model.table;
@@ -14114,7 +14873,7 @@ async function enrichRefsFromCatalog(projectDir, refs) {
14114
14873
  }
14115
14874
  }
14116
14875
  async function createServer(options) {
14117
- const { projectDir, port = 3001, host = "0.0.0.0", watch: watch2 = false, serveStatic = process.env.NODE_ENV === "production", staticDir = join6(__dirname, "public"), auth } = options;
14876
+ const { projectDir, port = 3001, host = "0.0.0.0", watch: watch2 = false, serveStatic = process.env.NODE_ENV === "production", staticDir = join8(__dirname, "public"), auth } = options;
14118
14877
  if (auth) {
14119
14878
  initAuthServer(auth.supabaseUrl, auth.supabaseServiceKey);
14120
14879
  }
@@ -14168,7 +14927,8 @@ async function createServer(options) {
14168
14927
  connector,
14169
14928
  cache,
14170
14929
  models,
14171
- refs
14930
+ refs,
14931
+ dialect: defaultConnection.type
14172
14932
  });
14173
14933
  const semanticService = new SemanticService({
14174
14934
  projectDir,
@@ -14248,6 +15008,7 @@ async function createServer(options) {
14248
15008
  await protectedRoutes.register(chartEventsRoutes, { configLoader, projectDir });
14249
15009
  await protectedRoutes.register(dashboardRoutes, { configLoader, gitService, projectDir, queryService });
14250
15010
  await protectedRoutes.register(chatRoutes, { configLoader, queryService, projectDir });
15011
+ await protectedRoutes.register(catalogRoutes, { configLoader, projectDir });
14251
15012
  });
14252
15013
  } else if (authDb) {
14253
15014
  const localMiddleware = createLocalAuthMiddleware(authDb);
@@ -14258,6 +15019,7 @@ async function createServer(options) {
14258
15019
  await protectedRoutes.register(chartEventsRoutes, { configLoader, projectDir });
14259
15020
  await protectedRoutes.register(dashboardRoutes, { configLoader, gitService, projectDir, queryService });
14260
15021
  await protectedRoutes.register(chatRoutes, { configLoader, queryService, projectDir });
15022
+ await protectedRoutes.register(catalogRoutes, { configLoader, projectDir });
14261
15023
  await protectedRoutes.register(sharesRoutes, { authDb });
14262
15024
  });
14263
15025
  } else {
@@ -14266,8 +15028,9 @@ async function createServer(options) {
14266
15028
  await fastify.register(chartEventsRoutes, { configLoader, projectDir });
14267
15029
  await fastify.register(dashboardRoutes, { configLoader, gitService, projectDir, queryService });
14268
15030
  await fastify.register(chatRoutes, { configLoader, queryService, projectDir });
15031
+ await fastify.register(catalogRoutes, { configLoader, projectDir });
14269
15032
  }
14270
- const assetsDir = join6(projectDir, "assets");
15033
+ const assetsDir = join8(projectDir, "assets");
14271
15034
  try {
14272
15035
  await access2(assetsDir);
14273
15036
  await fastify.register(fastifyStatic, {
@@ -14278,10 +15041,10 @@ async function createServer(options) {
14278
15041
  } catch {
14279
15042
  }
14280
15043
  if (project.theme?.customCss) {
14281
- const cssPath = join6(projectDir, project.theme.customCss);
15044
+ const cssPath = join8(projectDir, project.theme.customCss);
14282
15045
  fastify.get("/api/theme/custom.css", async (_request2, reply) => {
14283
15046
  try {
14284
- const content = await readFile3(cssPath, "utf-8");
15047
+ const content = await readFile5(cssPath, "utf-8");
14285
15048
  return reply.type("text/css").send(content);
14286
15049
  } catch {
14287
15050
  return reply.status(404).send("");
@@ -14337,7 +15100,7 @@ async function createServer(options) {
14337
15100
  fastify.log.info(`Semantic model rebuilt: ${semanticService.getModel()?.entities.length ?? 0} entities`);
14338
15101
  broadcastConfigChange();
14339
15102
  });
14340
- const catalogPath = join6(projectDir, ".yamchart", "catalog.json");
15103
+ const catalogPath = join8(projectDir, ".yamchart", "catalog.json");
14341
15104
  try {
14342
15105
  catalogWatcher = fsWatch(catalogPath, () => {
14343
15106
  semanticService.buildFromDisk();
@@ -14402,15 +15165,15 @@ async function runDevServer(projectDir, options) {
14402
15165
  let localAuth;
14403
15166
  try {
14404
15167
  const { readFileSync: readFileSync2 } = await import("fs");
14405
- const { parse } = await import("yaml");
14406
- const { resolveProjectConfig: resolveProjectConfig2, deepMerge: deepMerge3 } = await import("./dist-LJR7TAW4.js");
14407
- const raw = readFileSync2(resolve(projectDir, "yamchart.yaml"), "utf-8");
14408
- const rawConfig = parse(raw);
15168
+ const { parse: parse2 } = await import("yaml");
15169
+ const { resolveProjectConfig: resolveProjectConfig2, deepMerge: deepMerge3 } = await import("./dist-4GUE24QV.js");
15170
+ const raw = readFileSync2(resolve2(projectDir, "yamchart.yaml"), "utf-8");
15171
+ const rawConfig = parse2(raw);
14409
15172
  const resolvedEnv = options.env || process.env.YAMCHART_ENV || void 0;
14410
15173
  let projectConfig = resolveProjectConfig2(rawConfig, resolvedEnv);
14411
15174
  try {
14412
- const localRaw = readFileSync2(resolve(projectDir, "yamchart.local.yaml"), "utf-8");
14413
- const localOverrides = parse(localRaw);
15175
+ const localRaw = readFileSync2(resolve2(projectDir, "yamchart.local.yaml"), "utf-8");
15176
+ const localOverrides = parse2(localRaw);
14414
15177
  if (localOverrides && typeof localOverrides === "object") {
14415
15178
  projectConfig = deepMerge3(projectConfig, localOverrides);
14416
15179
  }
@@ -14419,16 +15182,16 @@ async function runDevServer(projectDir, options) {
14419
15182
  if (projectConfig?.auth?.enabled) {
14420
15183
  localAuth = {
14421
15184
  enabled: true,
14422
- dbPath: projectConfig.auth.db_path ? resolve(projectConfig.auth.db_path.replace(/^~/, homedir())) : resolve(homedir(), ".yamchart", "auth.db"),
15185
+ dbPath: projectConfig.auth.db_path ? resolve2(projectConfig.auth.db_path.replace(/^~/, homedir())) : resolve2(homedir(), ".yamchart", "auth.db"),
14423
15186
  sessionTtlMs: projectConfig.auth.session_ttl ? parseTtl(projectConfig.auth.session_ttl) : 30 * 24 * 60 * 60 * 1e3
14424
15187
  };
14425
15188
  }
14426
- const connectionDir = resolve(projectDir, "connections");
15189
+ const connectionDir = resolve2(projectDir, "connections");
14427
15190
  const targetConn = process.env.YAMCHART_CONNECTION || projectConfig?.defaults?.connection;
14428
15191
  if (targetConn) {
14429
15192
  try {
14430
- const connRaw = readFileSync2(resolve(connectionDir, `${targetConn}.yaml`), "utf-8");
14431
- const connConfig = parse(connRaw);
15193
+ const connRaw = readFileSync2(resolve2(connectionDir, `${targetConn}.yaml`), "utf-8");
15194
+ const connConfig = parse2(connRaw);
14432
15195
  if (connConfig?.auth?.type === "externalbrowser") {
14433
15196
  usesBrowserAuth = true;
14434
15197
  }
@@ -14443,21 +15206,33 @@ async function runDevServer(projectDir, options) {
14443
15206
  }
14444
15207
  const spinner2 = spinner("Starting server...");
14445
15208
  let server;
14446
- try {
14447
- server = await createServer({
14448
- projectDir,
14449
- port: options.port,
14450
- watch: true,
14451
- serveStatic: !options.apiOnly,
14452
- localAuth,
14453
- env: options.env
14454
- });
14455
- await server.start();
14456
- spinner2.stop();
14457
- } catch (err) {
14458
- spinner2.fail("Failed to start server");
14459
- error(err instanceof Error ? err.message : "Unknown error");
14460
- process.exit(1);
15209
+ let actualPort = options.port;
15210
+ const MAX_PORT_ATTEMPTS = 10;
15211
+ for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
15212
+ try {
15213
+ server = await createServer({
15214
+ projectDir,
15215
+ port: actualPort,
15216
+ watch: true,
15217
+ serveStatic: !options.apiOnly,
15218
+ localAuth,
15219
+ env: options.env
15220
+ });
15221
+ await server.start();
15222
+ spinner2.stop();
15223
+ if (actualPort !== options.port) {
15224
+ info(`Port ${options.port} is in use, starting on port ${actualPort}`);
15225
+ }
15226
+ break;
15227
+ } catch (err) {
15228
+ if (err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS - 1) {
15229
+ actualPort++;
15230
+ continue;
15231
+ }
15232
+ spinner2.fail("Failed to start server");
15233
+ error(err instanceof Error ? err.message : "Unknown error");
15234
+ process.exit(1);
15235
+ }
14461
15236
  }
14462
15237
  const project = server.configLoader.getProject();
14463
15238
  const charts = server.configLoader.getCharts();
@@ -14466,8 +15241,8 @@ async function runDevServer(projectDir, options) {
14466
15241
  box([
14467
15242
  `Yamchart v${options.version}`,
14468
15243
  ``,
14469
- `Dashboard: http://localhost:${options.port}`,
14470
- `API: http://localhost:${options.port}/api`,
15244
+ `Dashboard: http://localhost:${actualPort}`,
15245
+ `API: http://localhost:${actualPort}/api`,
14471
15246
  ``,
14472
15247
  `Project: ${project.name}`,
14473
15248
  `Charts: ${charts.length} loaded`,
@@ -14478,7 +15253,7 @@ async function runDevServer(projectDir, options) {
14478
15253
  ]);
14479
15254
  newline();
14480
15255
  if (options.open && !options.apiOnly) {
14481
- const url = `http://localhost:${options.port}`;
15256
+ const url = `http://localhost:${actualPort}`;
14482
15257
  const { exec } = await import("child_process");
14483
15258
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
14484
15259
  exec(`${command} ${url}`);
@@ -14495,4 +15270,4 @@ async function runDevServer(projectDir, options) {
14495
15270
  export {
14496
15271
  runDevServer
14497
15272
  };
14498
- //# sourceMappingURL=dev-Z2R2DBWO.js.map
15273
+ //# sourceMappingURL=dev-6QGAB4ZH.js.map