@runcontext/ui 0.5.2 → 0.5.4
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/index.cjs +706 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +706 -151
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -8
- package/static/setup.css +1069 -300
- package/static/setup.js +1371 -285
- package/static/uxd.css +672 -0
package/dist/index.cjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;// src/server.ts
|
|
2
2
|
var _hono = require('hono');
|
|
3
3
|
var _nodeserver = require('@hono/node-server');
|
|
4
4
|
var _cors = require('hono/cors');
|
|
5
|
-
var _fs = require('fs'); var
|
|
6
|
-
var _path = require('path'); var
|
|
5
|
+
var _fs = require('fs'); var fs6 = _interopRequireWildcard(_fs); var fs = _interopRequireWildcard(_fs); var fs2 = _interopRequireWildcard(_fs); var fs3 = _interopRequireWildcard(_fs); var fs4 = _interopRequireWildcard(_fs); var fs5 = _interopRequireWildcard(_fs);
|
|
6
|
+
var _path = require('path'); var path6 = _interopRequireWildcard(_path); var path = _interopRequireWildcard(_path); var path2 = _interopRequireWildcard(_path); var path3 = _interopRequireWildcard(_path); var path4 = _interopRequireWildcard(_path); var path5 = _interopRequireWildcard(_path);
|
|
7
7
|
var _url = require('url'); var url = _interopRequireWildcard(_url);
|
|
8
8
|
|
|
9
9
|
// src/routes/api/brief.ts
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
var _yaml = require('yaml');
|
|
13
|
+
var _yaml = require('yaml'); var yaml = _interopRequireWildcard(_yaml);
|
|
14
14
|
var _core = require('@runcontext/core');
|
|
15
15
|
function briefRoutes(contextDir) {
|
|
16
16
|
const app = new (0, _hono.Hono)();
|
|
@@ -22,17 +22,17 @@ function briefRoutes(contextDir) {
|
|
|
22
22
|
}
|
|
23
23
|
const brief = _core.ContextBriefSchema.parse(body);
|
|
24
24
|
brief.created_at = brief.created_at || (/* @__PURE__ */ new Date()).toISOString();
|
|
25
|
-
const
|
|
26
|
-
fs.mkdirSync(
|
|
27
|
-
fs.writeFileSync(
|
|
28
|
-
return c.json({ ok: true, path:
|
|
25
|
+
const briefPath = path.join(contextDir, `${brief.product_name}.context-brief.yaml`);
|
|
26
|
+
fs.mkdirSync(contextDir, { recursive: true });
|
|
27
|
+
fs.writeFileSync(briefPath, _yaml.stringify.call(void 0, brief), "utf-8");
|
|
28
|
+
return c.json({ ok: true, path: `${brief.product_name}.context-brief.yaml` });
|
|
29
29
|
});
|
|
30
30
|
app.get("/api/brief/:name", async (c) => {
|
|
31
31
|
const name = c.req.param("name");
|
|
32
32
|
if (!_core.PRODUCT_NAME_RE.test(name)) {
|
|
33
33
|
return c.json({ error: "Invalid product name" }, 400);
|
|
34
34
|
}
|
|
35
|
-
const briefPath = path.join(contextDir,
|
|
35
|
+
const briefPath = path.join(contextDir, `${name}.context-brief.yaml`);
|
|
36
36
|
if (!fs.existsSync(briefPath)) return c.json({ error: "Not found" }, 404);
|
|
37
37
|
return c.json(_yaml.parse.call(void 0, fs.readFileSync(briefPath, "utf-8")));
|
|
38
38
|
});
|
|
@@ -42,10 +42,183 @@ function briefRoutes(contextDir) {
|
|
|
42
42
|
// src/routes/api/sources.ts
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
var _os = require('os'); var os = _interopRequireWildcard(_os);
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
var NAME_PATTERNS = {
|
|
49
|
+
duckdb: "duckdb",
|
|
50
|
+
motherduck: "duckdb",
|
|
51
|
+
postgres: "postgres",
|
|
52
|
+
postgresql: "postgres",
|
|
53
|
+
neon: "postgres",
|
|
54
|
+
supabase: "postgres",
|
|
55
|
+
mysql: "mysql",
|
|
56
|
+
sqlite: "sqlite",
|
|
57
|
+
snowflake: "snowflake",
|
|
58
|
+
bigquery: "bigquery",
|
|
59
|
+
clickhouse: "clickhouse",
|
|
60
|
+
databricks: "databricks",
|
|
61
|
+
mssql: "mssql",
|
|
62
|
+
"sql-server": "mssql",
|
|
63
|
+
redshift: "postgres"
|
|
64
|
+
};
|
|
65
|
+
var PACKAGE_PATTERNS = {
|
|
66
|
+
"@motherduck/mcp": "duckdb",
|
|
67
|
+
"mcp-server-duckdb": "duckdb",
|
|
68
|
+
"mcp-server-postgres": "postgres",
|
|
69
|
+
"mcp-server-postgresql": "postgres",
|
|
70
|
+
"@neon/mcp": "postgres",
|
|
71
|
+
"@supabase/mcp": "postgres",
|
|
72
|
+
"mcp-server-mysql": "mysql",
|
|
73
|
+
"mcp-server-sqlite": "sqlite",
|
|
74
|
+
"mcp-server-snowflake": "snowflake",
|
|
75
|
+
"mcp-server-bigquery": "bigquery",
|
|
76
|
+
"mcp-server-clickhouse": "clickhouse",
|
|
77
|
+
"mcp-server-databricks": "databricks",
|
|
78
|
+
"mcp-server-mssql": "mssql",
|
|
79
|
+
"mcp-server-redshift": "postgres"
|
|
80
|
+
};
|
|
81
|
+
function readJsonSafe(filePath) {
|
|
82
|
+
try {
|
|
83
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
84
|
+
let raw = fs2.readFileSync(filePath, "utf-8");
|
|
85
|
+
raw = raw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(raw);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
const cleaned = raw.replace(/^\s*\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
|
|
90
|
+
return JSON.parse(cleaned);
|
|
91
|
+
}
|
|
92
|
+
} catch (e2) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function getConfigLocations(cwd) {
|
|
97
|
+
const home = os.homedir();
|
|
98
|
+
const locations = [];
|
|
99
|
+
locations.push({ ide: "claude-code", path: path2.join(cwd, ".mcp.json") });
|
|
100
|
+
locations.push({ ide: "claude-code", path: path2.join(home, ".claude.json") });
|
|
101
|
+
locations.push({ ide: "claude-code", path: path2.join(home, ".claude", "mcp_servers.json") });
|
|
102
|
+
if (process.platform === "darwin") {
|
|
103
|
+
locations.push({ ide: "claude-desktop", path: path2.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") });
|
|
104
|
+
} else if (process.platform === "win32") {
|
|
105
|
+
const appData = _nullishCoalesce(process.env.APPDATA, () => ( path2.join(home, "AppData", "Roaming")));
|
|
106
|
+
locations.push({ ide: "claude-desktop", path: path2.join(appData, "Claude", "claude_desktop_config.json") });
|
|
107
|
+
} else {
|
|
108
|
+
locations.push({ ide: "claude-desktop", path: path2.join(home, ".config", "claude", "claude_desktop_config.json") });
|
|
109
|
+
}
|
|
110
|
+
locations.push({ ide: "cursor", path: path2.join(cwd, ".cursor", "mcp.json") });
|
|
111
|
+
locations.push({ ide: "cursor", path: path2.join(home, ".cursor", "mcp.json") });
|
|
112
|
+
if (process.platform === "darwin") {
|
|
113
|
+
locations.push({ ide: "cursor", path: path2.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "cursor.mcp", "mcp.json") });
|
|
114
|
+
}
|
|
115
|
+
locations.push({ ide: "vscode", path: path2.join(cwd, ".vscode", "mcp.json") });
|
|
116
|
+
locations.push({ ide: "windsurf", path: path2.join(cwd, ".windsurf", "mcp.json") });
|
|
117
|
+
if (process.platform === "darwin") {
|
|
118
|
+
locations.push({ ide: "windsurf", path: path2.join(home, "Library", "Application Support", "Windsurf", "User", "globalStorage", "windsurf.mcp", "mcp.json") });
|
|
119
|
+
}
|
|
120
|
+
return locations;
|
|
121
|
+
}
|
|
122
|
+
function detectAdapterType(serverName, entry) {
|
|
123
|
+
const nameLower = serverName.toLowerCase();
|
|
124
|
+
for (const [pattern, adapter] of Object.entries(NAME_PATTERNS)) {
|
|
125
|
+
if (nameLower.includes(pattern)) return adapter;
|
|
126
|
+
}
|
|
127
|
+
const args = _nullishCoalesce(entry.args, () => ( []));
|
|
128
|
+
const allArgs = [_nullishCoalesce(entry.command, () => ( "")), ...args].join(" ").toLowerCase();
|
|
129
|
+
for (const [pkg, adapter] of Object.entries(PACKAGE_PATTERNS)) {
|
|
130
|
+
if (allArgs.includes(pkg.toLowerCase())) return adapter;
|
|
131
|
+
}
|
|
132
|
+
if (entry.url) {
|
|
133
|
+
const urlLower = entry.url.toLowerCase();
|
|
134
|
+
for (const [pattern, adapter] of Object.entries(NAME_PATTERNS)) {
|
|
135
|
+
if (urlLower.includes(pattern)) return adapter;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function discoverMcpDatabases(cwd) {
|
|
141
|
+
const results = [];
|
|
142
|
+
const seen = /* @__PURE__ */ new Set();
|
|
143
|
+
try {
|
|
144
|
+
const locations = getConfigLocations(cwd);
|
|
145
|
+
for (const loc of locations) {
|
|
146
|
+
const json = readJsonSafe(loc.path);
|
|
147
|
+
if (!json) continue;
|
|
148
|
+
const servers = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(json.mcpServers, () => ( json.mcp_servers)), () => ( json.servers)), () => ( json));
|
|
149
|
+
if (!servers || typeof servers !== "object") continue;
|
|
150
|
+
for (const [serverName, entry] of Object.entries(servers)) {
|
|
151
|
+
if (!entry || typeof entry !== "object") continue;
|
|
152
|
+
const adapterType = detectAdapterType(serverName, entry);
|
|
153
|
+
if (!adapterType) continue;
|
|
154
|
+
const key = `${adapterType}:${serverName}`;
|
|
155
|
+
if (seen.has(key)) continue;
|
|
156
|
+
seen.add(key);
|
|
157
|
+
const dbInfo = extractDbName(entry);
|
|
158
|
+
const label = dbInfo ? `${serverName} (${adapterType}${dbInfo ? " \u2014 " + dbInfo : ""})` : `${serverName} (${adapterType})`;
|
|
159
|
+
results.push({
|
|
160
|
+
name: label,
|
|
161
|
+
adapter: adapterType,
|
|
162
|
+
origin: `mcp:${loc.ide}/${serverName}`,
|
|
163
|
+
status: "detected"
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (e3) {
|
|
168
|
+
}
|
|
169
|
+
return results;
|
|
170
|
+
}
|
|
171
|
+
function extractDbName(entry) {
|
|
172
|
+
const args = _nullishCoalesce(entry.args, () => ( []));
|
|
173
|
+
for (let i = 0; i < args.length; i++) {
|
|
174
|
+
const arg = args[i];
|
|
175
|
+
if ((arg === "--database" || arg === "--db" || arg === "--dbname") && args[i + 1]) {
|
|
176
|
+
return args[i + 1];
|
|
177
|
+
}
|
|
178
|
+
if (arg && /^(postgres|mysql|mssql|clickhouse):\/\//.test(arg)) {
|
|
179
|
+
try {
|
|
180
|
+
const u = new URL(arg);
|
|
181
|
+
return u.pathname.replace(/^\//, "") || u.hostname;
|
|
182
|
+
} catch (e4) {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (entry.env) {
|
|
187
|
+
for (const [key, val] of Object.entries(entry.env)) {
|
|
188
|
+
if (/^(DATABASE_URL|POSTGRES_URL|PG_CONNECTION)/.test(key) && val) {
|
|
189
|
+
try {
|
|
190
|
+
const u = new URL(val);
|
|
191
|
+
return u.pathname.replace(/^\//, "") || u.hostname;
|
|
192
|
+
} catch (e5) {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return "";
|
|
198
|
+
}
|
|
45
199
|
function sourcesRoutes(rootDir, contextDir) {
|
|
46
200
|
const app = new (0, _hono.Hono)();
|
|
47
201
|
app.get("/api/sources", (c) => {
|
|
48
202
|
const sources = [];
|
|
203
|
+
try {
|
|
204
|
+
const configPath = path2.join(rootDir, "runcontext.config.yaml");
|
|
205
|
+
if (fs2.existsSync(configPath)) {
|
|
206
|
+
const raw = fs2.readFileSync(configPath, "utf-8");
|
|
207
|
+
const config = yaml.parse(raw);
|
|
208
|
+
if (_optionalChain([config, 'optionalAccess', _ => _.data_sources]) && typeof config.data_sources === "object") {
|
|
209
|
+
for (const [name, ds] of Object.entries(config.data_sources)) {
|
|
210
|
+
const src = ds;
|
|
211
|
+
sources.push({
|
|
212
|
+
name,
|
|
213
|
+
adapter: _nullishCoalesce(src.adapter, () => ( "auto")),
|
|
214
|
+
origin: `config:${name}`,
|
|
215
|
+
status: "detected"
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (e6) {
|
|
221
|
+
}
|
|
49
222
|
const envChecks = [
|
|
50
223
|
{ env: "DATABASE_URL", adapter: "auto", name: "Database (DATABASE_URL)" },
|
|
51
224
|
{ env: "POSTGRES_URL", adapter: "postgres", name: "PostgreSQL" },
|
|
@@ -78,7 +251,7 @@ function sourcesRoutes(rootDir, contextDir) {
|
|
|
78
251
|
status: "detected"
|
|
79
252
|
});
|
|
80
253
|
}
|
|
81
|
-
} catch (
|
|
254
|
+
} catch (e7) {
|
|
82
255
|
}
|
|
83
256
|
}
|
|
84
257
|
try {
|
|
@@ -91,10 +264,58 @@ function sourcesRoutes(rootDir, contextDir) {
|
|
|
91
264
|
status: "detected"
|
|
92
265
|
});
|
|
93
266
|
}
|
|
94
|
-
} catch (
|
|
267
|
+
} catch (e8) {
|
|
268
|
+
}
|
|
269
|
+
const mcpSources = discoverMcpDatabases(rootDir);
|
|
270
|
+
for (const mcp of mcpSources) {
|
|
271
|
+
const alreadyFound = sources.some(
|
|
272
|
+
(s) => s.origin === mcp.origin || s.adapter === mcp.adapter && s.name === mcp.name
|
|
273
|
+
);
|
|
274
|
+
if (!alreadyFound) {
|
|
275
|
+
sources.push(mcp);
|
|
276
|
+
}
|
|
95
277
|
}
|
|
96
278
|
return c.json(sources);
|
|
97
279
|
});
|
|
280
|
+
app.post("/api/sources", async (c) => {
|
|
281
|
+
const body = await c.req.json();
|
|
282
|
+
const { connection, name = "default" } = body;
|
|
283
|
+
if (!connection) {
|
|
284
|
+
return c.json({ error: "connection is required" }, 400);
|
|
285
|
+
}
|
|
286
|
+
let adapter = "auto";
|
|
287
|
+
if (connection.startsWith("postgres://") || connection.startsWith("postgresql://")) {
|
|
288
|
+
adapter = "postgres";
|
|
289
|
+
} else if (connection.startsWith("mysql://")) {
|
|
290
|
+
adapter = "mysql";
|
|
291
|
+
} else if (connection.startsWith("mssql://") || connection.startsWith("sqlserver://")) {
|
|
292
|
+
adapter = "mssql";
|
|
293
|
+
} else if (connection.startsWith("clickhouse://")) {
|
|
294
|
+
adapter = "clickhouse";
|
|
295
|
+
}
|
|
296
|
+
const configPath = path2.join(rootDir, "runcontext.config.yaml");
|
|
297
|
+
let config = {};
|
|
298
|
+
try {
|
|
299
|
+
if (fs2.existsSync(configPath)) {
|
|
300
|
+
const raw = fs2.readFileSync(configPath, "utf-8");
|
|
301
|
+
config = _nullishCoalesce(yaml.parse(raw), () => ( {}));
|
|
302
|
+
}
|
|
303
|
+
} catch (e9) {
|
|
304
|
+
}
|
|
305
|
+
if (!config.data_sources || typeof config.data_sources !== "object") {
|
|
306
|
+
config.data_sources = {};
|
|
307
|
+
}
|
|
308
|
+
const dataSources = config.data_sources;
|
|
309
|
+
dataSources[name] = { adapter, connection };
|
|
310
|
+
fs2.writeFileSync(configPath, yaml.stringify(config), "utf-8");
|
|
311
|
+
const created = {
|
|
312
|
+
name,
|
|
313
|
+
adapter,
|
|
314
|
+
origin: `config:${name}`,
|
|
315
|
+
status: "detected"
|
|
316
|
+
};
|
|
317
|
+
return c.json(created, 201);
|
|
318
|
+
});
|
|
98
319
|
return app;
|
|
99
320
|
}
|
|
100
321
|
|
|
@@ -120,15 +341,15 @@ function uploadRoutes(contextDir) {
|
|
|
120
341
|
if (file.size > MAX_FILE_SIZE) {
|
|
121
342
|
return c.json({ error: "File too large (max 10MB)" }, 400);
|
|
122
343
|
}
|
|
123
|
-
const ext =
|
|
344
|
+
const ext = path3.extname(file.name).toLowerCase();
|
|
124
345
|
if (!ALLOWED_EXTENSIONS.includes(ext)) {
|
|
125
346
|
return c.json({ error: `File type ${ext} not allowed` }, 400);
|
|
126
347
|
}
|
|
127
348
|
const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
128
|
-
const docsDir =
|
|
349
|
+
const docsDir = path3.join(contextDir, "products", productName, "docs");
|
|
129
350
|
fs3.mkdirSync(docsDir, { recursive: true });
|
|
130
351
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
131
|
-
fs3.writeFileSync(
|
|
352
|
+
fs3.writeFileSync(path3.join(docsDir, safeName), buffer);
|
|
132
353
|
return c.json({ ok: true, filename: safeName });
|
|
133
354
|
});
|
|
134
355
|
return app;
|
|
@@ -136,7 +357,53 @@ function uploadRoutes(contextDir) {
|
|
|
136
357
|
|
|
137
358
|
// src/routes/api/pipeline.ts
|
|
138
359
|
|
|
360
|
+
var _child_process = require('child_process');
|
|
139
361
|
var _crypto = require('crypto');
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
var _util = require('util');
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
// src/events.ts
|
|
369
|
+
var _events = require('events');
|
|
370
|
+
|
|
371
|
+
var SetupEventBus = (_class = class extends _events.EventEmitter {constructor(...args2) { super(...args2); _class.prototype.__init.call(this); }
|
|
372
|
+
__init() {this.sessions = /* @__PURE__ */ new Map()}
|
|
373
|
+
createSession() {
|
|
374
|
+
const id = _crypto.randomUUID.call(void 0, );
|
|
375
|
+
this.sessions.set(id, { createdAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
376
|
+
return id;
|
|
377
|
+
}
|
|
378
|
+
hasSession(id) {
|
|
379
|
+
return this.sessions.has(id);
|
|
380
|
+
}
|
|
381
|
+
removeSession(id) {
|
|
382
|
+
this.sessions.delete(id);
|
|
383
|
+
}
|
|
384
|
+
emitEvent(event) {
|
|
385
|
+
this.emit("event", event);
|
|
386
|
+
this.emit(event.type, event);
|
|
387
|
+
}
|
|
388
|
+
}, _class);
|
|
389
|
+
var setupBus = new SetupEventBus();
|
|
390
|
+
|
|
391
|
+
// src/routes/api/pipeline.ts
|
|
392
|
+
var execFile = _util.promisify.call(void 0, _child_process.execFile);
|
|
393
|
+
function resolveCliBin() {
|
|
394
|
+
try {
|
|
395
|
+
const thisDir = _path.dirname.call(void 0, _url.fileURLToPath.call(void 0, import.meta.url));
|
|
396
|
+
const localCli = _path.join.call(void 0, thisDir, "..", "..", "..", "cli", "dist", "index.js");
|
|
397
|
+
if (_fs.existsSync.call(void 0, localCli)) {
|
|
398
|
+
return { cmd: process.execPath, prefix: [localCli] };
|
|
399
|
+
}
|
|
400
|
+
} catch (e10) {
|
|
401
|
+
}
|
|
402
|
+
if (process.argv[1] && _fs.existsSync.call(void 0, process.argv[1])) {
|
|
403
|
+
return { cmd: process.execPath, prefix: [process.argv[1]] };
|
|
404
|
+
}
|
|
405
|
+
return { cmd: "npx", prefix: ["--yes", "@runcontext/cli"] };
|
|
406
|
+
}
|
|
140
407
|
var ALL_STAGES = [
|
|
141
408
|
"introspect",
|
|
142
409
|
"scaffold",
|
|
@@ -158,13 +425,20 @@ function pipelineRoutes(rootDir, contextDir) {
|
|
|
158
425
|
const app = new (0, _hono.Hono)();
|
|
159
426
|
app.post("/api/pipeline/start", async (c) => {
|
|
160
427
|
const body = await c.req.json();
|
|
161
|
-
const { productName, targetTier, dataSource } = body;
|
|
428
|
+
const { productName, targetTier, dataSource, sessionId } = body;
|
|
162
429
|
if (!productName || !targetTier) {
|
|
163
430
|
return c.json({ error: "productName and targetTier required" }, 400);
|
|
164
431
|
}
|
|
165
432
|
if (!["bronze", "silver", "gold"].includes(targetTier)) {
|
|
166
433
|
return c.json({ error: "targetTier must be bronze, silver, or gold" }, 400);
|
|
167
434
|
}
|
|
435
|
+
const safeNamePattern = /^[a-zA-Z0-9_-]+$/;
|
|
436
|
+
if (!safeNamePattern.test(productName)) {
|
|
437
|
+
return c.json({ error: "productName must contain only letters, numbers, hyphens, and underscores" }, 400);
|
|
438
|
+
}
|
|
439
|
+
if (dataSource && !safeNamePattern.test(dataSource)) {
|
|
440
|
+
return c.json({ error: "dataSource must contain only letters, numbers, hyphens, and underscores" }, 400);
|
|
441
|
+
}
|
|
168
442
|
const id = _crypto.randomUUID.call(void 0, );
|
|
169
443
|
const activeStages = stagesForTier(targetTier);
|
|
170
444
|
const skippedStages = ALL_STAGES.filter((s) => !activeStages.includes(s));
|
|
@@ -180,7 +454,7 @@ function pipelineRoutes(rootDir, contextDir) {
|
|
|
180
454
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
181
455
|
};
|
|
182
456
|
runs.set(id, run);
|
|
183
|
-
executePipeline(run, rootDir, contextDir, dataSource).catch((err) => {
|
|
457
|
+
executePipeline(run, rootDir, contextDir, dataSource, sessionId).catch((err) => {
|
|
184
458
|
run.status = "error";
|
|
185
459
|
const currentStage = run.stages.find((s) => s.status === "running");
|
|
186
460
|
if (currentStage) {
|
|
@@ -195,21 +469,128 @@ function pipelineRoutes(rootDir, contextDir) {
|
|
|
195
469
|
if (!run) return c.json({ error: "Not found" }, 404);
|
|
196
470
|
return c.json(run);
|
|
197
471
|
});
|
|
472
|
+
app.get("/api/mcp-config", (c) => {
|
|
473
|
+
let connection;
|
|
474
|
+
try {
|
|
475
|
+
const configPath = _path.join.call(void 0, rootDir, "runcontext.config.yaml");
|
|
476
|
+
const configRaw = _fs.readFileSync.call(void 0, configPath, "utf-8");
|
|
477
|
+
const config = _yaml.parse.call(void 0, configRaw);
|
|
478
|
+
const dataSources = _optionalChain([config, 'optionalAccess', _2 => _2.data_sources]);
|
|
479
|
+
if (dataSources) {
|
|
480
|
+
const firstKey = Object.keys(dataSources)[0];
|
|
481
|
+
if (firstKey) {
|
|
482
|
+
connection = dataSources[firstKey].connection || dataSources[firstKey].path;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
} catch (e11) {
|
|
486
|
+
}
|
|
487
|
+
const cli = resolveCliBin();
|
|
488
|
+
const mcpServers = {
|
|
489
|
+
runcontext: {
|
|
490
|
+
command: cli.cmd,
|
|
491
|
+
args: [...cli.prefix, "serve"],
|
|
492
|
+
cwd: rootDir
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
if (connection) {
|
|
496
|
+
mcpServers["runcontext-db"] = {
|
|
497
|
+
command: "npx",
|
|
498
|
+
args: ["--yes", "@runcontext/db", "--url", connection]
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
return c.json({ mcpServers });
|
|
502
|
+
});
|
|
198
503
|
return app;
|
|
199
504
|
}
|
|
200
|
-
|
|
505
|
+
function buildCliArgs(stage, dataSource) {
|
|
506
|
+
switch (stage) {
|
|
507
|
+
case "introspect": {
|
|
508
|
+
const args = ["introspect"];
|
|
509
|
+
if (dataSource) args.push("--source", dataSource);
|
|
510
|
+
return args;
|
|
511
|
+
}
|
|
512
|
+
case "scaffold":
|
|
513
|
+
return ["build"];
|
|
514
|
+
case "enrich-silver": {
|
|
515
|
+
const args = ["enrich", "--target", "silver", "--apply"];
|
|
516
|
+
if (dataSource) args.push("--source", dataSource);
|
|
517
|
+
return args;
|
|
518
|
+
}
|
|
519
|
+
case "enrich-gold": {
|
|
520
|
+
const args = ["enrich", "--target", "gold", "--apply"];
|
|
521
|
+
if (dataSource) args.push("--source", dataSource);
|
|
522
|
+
return args;
|
|
523
|
+
}
|
|
524
|
+
case "verify":
|
|
525
|
+
return ["verify"];
|
|
526
|
+
case "autofix":
|
|
527
|
+
return ["fix"];
|
|
528
|
+
case "agent-instructions":
|
|
529
|
+
return ["build"];
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function extractSummary(stdout) {
|
|
533
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
534
|
+
return lines.slice(-3).join("\n") || "completed";
|
|
535
|
+
}
|
|
536
|
+
async function executePipeline(run, rootDir, contextDir, dataSource, sessionId) {
|
|
201
537
|
for (const stage of run.stages) {
|
|
202
538
|
if (stage.status === "skipped") continue;
|
|
203
539
|
stage.status = "running";
|
|
204
540
|
stage.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
541
|
+
if (sessionId) {
|
|
542
|
+
setupBus.emitEvent({
|
|
543
|
+
type: "pipeline:stage",
|
|
544
|
+
sessionId,
|
|
545
|
+
payload: { stage: stage.stage, status: "running" }
|
|
546
|
+
});
|
|
547
|
+
}
|
|
205
548
|
try {
|
|
549
|
+
const cliArgs = buildCliArgs(stage.stage, dataSource);
|
|
550
|
+
const cli = resolveCliBin();
|
|
551
|
+
const { stdout } = await execFile(cli.cmd, [...cli.prefix, ...cliArgs], {
|
|
552
|
+
cwd: rootDir,
|
|
553
|
+
timeout: 12e4,
|
|
554
|
+
env: {
|
|
555
|
+
...process.env,
|
|
556
|
+
NODE_OPTIONS: "--max-old-space-size=4096 --no-deprecation"
|
|
557
|
+
}
|
|
558
|
+
});
|
|
206
559
|
stage.status = "done";
|
|
207
|
-
stage.summary =
|
|
560
|
+
stage.summary = extractSummary(stdout);
|
|
208
561
|
stage.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
562
|
+
if (sessionId) {
|
|
563
|
+
setupBus.emitEvent({
|
|
564
|
+
type: "pipeline:stage",
|
|
565
|
+
sessionId,
|
|
566
|
+
payload: { stage: stage.stage, status: "done", summary: stage.summary }
|
|
567
|
+
});
|
|
568
|
+
}
|
|
209
569
|
} catch (err) {
|
|
570
|
+
const execErr = err;
|
|
571
|
+
if (execErr.stdout && execErr.stdout.trim().length > 0) {
|
|
572
|
+
stage.status = "done";
|
|
573
|
+
stage.summary = extractSummary(execErr.stdout);
|
|
574
|
+
stage.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
575
|
+
if (sessionId) {
|
|
576
|
+
setupBus.emitEvent({
|
|
577
|
+
type: "pipeline:stage",
|
|
578
|
+
sessionId,
|
|
579
|
+
payload: { stage: stage.stage, status: "done", summary: stage.summary }
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
210
584
|
stage.status = "error";
|
|
211
585
|
stage.error = err instanceof Error ? err.message : String(err);
|
|
212
586
|
stage.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
587
|
+
if (sessionId) {
|
|
588
|
+
setupBus.emitEvent({
|
|
589
|
+
type: "pipeline:stage",
|
|
590
|
+
sessionId,
|
|
591
|
+
payload: { stage: stage.stage, status: "error", error: stage.error }
|
|
592
|
+
});
|
|
593
|
+
}
|
|
213
594
|
run.status = "error";
|
|
214
595
|
return;
|
|
215
596
|
}
|
|
@@ -225,17 +606,17 @@ async function executePipeline(run, _rootDir, _contextDir, _dataSource) {
|
|
|
225
606
|
function productsRoutes(contextDir) {
|
|
226
607
|
const app = new (0, _hono.Hono)();
|
|
227
608
|
app.get("/api/products", (c) => {
|
|
228
|
-
const productsDir =
|
|
609
|
+
const productsDir = path4.join(contextDir, "products");
|
|
229
610
|
if (!fs4.existsSync(productsDir)) {
|
|
230
611
|
return c.json([]);
|
|
231
612
|
}
|
|
232
613
|
const products = [];
|
|
233
614
|
const dirs = fs4.readdirSync(productsDir).filter((name) => {
|
|
234
|
-
const fullPath =
|
|
615
|
+
const fullPath = path4.join(productsDir, name);
|
|
235
616
|
return fs4.statSync(fullPath).isDirectory() && !name.startsWith(".");
|
|
236
617
|
});
|
|
237
618
|
for (const name of dirs) {
|
|
238
|
-
const briefPath =
|
|
619
|
+
const briefPath = path4.join(productsDir, name, "context-brief.yaml");
|
|
239
620
|
let description;
|
|
240
621
|
let sensitivity;
|
|
241
622
|
let hasBrief = false;
|
|
@@ -243,9 +624,9 @@ function productsRoutes(contextDir) {
|
|
|
243
624
|
hasBrief = true;
|
|
244
625
|
try {
|
|
245
626
|
const brief = _yaml.parse.call(void 0, fs4.readFileSync(briefPath, "utf-8"));
|
|
246
|
-
description = _optionalChain([brief, 'optionalAccess',
|
|
247
|
-
sensitivity = _optionalChain([brief, 'optionalAccess',
|
|
248
|
-
} catch (
|
|
627
|
+
description = _optionalChain([brief, 'optionalAccess', _3 => _3.description]);
|
|
628
|
+
sensitivity = _optionalChain([brief, 'optionalAccess', _4 => _4.sensitivity]);
|
|
629
|
+
} catch (e12) {
|
|
249
630
|
}
|
|
250
631
|
}
|
|
251
632
|
products.push({ name, description, sensitivity, hasBrief });
|
|
@@ -255,28 +636,244 @@ function productsRoutes(contextDir) {
|
|
|
255
636
|
return app;
|
|
256
637
|
}
|
|
257
638
|
|
|
639
|
+
// src/routes/api/auth.ts
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
function authRoutes(rootDir) {
|
|
649
|
+
const app = new (0, _hono.Hono)();
|
|
650
|
+
const registry = _core.createDefaultRegistry.call(void 0, );
|
|
651
|
+
const store = new (0, _core.CredentialStore)();
|
|
652
|
+
app.get("/api/auth/providers", async (c) => {
|
|
653
|
+
const providers = await Promise.all(
|
|
654
|
+
registry.getAll().map(async (p) => {
|
|
655
|
+
const cli = await p.detectCli();
|
|
656
|
+
return {
|
|
657
|
+
id: p.id,
|
|
658
|
+
displayName: p.displayName,
|
|
659
|
+
adapters: p.adapters,
|
|
660
|
+
cliInstalled: cli.installed,
|
|
661
|
+
cliAuthenticated: cli.authenticated
|
|
662
|
+
};
|
|
663
|
+
})
|
|
664
|
+
);
|
|
665
|
+
return c.json(providers);
|
|
666
|
+
});
|
|
667
|
+
app.post("/api/auth/start", async (c) => {
|
|
668
|
+
const { provider: providerId } = await c.req.json();
|
|
669
|
+
const provider = registry.get(providerId);
|
|
670
|
+
if (!provider) {
|
|
671
|
+
return c.json({ error: `Unknown provider: ${providerId}` }, 400);
|
|
672
|
+
}
|
|
673
|
+
const result = await provider.authenticate();
|
|
674
|
+
if (!result.ok) {
|
|
675
|
+
return c.json({ error: result.error }, 401);
|
|
676
|
+
}
|
|
677
|
+
const databases = await provider.listDatabases(result.token);
|
|
678
|
+
return c.json({
|
|
679
|
+
ok: true,
|
|
680
|
+
provider: providerId,
|
|
681
|
+
databases
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
app.post("/api/auth/select-db", async (c) => {
|
|
685
|
+
const { provider: providerId, database } = await c.req.json();
|
|
686
|
+
const provider = registry.get(providerId);
|
|
687
|
+
if (!provider) {
|
|
688
|
+
return c.json({ error: `Unknown provider: ${providerId}` }, 400);
|
|
689
|
+
}
|
|
690
|
+
const authResult = await provider.authenticate();
|
|
691
|
+
if (!authResult.ok) {
|
|
692
|
+
return c.json({ error: authResult.error }, 401);
|
|
693
|
+
}
|
|
694
|
+
database.metadata = { ...database.metadata, token: authResult.token };
|
|
695
|
+
const connStr = await provider.getConnectionString(database);
|
|
696
|
+
try {
|
|
697
|
+
const { createAdapter } = await Promise.resolve().then(() => _interopRequireWildcard(require("@runcontext/core")));
|
|
698
|
+
const adapter = await createAdapter({
|
|
699
|
+
adapter: database.adapter,
|
|
700
|
+
connection: connStr
|
|
701
|
+
});
|
|
702
|
+
await adapter.connect();
|
|
703
|
+
await adapter.query("SELECT 1");
|
|
704
|
+
await adapter.disconnect();
|
|
705
|
+
} catch (err) {
|
|
706
|
+
return c.json({ error: `Connection failed: ${err.message}` }, 400);
|
|
707
|
+
}
|
|
708
|
+
const credKey = `${providerId}:${database.id}`;
|
|
709
|
+
await store.save({
|
|
710
|
+
provider: providerId,
|
|
711
|
+
key: credKey,
|
|
712
|
+
token: authResult.token,
|
|
713
|
+
refreshToken: authResult.ok ? authResult.refreshToken : void 0,
|
|
714
|
+
expiresAt: authResult.ok ? authResult.expiresAt : void 0,
|
|
715
|
+
metadata: {
|
|
716
|
+
host: database.host,
|
|
717
|
+
database: database.name,
|
|
718
|
+
...database.metadata,
|
|
719
|
+
token: void 0
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
const configPath = path5.join(rootDir, "runcontext.config.yaml");
|
|
723
|
+
let config = {};
|
|
724
|
+
if (fs5.existsSync(configPath)) {
|
|
725
|
+
try {
|
|
726
|
+
config = _nullishCoalesce(_yaml.parse.call(void 0, fs5.readFileSync(configPath, "utf-8")), () => ( {}));
|
|
727
|
+
} catch (e13) {
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
config.data_sources = _nullishCoalesce(config.data_sources, () => ( {}));
|
|
731
|
+
config.data_sources.default = { adapter: database.adapter, auth: credKey };
|
|
732
|
+
fs5.writeFileSync(configPath, _yaml.stringify.call(void 0, config), "utf-8");
|
|
733
|
+
return c.json({ ok: true, auth: credKey });
|
|
734
|
+
});
|
|
735
|
+
app.get("/api/auth/credentials", async (c) => {
|
|
736
|
+
const keys = await store.list();
|
|
737
|
+
return c.json(keys);
|
|
738
|
+
});
|
|
739
|
+
return app;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/routes/api/suggest-brief.ts
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
var execFile2 = _util.promisify.call(void 0, _child_process.execFile);
|
|
747
|
+
function suggestBriefRoutes(rootDir) {
|
|
748
|
+
const app = new (0, _hono.Hono)();
|
|
749
|
+
app.post("/api/suggest-brief", async (c) => {
|
|
750
|
+
const body = await c.req.json();
|
|
751
|
+
const source = body.source || {};
|
|
752
|
+
const meta = source.metadata || {};
|
|
753
|
+
const projectName = meta.project || "";
|
|
754
|
+
const dbName = source.name || "";
|
|
755
|
+
const rawName = projectName || dbName || "my-data";
|
|
756
|
+
const productName = rawName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
757
|
+
const branch = meta.branch || "main";
|
|
758
|
+
const adapter = source.adapter || "database";
|
|
759
|
+
const region = meta.region || "";
|
|
760
|
+
const org = meta.org || "";
|
|
761
|
+
let description = `Semantic context for the ${rawName} ${adapter} database`;
|
|
762
|
+
if (branch !== "main") description += ` (${branch} branch)`;
|
|
763
|
+
if (org && org !== "Personal") description += `, managed by ${org}`;
|
|
764
|
+
description += ".";
|
|
765
|
+
let ownerName = "";
|
|
766
|
+
let ownerEmail = "";
|
|
767
|
+
let ownerTeam = "";
|
|
768
|
+
try {
|
|
769
|
+
const { stdout: name } = await execFile2("git", ["config", "user.name"], {
|
|
770
|
+
cwd: rootDir,
|
|
771
|
+
timeout: 3e3
|
|
772
|
+
});
|
|
773
|
+
ownerName = name.trim();
|
|
774
|
+
} catch (e14) {
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
const { stdout: email } = await execFile2("git", ["config", "user.email"], {
|
|
778
|
+
cwd: rootDir,
|
|
779
|
+
timeout: 3e3
|
|
780
|
+
});
|
|
781
|
+
ownerEmail = email.trim();
|
|
782
|
+
} catch (e15) {
|
|
783
|
+
}
|
|
784
|
+
if (org && org !== "Personal") {
|
|
785
|
+
ownerTeam = org;
|
|
786
|
+
} else if (ownerEmail) {
|
|
787
|
+
const domain = ownerEmail.split("@")[1];
|
|
788
|
+
if (domain && !["gmail.com", "yahoo.com", "hotmail.com", "outlook.com", "icloud.com"].includes(domain)) {
|
|
789
|
+
ownerTeam = domain.split(".")[0].charAt(0).toUpperCase() + domain.split(".")[0].slice(1);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return c.json({
|
|
793
|
+
product_name: productName,
|
|
794
|
+
description,
|
|
795
|
+
owner: {
|
|
796
|
+
name: ownerName,
|
|
797
|
+
email: ownerEmail,
|
|
798
|
+
team: ownerTeam
|
|
799
|
+
},
|
|
800
|
+
sensitivity: "internal"
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
return app;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/routes/ws.ts
|
|
807
|
+
var _ws = require('ws');
|
|
808
|
+
function attachWebSocket(server) {
|
|
809
|
+
const wss = new (0, _ws.WebSocketServer)({ server, path: "/ws" });
|
|
810
|
+
wss.on("connection", (ws, req) => {
|
|
811
|
+
const url2 = new URL(_nullishCoalesce(req.url, () => ( "/")), `http://${req.headers.host}`);
|
|
812
|
+
const sessionId = url2.searchParams.get("session");
|
|
813
|
+
if (!sessionId) {
|
|
814
|
+
ws.close(4001, "session query param required");
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (!setupBus.hasSession(sessionId)) {
|
|
818
|
+
setupBus.createSession();
|
|
819
|
+
}
|
|
820
|
+
const onEvent = (event) => {
|
|
821
|
+
if (event.sessionId !== sessionId) return;
|
|
822
|
+
if (ws.readyState === _ws.WebSocket.OPEN) {
|
|
823
|
+
ws.send(JSON.stringify(event));
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
setupBus.on("event", onEvent);
|
|
827
|
+
ws.on("message", (raw) => {
|
|
828
|
+
try {
|
|
829
|
+
const msg = JSON.parse(raw.toString());
|
|
830
|
+
msg.sessionId = sessionId;
|
|
831
|
+
setupBus.emitEvent(msg);
|
|
832
|
+
} catch (e16) {
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
ws.on("close", () => {
|
|
836
|
+
setupBus.off("event", onEvent);
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
258
841
|
// src/server.ts
|
|
259
|
-
var __dirname =
|
|
260
|
-
var staticDir =
|
|
842
|
+
var __dirname = path6.dirname(url.fileURLToPath(import.meta.url));
|
|
843
|
+
var staticDir = path6.resolve(__dirname, "..", "static");
|
|
261
844
|
function createApp(opts) {
|
|
262
845
|
const app = new (0, _hono.Hono)();
|
|
263
|
-
app.use("*", _cors.cors.call(void 0,
|
|
846
|
+
app.use("*", _cors.cors.call(void 0, {
|
|
847
|
+
origin: (origin) => {
|
|
848
|
+
if (!origin) return origin;
|
|
849
|
+
if (/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) {
|
|
850
|
+
return origin;
|
|
851
|
+
}
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
}));
|
|
264
855
|
app.route("", briefRoutes(opts.contextDir));
|
|
265
856
|
app.route("", sourcesRoutes(opts.rootDir, opts.contextDir));
|
|
266
857
|
app.route("", uploadRoutes(opts.contextDir));
|
|
267
858
|
app.route("", pipelineRoutes(opts.rootDir, opts.contextDir));
|
|
268
859
|
app.route("", productsRoutes(opts.contextDir));
|
|
860
|
+
app.route("", authRoutes(opts.rootDir));
|
|
861
|
+
app.route("", suggestBriefRoutes(opts.rootDir));
|
|
269
862
|
app.get("/api/health", (c) => c.json({ ok: true }));
|
|
863
|
+
app.post("/api/session", (c) => {
|
|
864
|
+
const id = setupBus.createSession();
|
|
865
|
+
return c.json({ sessionId: id });
|
|
866
|
+
});
|
|
270
867
|
app.get("/static/:filename", (c) => {
|
|
271
868
|
const filename = c.req.param("filename");
|
|
272
869
|
if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
|
|
273
870
|
return c.text("Not found", 404);
|
|
274
871
|
}
|
|
275
|
-
const filePath =
|
|
276
|
-
if (!
|
|
277
|
-
const ext =
|
|
872
|
+
const filePath = path6.join(staticDir, filename);
|
|
873
|
+
if (!fs6.existsSync(filePath)) return c.text("Not found", 404);
|
|
874
|
+
const ext = path6.extname(filename);
|
|
278
875
|
const contentType = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "application/octet-stream";
|
|
279
|
-
return c.body(
|
|
876
|
+
return c.body(fs6.readFileSync(filePath), 200, { "Content-Type": contentType });
|
|
280
877
|
});
|
|
281
878
|
app.get("/setup", (c) => {
|
|
282
879
|
return c.html(setupPageHTML());
|
|
@@ -290,138 +887,95 @@ function setupPageHTML() {
|
|
|
290
887
|
<head>
|
|
291
888
|
<meta charset="utf-8" />
|
|
292
889
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
293
|
-
<title>
|
|
890
|
+
<title>RunContext \u2014 Build Your Data Product</title>
|
|
891
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
892
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
893
|
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
894
|
+
<link rel="stylesheet" href="/static/uxd.css" />
|
|
294
895
|
<link rel="stylesheet" href="/static/setup.css" />
|
|
295
896
|
</head>
|
|
296
897
|
<body>
|
|
297
|
-
<div class="
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
</div>
|
|
310
|
-
|
|
311
|
-
<!-- Step 1: Product Name + Description -->
|
|
312
|
-
<div class="step active" id="step-1">
|
|
313
|
-
<h2>Name your data product</h2>
|
|
314
|
-
<div class="field">
|
|
315
|
-
<label for="product-name">Product Name</label>
|
|
316
|
-
<input type="text" id="product-name" class="input" placeholder="e.g. player-engagement" />
|
|
317
|
-
<p class="hint">Letters, numbers, dashes, underscores only</p>
|
|
898
|
+
<div class="app-shell">
|
|
899
|
+
<!-- Sidebar -->
|
|
900
|
+
<aside class="sidebar">
|
|
901
|
+
<div class="sidebar-brand">
|
|
902
|
+
<svg class="brand-chevron" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
903
|
+
<path d="M4 4l8 8-8 8" stroke="#c9a55a" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
904
|
+
<path d="M12 4l8 8-8 8" stroke="#c9a55a" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.5"/>
|
|
905
|
+
</svg>
|
|
906
|
+
<span class="brand-text">
|
|
907
|
+
<span class="brand-run">Run</span><span class="brand-context">Context</span>
|
|
908
|
+
</span>
|
|
909
|
+
<span class="brand-badge">Local</span>
|
|
318
910
|
</div>
|
|
319
|
-
<
|
|
320
|
-
<
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
911
|
+
<nav class="sidebar-nav">
|
|
912
|
+
<a class="nav-item active" data-nav="setup">
|
|
913
|
+
<span>Setup</span>
|
|
914
|
+
</a>
|
|
915
|
+
<a class="nav-item locked" data-nav="planes">
|
|
916
|
+
<span>Semantic Planes</span>
|
|
917
|
+
<svg class="lock-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
918
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
|
919
|
+
<path d="M7 11V7a5 5 0 0110 0v4"/>
|
|
920
|
+
</svg>
|
|
921
|
+
</a>
|
|
922
|
+
<a class="nav-item locked" data-nav="analytics">
|
|
923
|
+
<span>Analytics</span>
|
|
924
|
+
<svg class="lock-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
925
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
|
926
|
+
<path d="M7 11V7a5 5 0 0110 0v4"/>
|
|
927
|
+
</svg>
|
|
928
|
+
</a>
|
|
929
|
+
<a class="nav-item" data-nav="mcp">
|
|
930
|
+
<span class="status-dot" id="mcp-status-dot"></span>
|
|
931
|
+
<span>MCP Server</span>
|
|
932
|
+
<span class="nav-detail" id="mcp-status-text">checking...</span>
|
|
933
|
+
</a>
|
|
934
|
+
<a class="nav-item locked" data-nav="settings">
|
|
935
|
+
<span>Settings</span>
|
|
936
|
+
<svg class="lock-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
937
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
|
938
|
+
<path d="M7 11V7a5 5 0 0110 0v4"/>
|
|
939
|
+
</svg>
|
|
940
|
+
</a>
|
|
941
|
+
</nav>
|
|
942
|
+
<div class="sidebar-status">
|
|
943
|
+
<div class="status-row">
|
|
944
|
+
<span class="status-dot" id="db-status-dot"></span>
|
|
945
|
+
<span id="db-status-text">No database</span>
|
|
324
946
|
</div>
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
<button type="button" class="btn btn-primary" data-next>Next</button>
|
|
329
|
-
</div>
|
|
330
|
-
</div>
|
|
331
|
-
|
|
332
|
-
<!-- Step 2: Owner -->
|
|
333
|
-
<div class="step" id="step-2">
|
|
334
|
-
<h2>Who owns this data?</h2>
|
|
335
|
-
<div class="field">
|
|
336
|
-
<label for="owner-name">Your Name</label>
|
|
337
|
-
<input type="text" id="owner-name" class="input" placeholder="e.g. Tyler" />
|
|
338
|
-
</div>
|
|
339
|
-
<div class="field">
|
|
340
|
-
<label for="owner-team">Team</label>
|
|
341
|
-
<input type="text" id="owner-team" class="input" placeholder="e.g. Analytics" />
|
|
342
|
-
</div>
|
|
343
|
-
<div class="field">
|
|
344
|
-
<label for="owner-email">Email</label>
|
|
345
|
-
<input type="email" id="owner-email" class="input" placeholder="e.g. tyler@company.com" />
|
|
346
|
-
</div>
|
|
347
|
-
<div class="step-actions">
|
|
348
|
-
<button type="button" class="btn btn-secondary" data-prev>Back</button>
|
|
349
|
-
<button type="button" class="btn btn-primary" data-next>Next</button>
|
|
350
|
-
</div>
|
|
351
|
-
</div>
|
|
352
|
-
|
|
353
|
-
<!-- Step 3: Sensitivity + Sources + Upload -->
|
|
354
|
-
<div class="step" id="step-3">
|
|
355
|
-
<h2>Context & sensitivity</h2>
|
|
356
|
-
<div class="field">
|
|
357
|
-
<label>Data Sensitivity</label>
|
|
358
|
-
<div class="sensitivity-cards">
|
|
359
|
-
<div class="card" data-sensitivity="public">
|
|
360
|
-
<strong>Public</strong>
|
|
361
|
-
<p>Open data, no restrictions</p>
|
|
362
|
-
</div>
|
|
363
|
-
<div class="card selected" data-sensitivity="internal">
|
|
364
|
-
<strong>Internal</strong>
|
|
365
|
-
<p>Company use only</p>
|
|
366
|
-
</div>
|
|
367
|
-
<div class="card" data-sensitivity="confidential">
|
|
368
|
-
<strong>Confidential</strong>
|
|
369
|
-
<p>Need-to-know basis</p>
|
|
370
|
-
</div>
|
|
371
|
-
<div class="card" data-sensitivity="restricted">
|
|
372
|
-
<strong>Restricted</strong>
|
|
373
|
-
<p>Strict access controls</p>
|
|
374
|
-
</div>
|
|
947
|
+
<div class="status-row">
|
|
948
|
+
<span class="status-dot" id="mcp-server-dot"></span>
|
|
949
|
+
<span id="mcp-server-text">MCP stopped</span>
|
|
375
950
|
</div>
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
<div class="field">
|
|
379
|
-
<label>Data Sources</label>
|
|
380
|
-
<div id="sources-list" class="source-cards">
|
|
381
|
-
<p class="muted">Detecting data sources...</p>
|
|
951
|
+
<div class="status-row" id="tier-row">
|
|
952
|
+
<span class="tier-badge" id="tier-badge">Free</span>
|
|
382
953
|
</div>
|
|
383
954
|
</div>
|
|
955
|
+
</aside>
|
|
384
956
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
<p class="hint">Supports .md, .txt, .pdf, .csv, .json, .yaml, .sql</p>
|
|
390
|
-
<input type="file" id="file-input" hidden multiple accept=".md,.txt,.pdf,.csv,.json,.yaml,.yml,.sql,.html" />
|
|
391
|
-
</div>
|
|
392
|
-
<div id="uploaded-files"></div>
|
|
393
|
-
</div>
|
|
957
|
+
<!-- Header -->
|
|
958
|
+
<header class="app-header">
|
|
959
|
+
<div class="header-stepper" id="stepper"></div>
|
|
960
|
+
</header>
|
|
394
961
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
</div>
|
|
400
|
-
|
|
401
|
-
<!-- Step 4: Review -->
|
|
402
|
-
<div class="step" id="step-4">
|
|
403
|
-
<h2>Review your data product</h2>
|
|
404
|
-
<div id="review-content" class="review-content"></div>
|
|
405
|
-
<div class="step-actions">
|
|
406
|
-
<button type="button" class="btn btn-secondary" data-prev>Back</button>
|
|
407
|
-
<button type="button" class="btn btn-primary" data-next>Build it</button>
|
|
408
|
-
</div>
|
|
409
|
-
</div>
|
|
410
|
-
|
|
411
|
-
<!-- Step 5: Build Pipeline -->
|
|
412
|
-
<div class="step" id="step-5">
|
|
413
|
-
<h2>Building your semantic plane</h2>
|
|
414
|
-
<div id="pipeline-timeline" class="pipeline-timeline"></div>
|
|
415
|
-
<div id="pipeline-done" class="pipeline-done" style="display:none">
|
|
416
|
-
<p>Your semantic plane is live. AI agents can now query your data with context.</p>
|
|
417
|
-
<p class="muted">Powered by ContextKit \xB7 Open Semantic Interchange</p>
|
|
418
|
-
</div>
|
|
419
|
-
</div>
|
|
962
|
+
<!-- Main Content -->
|
|
963
|
+
<main class="main-content">
|
|
964
|
+
<div class="content-wrapper" id="wizard-content"></div>
|
|
965
|
+
</main>
|
|
420
966
|
|
|
421
|
-
|
|
422
|
-
|
|
967
|
+
<!-- Footer -->
|
|
968
|
+
<footer class="app-footer">
|
|
969
|
+
<span>Powered by RunContext · Open Semantic Interchange</span>
|
|
423
970
|
</footer>
|
|
424
971
|
</div>
|
|
972
|
+
|
|
973
|
+
<!-- Locked tooltip (hidden by default) -->
|
|
974
|
+
<div class="locked-tooltip" id="locked-tooltip" style="display:none">
|
|
975
|
+
<p>Available on RunContext Cloud</p>
|
|
976
|
+
<a href="https://runcontext.dev/pricing" target="_blank" rel="noopener">Learn more</a>
|
|
977
|
+
</div>
|
|
978
|
+
|
|
425
979
|
<script src="/static/setup.js"></script>
|
|
426
980
|
</body>
|
|
427
981
|
</html>`;
|
|
@@ -434,9 +988,10 @@ function startUIServer(opts) {
|
|
|
434
988
|
port: opts.port,
|
|
435
989
|
hostname: opts.host
|
|
436
990
|
}, (info) => {
|
|
437
|
-
console.log(`
|
|
991
|
+
console.log(`RunContext UI running at http://${opts.host === "0.0.0.0" ? "localhost" : opts.host}:${info.port}/setup`);
|
|
438
992
|
resolve2();
|
|
439
993
|
});
|
|
994
|
+
attachWebSocket(server);
|
|
440
995
|
server.on("error", reject);
|
|
441
996
|
});
|
|
442
997
|
}
|