@runcontext/ui 0.5.2 → 0.5.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/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.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// src/server.ts
|
|
2
|
-
import { Hono as
|
|
2
|
+
import { Hono as Hono8 } from "hono";
|
|
3
3
|
import { serve } from "@hono/node-server";
|
|
4
4
|
import { cors } from "hono/cors";
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
5
|
+
import * as fs6 from "fs";
|
|
6
|
+
import * as path6 from "path";
|
|
7
7
|
import * as url from "url";
|
|
8
8
|
|
|
9
9
|
// src/routes/api/brief.ts
|
|
@@ -22,17 +22,17 @@ function briefRoutes(contextDir) {
|
|
|
22
22
|
}
|
|
23
23
|
const brief = 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, stringify(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 (!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(parse(fs.readFileSync(briefPath, "utf-8")));
|
|
38
38
|
});
|
|
@@ -42,10 +42,183 @@ function briefRoutes(contextDir) {
|
|
|
42
42
|
// src/routes/api/sources.ts
|
|
43
43
|
import { Hono as Hono2 } from "hono";
|
|
44
44
|
import * as fs2 from "fs";
|
|
45
|
+
import * as os from "os";
|
|
46
|
+
import * as path2 from "path";
|
|
47
|
+
import * as yaml from "yaml";
|
|
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 {
|
|
89
|
+
const cleaned = raw.replace(/^\s*\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
|
|
90
|
+
return JSON.parse(cleaned);
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
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 = 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 = entry.args ?? [];
|
|
128
|
+
const allArgs = [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 = 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 {
|
|
168
|
+
}
|
|
169
|
+
return results;
|
|
170
|
+
}
|
|
171
|
+
function extractDbName(entry) {
|
|
172
|
+
const args = 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 {
|
|
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 {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return "";
|
|
198
|
+
}
|
|
45
199
|
function sourcesRoutes(rootDir, contextDir) {
|
|
46
200
|
const app = new Hono2();
|
|
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 (config?.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: src.adapter ?? "auto",
|
|
214
|
+
origin: `config:${name}`,
|
|
215
|
+
status: "detected"
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
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" },
|
|
@@ -93,15 +266,63 @@ function sourcesRoutes(rootDir, contextDir) {
|
|
|
93
266
|
}
|
|
94
267
|
} catch {
|
|
95
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
|
+
}
|
|
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 = yaml.parse(raw) ?? {};
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
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
|
|
|
101
322
|
// src/routes/api/upload.ts
|
|
102
323
|
import { Hono as Hono3 } from "hono";
|
|
103
324
|
import * as fs3 from "fs";
|
|
104
|
-
import * as
|
|
325
|
+
import * as path3 from "path";
|
|
105
326
|
import { PRODUCT_NAME_RE as PRODUCT_NAME_RE2 } from "@runcontext/core";
|
|
106
327
|
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
107
328
|
var ALLOWED_EXTENSIONS = [".md", ".txt", ".pdf", ".csv", ".json", ".yaml", ".yml", ".sql", ".html"];
|
|
@@ -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
|
import { Hono as Hono4 } from "hono";
|
|
360
|
+
import { execFile as execFileCb } from "child_process";
|
|
361
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
362
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
363
|
+
import { join as join4, dirname } from "path";
|
|
364
|
+
import { promisify } from "util";
|
|
365
|
+
import { fileURLToPath } from "url";
|
|
366
|
+
import { parse as parseYaml } from "yaml";
|
|
367
|
+
|
|
368
|
+
// src/events.ts
|
|
369
|
+
import { EventEmitter } from "events";
|
|
139
370
|
import { randomUUID } from "crypto";
|
|
371
|
+
var SetupEventBus = class extends EventEmitter {
|
|
372
|
+
sessions = /* @__PURE__ */ new Map();
|
|
373
|
+
createSession() {
|
|
374
|
+
const id = randomUUID();
|
|
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
|
+
};
|
|
389
|
+
var setupBus = new SetupEventBus();
|
|
390
|
+
|
|
391
|
+
// src/routes/api/pipeline.ts
|
|
392
|
+
var execFile = promisify(execFileCb);
|
|
393
|
+
function resolveCliBin() {
|
|
394
|
+
try {
|
|
395
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
396
|
+
const localCli = join4(thisDir, "..", "..", "..", "cli", "dist", "index.js");
|
|
397
|
+
if (existsSync3(localCli)) {
|
|
398
|
+
return { cmd: process.execPath, prefix: [localCli] };
|
|
399
|
+
}
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
if (process.argv[1] && existsSync3(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,14 +425,21 @@ function pipelineRoutes(rootDir, contextDir) {
|
|
|
158
425
|
const app = new Hono4();
|
|
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
|
}
|
|
168
|
-
const
|
|
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
|
+
}
|
|
442
|
+
const id = randomUUID2();
|
|
169
443
|
const activeStages = stagesForTier(targetTier);
|
|
170
444
|
const skippedStages = ALL_STAGES.filter((s) => !activeStages.includes(s));
|
|
171
445
|
const run = {
|
|
@@ -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 = join4(rootDir, "runcontext.config.yaml");
|
|
476
|
+
const configRaw = readFileSync3(configPath, "utf-8");
|
|
477
|
+
const config = parseYaml(configRaw);
|
|
478
|
+
const dataSources = config?.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 {
|
|
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
|
}
|
|
@@ -220,29 +601,29 @@ async function executePipeline(run, _rootDir, _contextDir, _dataSource) {
|
|
|
220
601
|
// src/routes/api/products.ts
|
|
221
602
|
import { Hono as Hono5 } from "hono";
|
|
222
603
|
import * as fs4 from "fs";
|
|
223
|
-
import * as
|
|
224
|
-
import { parse as
|
|
604
|
+
import * as path4 from "path";
|
|
605
|
+
import { parse as parse3 } from "yaml";
|
|
225
606
|
function productsRoutes(contextDir) {
|
|
226
607
|
const app = new Hono5();
|
|
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;
|
|
242
623
|
if (fs4.existsSync(briefPath)) {
|
|
243
624
|
hasBrief = true;
|
|
244
625
|
try {
|
|
245
|
-
const brief =
|
|
626
|
+
const brief = parse3(fs4.readFileSync(briefPath, "utf-8"));
|
|
246
627
|
description = brief?.description;
|
|
247
628
|
sensitivity = brief?.sensitivity;
|
|
248
629
|
} catch {
|
|
@@ -255,28 +636,244 @@ function productsRoutes(contextDir) {
|
|
|
255
636
|
return app;
|
|
256
637
|
}
|
|
257
638
|
|
|
639
|
+
// src/routes/api/auth.ts
|
|
640
|
+
import { Hono as Hono6 } from "hono";
|
|
641
|
+
import {
|
|
642
|
+
createDefaultRegistry,
|
|
643
|
+
CredentialStore
|
|
644
|
+
} from "@runcontext/core";
|
|
645
|
+
import * as fs5 from "fs";
|
|
646
|
+
import * as path5 from "path";
|
|
647
|
+
import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
648
|
+
function authRoutes(rootDir) {
|
|
649
|
+
const app = new Hono6();
|
|
650
|
+
const registry = createDefaultRegistry();
|
|
651
|
+
const store = new 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 import("@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 = parseYaml2(fs5.readFileSync(configPath, "utf-8")) ?? {};
|
|
727
|
+
} catch {
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
config.data_sources = config.data_sources ?? {};
|
|
731
|
+
config.data_sources.default = { adapter: database.adapter, auth: credKey };
|
|
732
|
+
fs5.writeFileSync(configPath, stringifyYaml(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
|
+
import { Hono as Hono7 } from "hono";
|
|
744
|
+
import { execFile as execFileCb2 } from "child_process";
|
|
745
|
+
import { promisify as promisify2 } from "util";
|
|
746
|
+
var execFile2 = promisify2(execFileCb2);
|
|
747
|
+
function suggestBriefRoutes(rootDir) {
|
|
748
|
+
const app = new Hono7();
|
|
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 {
|
|
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 {
|
|
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
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
808
|
+
function attachWebSocket(server) {
|
|
809
|
+
const wss = new WebSocketServer({ server, path: "/ws" });
|
|
810
|
+
wss.on("connection", (ws, req) => {
|
|
811
|
+
const url2 = new URL(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 === 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 {
|
|
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
|
-
const app = new
|
|
263
|
-
app.use("*", cors(
|
|
845
|
+
const app = new Hono8();
|
|
846
|
+
app.use("*", cors({
|
|
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>
|
|
375
|
-
</div>
|
|
376
|
-
</div>
|
|
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>
|
|
947
|
+
<div class="status-row">
|
|
948
|
+
<span class="status-dot" id="mcp-server-dot"></span>
|
|
949
|
+
<span id="mcp-server-text">MCP stopped</span>
|
|
382
950
|
</div>
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
<div class="field">
|
|
386
|
-
<label>Documentation (optional)</label>
|
|
387
|
-
<div id="upload-area" class="upload-area">
|
|
388
|
-
<p>Drop files here or click to upload</p>
|
|
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" />
|
|
951
|
+
<div class="status-row" id="tier-row">
|
|
952
|
+
<span class="tier-badge" id="tier-badge">Free</span>
|
|
391
953
|
</div>
|
|
392
|
-
<div id="uploaded-files"></div>
|
|
393
954
|
</div>
|
|
955
|
+
</aside>
|
|
394
956
|
|
|
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>
|
|
957
|
+
<!-- Header -->
|
|
958
|
+
<header class="app-header">
|
|
959
|
+
<div class="header-stepper" id="stepper"></div>
|
|
960
|
+
</header>
|
|
410
961
|
|
|
411
|
-
<!--
|
|
412
|
-
<
|
|
413
|
-
<
|
|
414
|
-
|
|
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
|
}
|