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.
- package/dist/{advisor-5TJDNAY7.js → advisor-XTJQU7E4.js} +12 -12
- package/dist/{chunk-QUIDZO5G.js → chunk-2XJZJ6IA.js} +22 -6
- package/dist/chunk-2XJZJ6IA.js.map +1 -0
- package/dist/{chunk-UFDQ3C7Q.js → chunk-6ELSQNIH.js} +257 -257
- package/dist/chunk-6ELSQNIH.js.map +1 -0
- package/dist/{chunk-64CI3HSY.js → chunk-KDDO4UII.js} +70 -11
- package/dist/chunk-KDDO4UII.js.map +1 -0
- package/dist/{chunk-S2CH4HUZ.js → chunk-ODSPBOJW.js} +4 -4
- package/dist/chunk-OYBI433Z.js +1296 -0
- package/dist/chunk-OYBI433Z.js.map +1 -0
- package/dist/{chunk-ZBCQNWVN.js → chunk-PXFTWXSV.js} +5 -5
- package/dist/{chunk-5FHV22X2.js → chunk-XNMB25GT.js} +4 -4
- package/dist/{connection-utils-FEUWER5I.js → connection-utils-S7EFPTLY.js} +5 -5
- package/dist/{describe-MEP72B56.js → describe-TA2CTODC.js} +7 -7
- package/dist/{dev-H6SJZ5UA.js → dev-PHFYLJPJ.js} +2589 -1243
- package/dist/dev-PHFYLJPJ.js.map +1 -0
- package/dist/dev-supervisor-JXLVGEHP.js +43 -0
- package/dist/dev-supervisor-JXLVGEHP.js.map +1 -0
- package/dist/{dist-PPAD6KOM.js → dist-F6TVLP6C.js} +34 -4
- package/dist/{dist-XNCED7JW.js → dist-Q5ETETUU.js} +4 -4
- package/dist/{dist-5DQO6L2S.js → dist-YJOOH65Y.js} +9 -5
- package/dist/index.js +65 -42
- package/dist/index.js.map +1 -1
- package/dist/{init-SECXKD5G.js → init-TJJVKMEN.js} +24 -5
- package/dist/init-TJJVKMEN.js.map +1 -0
- package/dist/public/assets/DataView-V-E6oxy6.js +9 -0
- package/dist/public/assets/{EventManagement-BnmeJDQl.js → EventManagement-Do2xU5lV.js} +2 -2
- package/dist/public/assets/ExplorePage-Dg50mvGB.js +1 -0
- package/dist/public/assets/{LoginPage-CzaFkkjg.js → LoginPage-DmYGIXcu.js} +1 -1
- package/dist/public/assets/{PublicViewer-irjxqH6a.js → PublicViewer-CJTgc9OF.js} +1 -1
- package/dist/public/assets/{SetupWizard-ConWIcmy.js → SetupWizard-Bo-zmh43.js} +1 -1
- package/dist/public/assets/{ShareManagement-CP4wdwLR.js → ShareManagement-6Wp6b2Gp.js} +1 -1
- package/dist/public/assets/{SourceDetailView-DZS5518E.js → SourceDetailView-CTzSXhZc.js} +1 -1
- package/dist/public/assets/UserManagement-DQhB13T0.js +1 -0
- package/dist/public/assets/{index-jlfTO7f5.js → index-CY8xsfQH.js} +41 -41
- package/dist/public/assets/{index-C1X8RW4Z.css → index-Dkn-tJCT.css} +1 -1
- package/dist/public/assets/{index.es-CgnvEWi5.js → index.es-Bc3yys5L.js} +1 -1
- package/dist/public/assets/{jspdf.es.min-Cw5WefMt.js → jspdf.es.min-D94JEH-J.js} +3 -3
- package/dist/public/index.html +2 -2
- package/dist/{query-2H3YOPI2.js → query-AX4BOPMH.js} +5 -5
- package/dist/sample-4NUBJGIL.js +76 -0
- package/dist/sample-4NUBJGIL.js.map +1 -0
- package/dist/{search-IPE4ISFB.js → search-IBVVYN4I.js} +7 -7
- package/dist/{semantic-K3MYXXJI.js → semantic-33YIUMUV.js} +39 -4
- package/dist/semantic-33YIUMUV.js.map +1 -0
- package/dist/semantic-5WGVZ7HY.js +30 -0
- package/dist/{source-resolver-HZQLOODU.js → source-resolver-GYXIRIVK.js} +7 -7
- package/dist/source-resolver-GYXIRIVK.js.map +1 -0
- package/dist/{sync-warehouse-TUNULDUY.js → sync-warehouse-5NKENCOZ.js} +15 -11
- package/dist/sync-warehouse-5NKENCOZ.js.map +1 -0
- package/dist/{tables-K5NAN2WK.js → tables-QMZMYYV5.js} +7 -7
- package/dist/templates/default/docs/yamchart-reference.md +26 -0
- package/dist/{test-SRHVOXZB.js → test-6MCPGXJP.js} +19 -7
- package/dist/test-6MCPGXJP.js.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-64CI3HSY.js.map +0 -1
- package/dist/chunk-QUIDZO5G.js.map +0 -1
- package/dist/chunk-UFDQ3C7Q.js.map +0 -1
- package/dist/chunk-ZIY22VO7.js +0 -1190
- package/dist/chunk-ZIY22VO7.js.map +0 -1
- package/dist/dev-H6SJZ5UA.js.map +0 -1
- package/dist/init-SECXKD5G.js.map +0 -1
- package/dist/public/assets/DataView-DUCz_96y.js +0 -9
- package/dist/public/assets/ExplorePage-kk4z9ldZ.js +0 -1
- package/dist/public/assets/UserManagement-AubGd9hl.js +0 -1
- package/dist/sample-ODUGGSFA.js +0 -42
- package/dist/sample-ODUGGSFA.js.map +0 -1
- package/dist/semantic-K3MYXXJI.js.map +0 -1
- package/dist/sync-warehouse-TUNULDUY.js.map +0 -1
- package/dist/test-SRHVOXZB.js.map +0 -1
- /package/dist/{advisor-5TJDNAY7.js.map → advisor-XTJQU7E4.js.map} +0 -0
- /package/dist/{chunk-S2CH4HUZ.js.map → chunk-ODSPBOJW.js.map} +0 -0
- /package/dist/{chunk-ZBCQNWVN.js.map → chunk-PXFTWXSV.js.map} +0 -0
- /package/dist/{chunk-5FHV22X2.js.map → chunk-XNMB25GT.js.map} +0 -0
- /package/dist/{connection-utils-FEUWER5I.js.map → connection-utils-S7EFPTLY.js.map} +0 -0
- /package/dist/{describe-MEP72B56.js.map → describe-TA2CTODC.js.map} +0 -0
- /package/dist/{dist-5DQO6L2S.js.map → dist-F6TVLP6C.js.map} +0 -0
- /package/dist/{dist-PPAD6KOM.js.map → dist-Q5ETETUU.js.map} +0 -0
- /package/dist/{dist-XNCED7JW.js.map → dist-YJOOH65Y.js.map} +0 -0
- /package/dist/{query-2H3YOPI2.js.map → query-AX4BOPMH.js.map} +0 -0
- /package/dist/{search-IPE4ISFB.js.map → search-IBVVYN4I.js.map} +0 -0
- /package/dist/{source-resolver-HZQLOODU.js.map → semantic-5WGVZ7HY.js.map} +0 -0
- /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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
6857
|
-
import {
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
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
|
-
|
|
7020
|
-
|
|
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
|
-
|
|
7023
|
-
|
|
6891
|
+
async set(key, value, ttlMs) {
|
|
6892
|
+
this.cache.set(key, value, { ttl: ttlMs ?? this.defaultTtlMs });
|
|
7024
6893
|
}
|
|
7025
|
-
|
|
7026
|
-
this.
|
|
6894
|
+
async invalidate(key) {
|
|
6895
|
+
this.cache.delete(key);
|
|
7027
6896
|
}
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
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
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
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
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
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
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
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
|
-
//
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
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
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
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
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
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
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
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
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
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
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
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
|
-
|
|
7018
|
+
default:
|
|
7019
|
+
throw new Error(`Unsupported connection type: ${connection.type}`);
|
|
7211
7020
|
}
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
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
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7257
|
-
|
|
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
|
-
|
|
7278
|
-
|
|
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
|
-
|
|
7287
|
-
|
|
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
|
-
|
|
7322
|
-
const schedulesDir = join(this.projectDir, "schedules");
|
|
7506
|
+
loadFromYaml(yamlContent, filePath, onWarning) {
|
|
7323
7507
|
try {
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
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
|
-
//
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
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
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7563
|
+
selectAgent(message, context, explicitAgent) {
|
|
7564
|
+
if (explicitAgent) {
|
|
7565
|
+
const agent = this.registry.getAgent(explicitAgent);
|
|
7566
|
+
if (agent)
|
|
7567
|
+
return agent;
|
|
7531
7568
|
}
|
|
7532
|
-
|
|
7533
|
-
|
|
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
|
-
|
|
7536
|
-
|
|
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
|
|
7539
|
-
this.
|
|
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
|
|
7542
|
-
const
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
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
|
|
7551
|
-
this.
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
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
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
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
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
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
|
|
7609
|
-
const
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
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
|
-
|
|
7656
|
-
|
|
7657
|
-
const
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
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
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
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
|
-
|
|
7698
|
-
|
|
7699
|
-
const
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
}
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
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
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
const
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
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
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
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
|
-
|
|
7753
|
-
this.
|
|
7922
|
+
getProject() {
|
|
7923
|
+
if (!this.project) {
|
|
7924
|
+
throw new Error("Config not loaded");
|
|
7925
|
+
}
|
|
7926
|
+
return this.project;
|
|
7754
7927
|
}
|
|
7755
|
-
|
|
7756
|
-
this.
|
|
7928
|
+
getConnections() {
|
|
7929
|
+
return Array.from(this.connections.values());
|
|
7757
7930
|
}
|
|
7758
|
-
|
|
7759
|
-
|
|
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
|
-
|
|
7771
|
-
|
|
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
|
-
|
|
7774
|
-
return this.
|
|
7945
|
+
getConnectionSecrets(name) {
|
|
7946
|
+
return this.connectionSecrets.get(name);
|
|
7775
7947
|
}
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
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
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
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
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
}
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
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
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
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
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
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
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
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
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
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, {
|
|
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, {
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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) ??
|
|
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:
|
|
8835
|
-
const fullPath =
|
|
8836
|
-
|
|
8837
|
-
|
|
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
|
|
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 =
|
|
9070
|
+
const catalogPath = join7(this.projectDir, ".yamchart", "catalog.json");
|
|
8871
9071
|
let catalog;
|
|
8872
|
-
if (
|
|
8873
|
-
const raw =
|
|
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((
|
|
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(
|
|
9494
|
-
|
|
9771
|
+
return value instanceof P ? value : new P(function(resolve4) {
|
|
9772
|
+
resolve4(value);
|
|
9495
9773
|
});
|
|
9496
9774
|
}
|
|
9497
|
-
return new (P || (P = Promise))(function(
|
|
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 ?
|
|
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((
|
|
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
|
-
|
|
12326
|
+
resolve4("ok");
|
|
12049
12327
|
}
|
|
12050
|
-
push.receive("ok", () =>
|
|
12051
|
-
push.receive("error", () =>
|
|
12052
|
-
push.receive("timeout", () =>
|
|
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((
|
|
12358
|
+
return new Promise((resolve4) => {
|
|
12081
12359
|
leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout);
|
|
12082
12360
|
leavePush.receive("ok", () => {
|
|
12083
12361
|
onClose();
|
|
12084
|
-
|
|
12362
|
+
resolve4("ok");
|
|
12085
12363
|
}).receive("timeout", () => {
|
|
12086
12364
|
onClose();
|
|
12087
|
-
|
|
12365
|
+
resolve4("timed out");
|
|
12088
12366
|
}).receive("error", () => {
|
|
12089
|
-
|
|
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:
|
|
12169
|
-
const events = [close, error2, leave,
|
|
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((
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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((
|
|
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((
|
|
24704
|
+
return new Promise((resolve4) => {
|
|
24429
24705
|
this._streamMapping.set(streamId, {
|
|
24430
|
-
resolveJson:
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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(
|
|
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((
|
|
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
|
|
28838
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
28877
|
-
|
|
28878
|
-
|
|
28879
|
-
|
|
28880
|
-
|
|
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
|
|
28885
|
-
|
|
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
|
|
29258
|
+
const match = tables.find((t) => t.name === tableName && (schema === void 0 || schemaMatches(t.schema, schema)));
|
|
28890
29259
|
if (!match) {
|
|
28891
|
-
|
|
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
|
-
|
|
28902
|
-
|
|
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 =
|
|
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
|
|
28910
|
-
const from = providedSchema === void 0 && match?.schema ? `${match.schema}.${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
|
|
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 =
|
|
29627
|
+
const chartsDir = join10(deps.projectDir, "charts");
|
|
29062
29628
|
await mkdir(chartsDir, { recursive: true });
|
|
29063
|
-
await writeFile3(
|
|
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
|
|
29371
|
-
import { join as
|
|
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 =
|
|
29376
|
-
if (!
|
|
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) =>
|
|
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
|
-
|
|
29501
|
-
|
|
29502
|
-
|
|
29503
|
-
|
|
29504
|
-
|
|
29505
|
-
|
|
29506
|
-
|
|
29507
|
-
|
|
29508
|
-
|
|
29509
|
-
|
|
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
|
-
|
|
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 {
|
|
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: "
|
|
29612
|
-
inputSchema: { name: z2.string(), params: z2.record(z2.unknown()).optional() }
|
|
29613
|
-
|
|
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
|
-
|
|
29618
|
-
|
|
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
|
-
|
|
29623
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29690
|
-
|
|
29691
|
-
|
|
29692
|
-
|
|
29693
|
-
|
|
29694
|
-
|
|
29695
|
-
|
|
29696
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
29830
|
-
|
|
29831
|
-
|
|
29832
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
31898
|
+
import { join as join14, dirname as dirname4 } from "path";
|
|
30635
31899
|
import { fileURLToPath } from "url";
|
|
30636
|
-
import { access as access2, readFile as
|
|
31900
|
+
import { access as access2, readFile as readFile7 } from "fs/promises";
|
|
30637
31901
|
import { watch as fsWatch } from "fs";
|
|
30638
|
-
var __dirname =
|
|
30639
|
-
var packageJsonPath =
|
|
30640
|
-
var packageJson = JSON.parse(await
|
|
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 =
|
|
30645
|
-
const catalogData = JSON.parse(await
|
|
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 =
|
|
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, {
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
32182
|
+
const catalogPath = join14(projectDir, ".yamchart", "catalog.json");
|
|
30910
32183
|
try {
|
|
30911
|
-
const catalogData = JSON.parse(await
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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:
|
|
32434
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
31089
32435
|
const { parse: parse2 } = await import("yaml");
|
|
31090
|
-
const { resolveProjectConfig: resolveProjectConfig2, deepMerge: deepMerge3 } = await import("./dist-
|
|
31091
|
-
const raw =
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
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-
|
|
32540
|
+
//# sourceMappingURL=dev-PHFYLJPJ.js.map
|