@runcontext/ui 0.5.10 → 0.6.0

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
@@ -357,13 +357,12 @@ function uploadRoutes(contextDir) {
357
357
 
358
358
  // src/routes/api/pipeline.ts
359
359
  import { Hono as Hono4 } from "hono";
360
- import { execFile as execFileCb } from "child_process";
360
+ import { execFile as execFileCb, spawn } from "child_process";
361
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";
362
+ import { existsSync as existsSync3 } from "fs";
363
+ import { join as join4, dirname, resolve } from "path";
364
364
  import { promisify } from "util";
365
365
  import { fileURLToPath } from "url";
366
- import { parse as parseYaml } from "yaml";
367
366
 
368
367
  // src/events.ts
369
368
  import { EventEmitter } from "events";
@@ -399,6 +398,10 @@ function resolveCliBin() {
399
398
  }
400
399
  } catch {
401
400
  }
401
+ const cwdCli = join4(process.cwd(), "packages", "cli", "dist", "index.js");
402
+ if (existsSync3(cwdCli)) {
403
+ return { cmd: process.execPath, prefix: [cwdCli] };
404
+ }
402
405
  if (process.argv[1] && existsSync3(process.argv[1])) {
403
406
  return { cmd: process.execPath, prefix: [process.argv[1]] };
404
407
  }
@@ -469,35 +472,47 @@ function pipelineRoutes(rootDir, contextDir) {
469
472
  if (!run) return c.json({ error: "Not found" }, 404);
470
473
  return c.json(run);
471
474
  });
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 {
475
+ let mcpProcess = null;
476
+ app.post("/api/mcp/start", (c) => {
477
+ if (mcpProcess && !mcpProcess.killed) {
478
+ return c.json({ ok: true, status: "already_running" });
479
+ }
480
+ const cli = resolveCliBin();
481
+ mcpProcess = spawn(cli.cmd, [...cli.prefix, "serve"], {
482
+ cwd: rootDir,
483
+ stdio: ["pipe", "pipe", "ignore"],
484
+ detached: false,
485
+ env: { ...process.env, NODE_OPTIONS: "--no-deprecation" }
486
+ });
487
+ mcpProcess.on("exit", () => {
488
+ mcpProcess = null;
489
+ });
490
+ mcpProcess.on("error", () => {
491
+ mcpProcess = null;
492
+ });
493
+ return c.json({ ok: true, status: "started" });
494
+ });
495
+ app.post("/api/mcp/stop", (c) => {
496
+ if (mcpProcess && !mcpProcess.killed) {
497
+ mcpProcess.kill();
498
+ mcpProcess = null;
486
499
  }
500
+ return c.json({ ok: true, status: "stopped" });
501
+ });
502
+ app.get("/api/mcp/status", (c) => {
503
+ const running = mcpProcess !== null && !mcpProcess.killed;
504
+ return c.json({ running });
505
+ });
506
+ app.get("/api/mcp-config", (c) => {
487
507
  const cli = resolveCliBin();
508
+ const absRoot = resolve(rootDir);
488
509
  const mcpServers = {
489
510
  runcontext: {
490
511
  command: cli.cmd,
491
512
  args: [...cli.prefix, "serve"],
492
- cwd: rootDir
513
+ cwd: absRoot
493
514
  }
494
515
  };
495
- if (connection) {
496
- mcpServers["runcontext-db"] = {
497
- command: "npx",
498
- args: ["--yes", "@runcontext/db", "--url", connection]
499
- };
500
- }
501
516
  return c.json({ mcpServers });
502
517
  });
503
518
  return app;
@@ -521,10 +536,16 @@ function buildCliArgs(stage, dataSource) {
521
536
  if (dataSource) args.push("--source", dataSource);
522
537
  return args;
523
538
  }
524
- case "verify":
525
- return ["verify"];
526
- case "autofix":
527
- return ["fix"];
539
+ case "verify": {
540
+ const args = ["verify"];
541
+ if (dataSource) args.push("--source", dataSource);
542
+ return args;
543
+ }
544
+ case "autofix": {
545
+ const args = ["fix"];
546
+ if (dataSource) args.push("--source", dataSource);
547
+ return args;
548
+ }
528
549
  case "agent-instructions":
529
550
  return ["build"];
530
551
  }
@@ -550,7 +571,7 @@ async function executePipeline(run, rootDir, contextDir, dataSource, sessionId)
550
571
  const cli = resolveCliBin();
551
572
  const { stdout } = await execFile(cli.cmd, [...cli.prefix, ...cliArgs], {
552
573
  cwd: rootDir,
553
- timeout: 12e4,
574
+ timeout: 3e5,
554
575
  env: {
555
576
  ...process.env,
556
577
  NODE_OPTIONS: "--max-old-space-size=4096 --no-deprecation"
@@ -582,8 +603,10 @@ async function executePipeline(run, rootDir, contextDir, dataSource, sessionId)
582
603
  continue;
583
604
  }
584
605
  stage.status = "error";
585
- stage.error = err instanceof Error ? err.message : String(err);
606
+ const errDetail = execErr.stderr || (err instanceof Error ? err.message : String(err));
607
+ stage.error = errDetail;
586
608
  stage.completedAt = (/* @__PURE__ */ new Date()).toISOString();
609
+ console.error(`[pipeline] Stage ${stage.stage} failed:`, errDetail);
587
610
  if (sessionId) {
588
611
  setupBus.emitEvent({
589
612
  type: "pipeline:stage",
@@ -602,36 +625,236 @@ async function executePipeline(run, rootDir, contextDir, dataSource, sessionId)
602
625
  import { Hono as Hono5 } from "hono";
603
626
  import * as fs4 from "fs";
604
627
  import * as path4 from "path";
628
+ import { execFile as execFileCb2 } from "child_process";
629
+ import { promisify as promisify2 } from "util";
605
630
  import { parse as parse3 } from "yaml";
631
+ var execFile2 = promisify2(execFileCb2);
632
+ function detectTier(contextDir, sourceName) {
633
+ const rulesPath = path4.join(contextDir, "rules", `${sourceName}.rules.yaml`);
634
+ const modelPath = path4.join(contextDir, "models", `${sourceName}.osi.yaml`);
635
+ if (fs4.existsSync(rulesPath)) {
636
+ try {
637
+ const rules = parse3(fs4.readFileSync(rulesPath, "utf-8"));
638
+ const realQueries = (rules?.golden_queries || []).filter(
639
+ (q) => q.sql && !q.sql.includes("TODO") && !q.sql.includes("table_name") && q.question && !q.question.includes("TODO")
640
+ );
641
+ const realGuardrails = (rules?.guardrail_filters || rules?.guardrails || []).filter(
642
+ (g) => g.name && !g.name.includes("TODO")
643
+ );
644
+ if (realQueries.length >= 3 && realGuardrails.length >= 1) return "gold";
645
+ } catch {
646
+ }
647
+ }
648
+ if (fs4.existsSync(modelPath)) {
649
+ try {
650
+ const model = parse3(fs4.readFileSync(modelPath, "utf-8"));
651
+ let datasets = model?.tables || model?.models || [];
652
+ if (datasets.length === 0 && Array.isArray(model?.semantic_model)) {
653
+ for (const sm of model.semantic_model) {
654
+ if (Array.isArray(sm.datasets)) datasets.push(...sm.datasets);
655
+ }
656
+ }
657
+ let fieldsWithSamples = 0;
658
+ for (const d of datasets) {
659
+ for (const f of d.columns || d.fields || []) {
660
+ if (f.sample_values?.length > 0) fieldsWithSamples++;
661
+ }
662
+ }
663
+ if (fieldsWithSamples >= 2) return "silver";
664
+ } catch {
665
+ }
666
+ }
667
+ return "bronze";
668
+ }
669
+ function countTablesAndColumns(contextDir, sourceName) {
670
+ const modelPath = path4.join(contextDir, "models", `${sourceName}.osi.yaml`);
671
+ if (!fs4.existsSync(modelPath)) return { tables: 0, columns: 0 };
672
+ try {
673
+ const model = parse3(fs4.readFileSync(modelPath, "utf-8"));
674
+ let datasets = model?.tables || model?.models || [];
675
+ if (datasets.length === 0 && Array.isArray(model?.semantic_model)) {
676
+ for (const sm of model.semantic_model) {
677
+ if (Array.isArray(sm.datasets)) datasets.push(...sm.datasets);
678
+ }
679
+ }
680
+ const columns = datasets.reduce((sum, t) => sum + (t.columns?.length || t.fields?.length || 0), 0);
681
+ return { tables: datasets.length, columns };
682
+ } catch {
683
+ return { tables: 0, columns: 0 };
684
+ }
685
+ }
606
686
  function productsRoutes(contextDir) {
607
687
  const app = new Hono5();
608
688
  app.get("/api/products", (c) => {
609
- const productsDir = path4.join(contextDir, "products");
610
- if (!fs4.existsSync(productsDir)) {
689
+ if (!fs4.existsSync(contextDir)) {
611
690
  return c.json([]);
612
691
  }
613
692
  const products = [];
614
- const dirs = fs4.readdirSync(productsDir).filter((name) => {
615
- const fullPath = path4.join(productsDir, name);
616
- return fs4.statSync(fullPath).isDirectory() && !name.startsWith(".");
693
+ const briefFiles = fs4.readdirSync(contextDir).filter((f) => f.endsWith(".context-brief.yaml"));
694
+ for (const briefFile of briefFiles) {
695
+ const briefPath = path4.join(contextDir, briefFile);
696
+ try {
697
+ const brief = parse3(fs4.readFileSync(briefPath, "utf-8"));
698
+ const name = brief?.product_name || briefFile.replace(".context-brief.yaml", "");
699
+ const modelsDir = path4.join(contextDir, "models");
700
+ let sourceName = "";
701
+ const briefSource = brief?.data_sources?.[0]?.name || brief?.data_source || "";
702
+ if (briefSource && fs4.existsSync(path4.join(modelsDir, `${briefSource}.osi.yaml`))) {
703
+ sourceName = briefSource;
704
+ } else if (fs4.existsSync(modelsDir)) {
705
+ const modelFiles = fs4.readdirSync(modelsDir).filter((f) => f.endsWith(".osi.yaml"));
706
+ if (modelFiles.length > 0) {
707
+ sourceName = modelFiles[0].replace(".osi.yaml", "");
708
+ }
709
+ }
710
+ const { tables, columns } = sourceName ? countTablesAndColumns(contextDir, sourceName) : { tables: 0, columns: 0 };
711
+ const tier = sourceName ? detectTier(contextDir, sourceName) : "bronze";
712
+ products.push({
713
+ name,
714
+ description: brief?.description,
715
+ sensitivity: brief?.sensitivity,
716
+ tier,
717
+ tables,
718
+ columns,
719
+ hasBrief: true
720
+ });
721
+ } catch {
722
+ }
723
+ }
724
+ const seen = /* @__PURE__ */ new Set();
725
+ const unique = products.filter((p) => {
726
+ if (seen.has(p.name)) return false;
727
+ seen.add(p.name);
728
+ return true;
617
729
  });
618
- for (const name of dirs) {
619
- const briefPath = path4.join(productsDir, name, "context-brief.yaml");
620
- let description;
621
- let sensitivity;
622
- let hasBrief = false;
623
- if (fs4.existsSync(briefPath)) {
624
- hasBrief = true;
730
+ return c.json(unique);
731
+ });
732
+ app.get("/api/products/:name/detail", (c) => {
733
+ const modelsDir = path4.join(contextDir, "models");
734
+ const rulesDir = path4.join(contextDir, "rules");
735
+ const govDir = path4.join(contextDir, "governance");
736
+ const glossaryDir = path4.join(contextDir, "glossary");
737
+ const ownersDir = path4.join(contextDir, "owners");
738
+ const modelFiles = fs4.existsSync(modelsDir) ? fs4.readdirSync(modelsDir).filter((f) => f.endsWith(".osi.yaml")) : [];
739
+ const sourceName = modelFiles.length > 0 ? modelFiles[0].replace(".osi.yaml", "") : "";
740
+ let tables = [];
741
+ let modelYaml = "";
742
+ if (sourceName) {
743
+ const modelPath = path4.join(modelsDir, modelFiles[0]);
744
+ modelYaml = fs4.readFileSync(modelPath, "utf-8");
745
+ try {
746
+ const model = parse3(modelYaml);
747
+ let datasets = model?.tables || model?.models || [];
748
+ if (datasets.length === 0 && Array.isArray(model?.semantic_model)) {
749
+ for (const sm of model.semantic_model) {
750
+ if (Array.isArray(sm.datasets)) datasets.push(...sm.datasets);
751
+ }
752
+ }
753
+ tables = datasets.map((d) => {
754
+ const fields = d.columns || d.fields || [];
755
+ return {
756
+ name: d.name,
757
+ description: d.description || "",
758
+ fields: fields.map((f) => ({
759
+ name: f.name,
760
+ type: f.type || f.data_type || "",
761
+ description: f.description || "",
762
+ sampleValues: f.sample_values || [],
763
+ semanticRole: f.semantic_role || ""
764
+ }))
765
+ };
766
+ });
767
+ } catch {
768
+ }
769
+ }
770
+ let rules = {};
771
+ let rulesYaml = "";
772
+ if (sourceName) {
773
+ const rulesPath = path4.join(rulesDir, `${sourceName}.rules.yaml`);
774
+ if (fs4.existsSync(rulesPath)) {
775
+ rulesYaml = fs4.readFileSync(rulesPath, "utf-8");
776
+ try {
777
+ rules = parse3(rulesYaml) || {};
778
+ } catch {
779
+ }
780
+ }
781
+ }
782
+ let governance = {};
783
+ let govYaml = "";
784
+ if (sourceName) {
785
+ const govPath = path4.join(govDir, `${sourceName}.governance.yaml`);
786
+ if (fs4.existsSync(govPath)) {
787
+ govYaml = fs4.readFileSync(govPath, "utf-8");
788
+ try {
789
+ governance = parse3(govYaml) || {};
790
+ } catch {
791
+ }
792
+ }
793
+ }
794
+ const glossary = [];
795
+ if (fs4.existsSync(glossaryDir)) {
796
+ for (const f of fs4.readdirSync(glossaryDir).filter((f2) => f2.endsWith(".term.yaml"))) {
625
797
  try {
626
- const brief = parse3(fs4.readFileSync(briefPath, "utf-8"));
627
- description = brief?.description;
628
- sensitivity = brief?.sensitivity;
798
+ const term = parse3(fs4.readFileSync(path4.join(glossaryDir, f), "utf-8"));
799
+ if (term) glossary.push(term);
629
800
  } catch {
630
801
  }
631
802
  }
632
- products.push({ name, description, sensitivity, hasBrief });
633
803
  }
634
- return c.json(products);
804
+ const owners = [];
805
+ if (fs4.existsSync(ownersDir)) {
806
+ for (const f of fs4.readdirSync(ownersDir).filter((f2) => f2.endsWith(".owner.yaml"))) {
807
+ try {
808
+ const owner = parse3(fs4.readFileSync(path4.join(ownersDir, f), "utf-8"));
809
+ if (owner) owners.push(owner);
810
+ } catch {
811
+ }
812
+ }
813
+ }
814
+ return c.json({
815
+ tables,
816
+ rules: {
817
+ joinRules: rules.join_rules || [],
818
+ goldenQueries: rules.golden_queries || [],
819
+ guardrails: rules.guardrail_filters || rules.guardrails || [],
820
+ grainStatements: rules.grain_statements || []
821
+ },
822
+ governance,
823
+ glossary,
824
+ owners,
825
+ yaml: {
826
+ model: modelYaml.slice(0, 5e4),
827
+ rules: rulesYaml.slice(0, 2e4),
828
+ governance: govYaml.slice(0, 1e4)
829
+ }
830
+ });
831
+ });
832
+ app.get("/api/tier", async (c) => {
833
+ const cwdCli = path4.join(process.cwd(), "packages", "cli", "dist", "index.js");
834
+ const cliPath = fs4.existsSync(cwdCli) ? cwdCli : null;
835
+ if (!cliPath) return c.json({ tier: "unknown", output: "CLI not found" });
836
+ try {
837
+ const { stdout } = await execFile2(process.execPath, [cliPath, "tier"], {
838
+ cwd: process.cwd(),
839
+ timeout: 15e3,
840
+ env: { ...process.env, NODE_OPTIONS: "--no-deprecation" }
841
+ });
842
+ const tierMatch = stdout.match(/(BRONZE|SILVER|GOLD)/i);
843
+ return c.json({ tier: tierMatch ? tierMatch[1].toLowerCase() : "unknown", output: stdout });
844
+ } catch (err) {
845
+ const stdout = err?.stdout || "";
846
+ const tierMatch = stdout.match(/(BRONZE|SILVER|GOLD)/i);
847
+ return c.json({ tier: tierMatch ? tierMatch[1].toLowerCase() : "unknown", output: stdout || err.message });
848
+ }
849
+ });
850
+ app.get("/api/agent-instructions", (c) => {
851
+ const distInstructions = path4.join(process.cwd(), "dist", "AGENT_INSTRUCTIONS.md");
852
+ const cliInstructions = path4.join(process.cwd(), "packages", "cli", "assets", "AGENT_INSTRUCTIONS.md");
853
+ const instrPath = fs4.existsSync(distInstructions) ? distInstructions : fs4.existsSync(cliInstructions) ? cliInstructions : null;
854
+ if (!instrPath) {
855
+ return c.json({ instructions: null, error: "Agent instructions not found" });
856
+ }
857
+ return c.json({ instructions: fs4.readFileSync(instrPath, "utf-8") });
635
858
  });
636
859
  return app;
637
860
  }
@@ -644,7 +867,7 @@ import {
644
867
  } from "@runcontext/core";
645
868
  import * as fs5 from "fs";
646
869
  import * as path5 from "path";
647
- import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
870
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
648
871
  function authRoutes(rootDir) {
649
872
  const app = new Hono6();
650
873
  const registry = createDefaultRegistry();
@@ -734,11 +957,17 @@ function authRoutes(rootDir) {
734
957
  let config = {};
735
958
  if (fs5.existsSync(configPath)) {
736
959
  try {
737
- config = parseYaml2(fs5.readFileSync(configPath, "utf-8")) ?? {};
960
+ config = parseYaml(fs5.readFileSync(configPath, "utf-8")) ?? {};
738
961
  } catch {
739
962
  }
740
963
  }
741
- config.data_sources = config.data_sources ?? {};
964
+ const existingSources = config.data_sources ?? {};
965
+ for (const [key, val] of Object.entries(existingSources)) {
966
+ if (val && typeof val === "object" && !val.connection && !val.path) {
967
+ delete existingSources[key];
968
+ }
969
+ }
970
+ config.data_sources = existingSources;
742
971
  const sourceName = (database.name || database.database || "default").replace(/[^a-zA-Z0-9_-]/g, "_");
743
972
  config.data_sources[sourceName] = { adapter: database.adapter, connection: connStr };
744
973
  fs5.writeFileSync(configPath, stringifyYaml(config), "utf-8");
@@ -753,9 +982,9 @@ function authRoutes(rootDir) {
753
982
 
754
983
  // src/routes/api/suggest-brief.ts
755
984
  import { Hono as Hono7 } from "hono";
756
- import { execFile as execFileCb2 } from "child_process";
757
- import { promisify as promisify2 } from "util";
758
- var execFile2 = promisify2(execFileCb2);
985
+ import { execFile as execFileCb3 } from "child_process";
986
+ import { promisify as promisify3 } from "util";
987
+ var execFile3 = promisify3(execFileCb3);
759
988
  function suggestBriefRoutes(rootDir) {
760
989
  const app = new Hono7();
761
990
  app.post("/api/suggest-brief", async (c) => {
@@ -778,7 +1007,7 @@ function suggestBriefRoutes(rootDir) {
778
1007
  let ownerEmail = "";
779
1008
  let ownerTeam = "";
780
1009
  try {
781
- const { stdout: name } = await execFile2("git", ["config", "user.name"], {
1010
+ const { stdout: name } = await execFile3("git", ["config", "user.name"], {
782
1011
  cwd: rootDir,
783
1012
  timeout: 3e3
784
1013
  });
@@ -786,7 +1015,7 @@ function suggestBriefRoutes(rootDir) {
786
1015
  } catch {
787
1016
  }
788
1017
  try {
789
- const { stdout: email } = await execFile2("git", ["config", "user.email"], {
1018
+ const { stdout: email } = await execFile3("git", ["config", "user.email"], {
790
1019
  cwd: rootDir,
791
1020
  timeout: 3e3
792
1021
  });
@@ -890,26 +1119,38 @@ function createApp(opts) {
890
1119
  app.get("/setup", (c) => {
891
1120
  return c.html(setupPageHTML());
892
1121
  });
1122
+ app.get("/planes", (c) => {
1123
+ return c.html(pageHTML({
1124
+ title: "Semantic Planes",
1125
+ activePage: "planes",
1126
+ contentId: "page-content"
1127
+ }));
1128
+ });
1129
+ app.get("/analytics", (c) => {
1130
+ return c.html(pageHTML({
1131
+ title: "Analytics",
1132
+ activePage: "analytics",
1133
+ contentId: "page-content"
1134
+ }));
1135
+ });
1136
+ app.get("/settings", (c) => {
1137
+ return c.html(pageHTML({
1138
+ title: "Settings",
1139
+ activePage: "settings",
1140
+ contentId: "page-content"
1141
+ }));
1142
+ });
893
1143
  app.get("/", (c) => c.redirect("/setup"));
894
1144
  return app;
895
1145
  }
896
- function setupPageHTML() {
897
- return `<!DOCTYPE html>
898
- <html lang="en">
899
- <head>
900
- <meta charset="utf-8" />
901
- <meta name="viewport" content="width=device-width, initial-scale=1" />
902
- <title>RunContext \u2014 Build Your Data Product</title>
903
- <link rel="preconnect" href="https://fonts.googleapis.com" />
904
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
905
- <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" />
906
- <link rel="stylesheet" href="/static/uxd.css" />
907
- <link rel="stylesheet" href="/static/setup.css" />
908
- </head>
909
- <body>
910
- <div class="app-shell">
911
- <!-- Sidebar -->
912
- <aside class="sidebar">
1146
+ function sidebarHTML(activePage) {
1147
+ const nav = (page, href, label) => {
1148
+ const isActive = activePage === page;
1149
+ return `<a class="nav-item${isActive ? " active" : ""}" href="${href}">
1150
+ <span>${label}</span>
1151
+ </a>`;
1152
+ };
1153
+ return `<aside class="sidebar">
913
1154
  <div class="sidebar-brand">
914
1155
  <svg class="brand-chevron" width="24" height="24" viewBox="0 0 24 24" fill="none">
915
1156
  <path d="M4 4l8 8-8 8" stroke="#c9a55a" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
@@ -921,42 +1162,22 @@ function setupPageHTML() {
921
1162
  <span class="brand-badge">Local</span>
922
1163
  </div>
923
1164
  <nav class="sidebar-nav">
924
- <a class="nav-item active" data-nav="setup">
925
- <span>Setup</span>
926
- </a>
927
- <a class="nav-item locked" data-nav="planes">
928
- <span>Semantic Planes</span>
929
- <svg class="lock-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
930
- <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
931
- <path d="M7 11V7a5 5 0 0110 0v4"/>
932
- </svg>
933
- </a>
934
- <a class="nav-item locked" data-nav="analytics">
935
- <span>Analytics</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
- <a class="nav-item" data-nav="mcp">
1165
+ ${nav("setup", "/setup", "Setup")}
1166
+ ${nav("planes", "/planes", "Semantic Planes")}
1167
+ ${nav("analytics", "/analytics", "Analytics")}
1168
+ <div class="nav-item mcp-toggle" id="mcp-nav-toggle" title="Click to start/stop MCP server" style="cursor:pointer">
942
1169
  <span class="status-dot" id="mcp-status-dot"></span>
943
1170
  <span>MCP Server</span>
944
1171
  <span class="nav-detail" id="mcp-status-text">checking...</span>
945
- </a>
946
- <a class="nav-item locked" data-nav="settings">
947
- <span>Settings</span>
948
- <svg class="lock-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
949
- <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
950
- <path d="M7 11V7a5 5 0 0110 0v4"/>
951
- </svg>
952
- </a>
1172
+ </div>
1173
+ ${nav("settings", "/settings", "Settings")}
953
1174
  </nav>
954
1175
  <div class="sidebar-status">
955
1176
  <div class="status-row">
956
1177
  <span class="status-dot" id="db-status-dot"></span>
957
1178
  <span id="db-status-text">No database</span>
958
1179
  </div>
959
- <div class="status-row">
1180
+ <div class="status-row mcp-toggle" id="mcp-toggle-row" title="Click to start/stop MCP server">
960
1181
  <span class="status-dot" id="mcp-server-dot"></span>
961
1182
  <span id="mcp-server-text">MCP stopped</span>
962
1183
  </div>
@@ -964,36 +1185,80 @@ function setupPageHTML() {
964
1185
  <span class="tier-badge" id="tier-badge">Free</span>
965
1186
  </div>
966
1187
  </div>
967
- </aside>
1188
+ <div class="sidebar-security">
1189
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--rc-color-status-success)" stroke-width="2">
1190
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
1191
+ </svg>
1192
+ <span>Local-only processing</span>
1193
+ </div>
1194
+ </aside>`;
1195
+ }
1196
+ function footerHTML() {
1197
+ return `<footer class="app-footer">
1198
+ <span>Powered by <a href="https://runcontext.dev" target="_blank" rel="noopener">RunContext</a></span>
1199
+ <span class="footer-links">
1200
+ <a href="https://docs.runcontext.dev" target="_blank" rel="noopener">Docs</a>
1201
+ <span class="footer-sep">&middot;</span>
1202
+ <a href="https://runcontext.dev/pricing" target="_blank" rel="noopener">Cloud</a>
1203
+ <span class="footer-sep">&middot;</span>
1204
+ <a href="https://github.com/Quiet-Victory-Labs/runcontext" target="_blank" rel="noopener">GitHub</a>
1205
+ </span>
1206
+ </footer>`;
1207
+ }
1208
+ function pageHTML(opts) {
1209
+ const isSetup = opts.activePage === "setup";
1210
+ const headerContent = isSetup ? `<div class="header-stepper" id="stepper"></div>` : `<h1 class="header-title">${opts.title}</h1>`;
1211
+ const lockedTooltip = isSetup ? `
1212
+ <!-- Locked tooltip (hidden by default) -->
1213
+ <div class="locked-tooltip" id="locked-tooltip" style="display:none">
1214
+ <p><strong>Cloud Feature</strong></p>
1215
+ <p>This feature is available on RunContext Cloud with team collaboration, hosted endpoints, and analytics.</p>
1216
+ <a href="https://runcontext.dev/pricing" target="_blank" rel="noopener">View plans \u2192</a>
1217
+ </div>` : "";
1218
+ return `<!DOCTYPE html>
1219
+ <html lang="en">
1220
+ <head>
1221
+ <meta charset="utf-8" />
1222
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1223
+ <title>RunContext \u2014 ${opts.title}</title>
1224
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
1225
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
1226
+ <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" />
1227
+ <link rel="stylesheet" href="/static/uxd.css" />
1228
+ <link rel="stylesheet" href="/static/setup.css" />
1229
+ </head>
1230
+ <body data-page="${opts.activePage}">
1231
+ <div class="app-shell">
1232
+ <!-- Sidebar -->
1233
+ ${sidebarHTML(opts.activePage)}
968
1234
 
969
1235
  <!-- Header -->
970
1236
  <header class="app-header">
971
- <div class="header-stepper" id="stepper"></div>
1237
+ ${headerContent}
972
1238
  </header>
973
1239
 
974
1240
  <!-- Main Content -->
975
1241
  <main class="main-content">
976
- <div class="content-wrapper" id="wizard-content"></div>
1242
+ <div class="content-wrapper" id="${opts.contentId}"></div>
977
1243
  </main>
978
1244
 
979
1245
  <!-- Footer -->
980
- <footer class="app-footer">
981
- <span>Powered by RunContext &middot; Open Semantic Interchange</span>
982
- </footer>
1246
+ ${footerHTML()}
983
1247
  </div>
984
-
985
- <!-- Locked tooltip (hidden by default) -->
986
- <div class="locked-tooltip" id="locked-tooltip" style="display:none">
987
- <p>Available on RunContext Cloud</p>
988
- <a href="https://runcontext.dev/pricing" target="_blank" rel="noopener">Learn more</a>
989
- </div>
990
-
991
- <script src="/static/setup.js"></script>
1248
+ ${lockedTooltip}
1249
+ <script src="/static/app.js"></script>
992
1250
  </body>
993
1251
  </html>`;
994
1252
  }
1253
+ function setupPageHTML() {
1254
+ return pageHTML({
1255
+ title: "Build Your Context Layer",
1256
+ activePage: "setup",
1257
+ contentId: "wizard-content"
1258
+ });
1259
+ }
995
1260
  function startUIServer(opts) {
996
- return new Promise((resolve2, reject) => {
1261
+ return new Promise((resolve3, reject) => {
997
1262
  const app = createApp(opts);
998
1263
  const server = serve({
999
1264
  fetch: app.fetch,
@@ -1001,7 +1266,7 @@ function startUIServer(opts) {
1001
1266
  hostname: opts.host
1002
1267
  }, (info) => {
1003
1268
  console.log(`RunContext UI running at http://${opts.host === "0.0.0.0" ? "localhost" : opts.host}:${info.port}/setup`);
1004
- resolve2();
1269
+ resolve3();
1005
1270
  });
1006
1271
  attachWebSocket(server);
1007
1272
  server.on("error", reject);