@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 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; }// src/server.ts
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 fs5 = _interopRequireWildcard(_fs); var fs = _interopRequireWildcard(_fs); var fs2 = _interopRequireWildcard(_fs); var fs3 = _interopRequireWildcard(_fs); var fs4 = _interopRequireWildcard(_fs);
6
- var _path = require('path'); var path4 = _interopRequireWildcard(_path); var path = _interopRequireWildcard(_path); var path2 = _interopRequireWildcard(_path); var path3 = _interopRequireWildcard(_path);
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 productDir = path.join(contextDir, "products", brief.product_name);
26
- fs.mkdirSync(productDir, { recursive: true });
27
- fs.writeFileSync(path.join(productDir, "context-brief.yaml"), _yaml.stringify.call(void 0, 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, _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, "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(_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 (e) {
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 (e2) {
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 = 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
 
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
- 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
  }
@@ -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 = 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;
@@ -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', _ => _.description]);
247
- sensitivity = _optionalChain([brief, 'optionalAccess', _2 => _2.sensitivity]);
248
- } catch (e3) {
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 = 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
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 = 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>
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
- </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>
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
- <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" />
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
- <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>
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
- <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
  }