@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.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/server.ts
2
- import { Hono as Hono6 } from "hono";
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 fs5 from "fs";
6
- import * as path4 from "path";
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 productDir = path.join(contextDir, "products", brief.product_name);
26
- fs.mkdirSync(productDir, { recursive: true });
27
- fs.writeFileSync(path.join(productDir, "context-brief.yaml"), stringify(brief), "utf-8");
28
- return c.json({ ok: true, path: `products/${brief.product_name}/context-brief.yaml` });
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, "products", name, "context-brief.yaml");
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 path2 from "path";
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 = path2.extname(file.name).toLowerCase();
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 = path2.join(contextDir, "products", productName, "docs");
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(path2.join(docsDir, safeName), buffer);
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 id = randomUUID();
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
- async function executePipeline(run, _rootDir, _contextDir, _dataSource) {
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 = `${stage.stage} completed`;
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 path3 from "path";
224
- import { parse as parse2 } from "yaml";
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 = path3.join(contextDir, "products");
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 = path3.join(productsDir, name);
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 = path3.join(productsDir, name, "context-brief.yaml");
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 = parse2(fs4.readFileSync(briefPath, "utf-8"));
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 = path4.dirname(url.fileURLToPath(import.meta.url));
260
- var staticDir = path4.resolve(__dirname, "..", "static");
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 Hono6();
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 = path4.join(staticDir, filename);
276
- if (!fs5.existsSync(filePath)) return c.text("Not found", 404);
277
- const ext = path4.extname(filename);
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(fs5.readFileSync(filePath), 200, { "Content-Type": contentType });
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>ContextKit \u2014 Build Your Data Product</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="wizard">
298
- <header class="wizard-header">
299
- <h1>ContextKit</h1>
300
- <p class="tagline">Build your data product. AI handles the rest.</p>
301
- </header>
302
-
303
- <div class="progress-bar">
304
- <div class="progress-step active" data-step="1"><span class="step-num">1</span><span class="step-label">Product</span></div>
305
- <div class="progress-step" data-step="2"><span class="step-num">2</span><span class="step-label">Owner</span></div>
306
- <div class="progress-step" data-step="3"><span class="step-num">3</span><span class="step-label">Context</span></div>
307
- <div class="progress-step" data-step="4"><span class="step-num">4</span><span class="step-label">Review</span></div>
308
- <div class="progress-step" data-step="5"><span class="step-num">5</span><span class="step-label">Build</span></div>
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
- <div class="field">
320
- <label for="description">Description</label>
321
- <div class="textarea-wrapper">
322
- <textarea id="description" class="textarea" rows="4" placeholder="Describe what this data product covers..."></textarea>
323
- <button type="button" id="voice-btn" class="btn-icon" title="Voice input">\u{1F3A4}</button>
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
- </div>
326
- <div class="step-actions">
327
- <div></div>
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 &amp; 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
- </div>
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
- <div class="step-actions">
396
- <button type="button" class="btn btn-secondary" data-prev>Back</button>
397
- <button type="button" class="btn btn-primary" data-next>Next</button>
398
- </div>
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
- <!-- 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
- <footer class="wizard-footer">
422
- <p>Powered by ContextKit \xB7 Open Semantic Interchange</p>
967
+ <!-- Footer -->
968
+ <footer class="app-footer">
969
+ <span>Powered by RunContext &middot; 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(`ContextKit UI running at http://${opts.host === "0.0.0.0" ? "localhost" : opts.host}:${info.port}/setup`);
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
  }