@agent-inspect/studio 6.3.0 → 6.4.1

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
@@ -4,6 +4,7 @@ var promises = require('fs/promises');
4
4
  var path8 = require('path');
5
5
  var advanced = require('agent-inspect/advanced');
6
6
  var workspace = require('agent-inspect/workspace');
7
+ var fs = require('fs');
7
8
  var Database = require('better-sqlite3');
8
9
  var crypto = require('crypto');
9
10
  var http = require('http');
@@ -317,11 +318,16 @@ function isPostgresDbPath(dbPath) {
317
318
  function openStudioDb(dbPath) {
318
319
  if (isPostgresDbPath(dbPath)) {
319
320
  throw new Error(
320
- "Postgres studio databases are not implemented in v6.0.0; use a SQLite file path."
321
+ "Postgres studio databases are not supported; use a SQLite file path (preview only)."
321
322
  );
322
323
  }
323
324
  const dir = path8__default.default.dirname(dbPath);
324
- void promises.mkdir(dir, { recursive: true });
325
+ try {
326
+ fs.mkdirSync(dir, { recursive: true });
327
+ } catch (error) {
328
+ const message = error instanceof Error ? error.message : String(error);
329
+ throw new Error(`Failed to create studio database directory: ${message}`);
330
+ }
325
331
  const db = new Database__default.default(dbPath);
326
332
  db.pragma("journal_mode = WAL");
327
333
  db.exec(SCHEMA_SQL);
@@ -975,27 +981,147 @@ var studioIndexHtml = `<!DOCTYPE html>
975
981
  <html lang="en">
976
982
  <head>
977
983
  <meta charset="utf-8" />
984
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'" />
978
985
  <title>AgentInspect Studio</title>
979
986
  <style>
980
- body { font-family: system-ui, sans-serif; margin: 1.5rem; line-height: 1.4; }
981
- h1 { font-size: 1.25rem; }
987
+ body { font-family: system-ui, sans-serif; margin: 0; line-height: 1.4; color: #111; }
988
+ header { padding: 1rem 1.5rem; border-bottom: 1px solid #ddd; background: #fafafa; }
989
+ nav a { margin-right: 1rem; color: #0b57d0; text-decoration: none; }
990
+ nav a.active { font-weight: 600; text-decoration: underline; }
991
+ main { padding: 1.5rem; max-width: 1100px; }
982
992
  .muted { color: #666; }
993
+ table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
994
+ th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; vertical-align: top; }
995
+ th { background: #f6f6f6; }
996
+ pre { background: #f4f4f5; padding: 1rem; overflow: auto; max-height: 60vh; }
997
+ .error { color: #b42318; }
983
998
  </style>
984
999
  </head>
985
1000
  <body>
986
- <h1>AgentInspect Studio</h1>
987
- <p class="muted">Self-hosted, read-only. JSONL and workspace manifests remain canonical.</p>
988
- <p id="status">Loading health\u2026</p>
1001
+ <header>
1002
+ <h1>AgentInspect Studio</h1>
1003
+ <p class="muted">Self-hosted, read-only. SQLite only. JSONL remains canonical.</p>
1004
+ <nav id="nav">
1005
+ <a href="#projects" data-page="projects">Projects</a>
1006
+ <a href="#runs" data-page="runs">Runs</a>
1007
+ <a href="#sessions" data-page="sessions">Sessions</a>
1008
+ <a href="#suites" data-page="suites">Suites</a>
1009
+ <a href="#safety" data-page="safety">Safety</a>
1010
+ <a href="#imports" data-page="imports">Imports</a>
1011
+ <a href="#search" data-page="search">Search</a>
1012
+ </nav>
1013
+ </header>
1014
+ <main id="content"><p class="muted">Loading\u2026</p></main>
989
1015
  <script>
990
- fetch("/api/health")
991
- .then((res) => res.json())
992
- .then((data) => {
993
- document.getElementById("status").textContent =
994
- data.ok ? "Studio is running (read-only)." : "Studio health check failed.";
995
- })
996
- .catch(() => {
997
- document.getElementById("status").textContent = "Studio health check failed.";
1016
+ function escapeHtml(value) {
1017
+ return String(value ?? "")
1018
+ .replaceAll("&", "&amp;")
1019
+ .replaceAll("<", "&lt;")
1020
+ .replaceAll(">", "&gt;")
1021
+ .replaceAll('"', "&quot;")
1022
+ .replaceAll("'", "&#39;");
1023
+ }
1024
+
1025
+ let state = { projects: [], projectId: null };
1026
+
1027
+ async function api(path) {
1028
+ const res = await fetch(path);
1029
+ const data = await res.json();
1030
+ if (!res.ok) throw new Error(data.error || res.statusText);
1031
+ return data;
1032
+ }
1033
+
1034
+ function setPage(page) {
1035
+ document.querySelectorAll("#nav a").forEach((a) => {
1036
+ a.classList.toggle("active", a.dataset.page === page);
998
1037
  });
1038
+ }
1039
+
1040
+ function renderProjects() {
1041
+ setPage("projects");
1042
+ const rows = state.projects.map((p) =>
1043
+ '<tr><td><a href="#runs" data-select-project="' + escapeHtml(p.id) + '">' +
1044
+ escapeHtml(p.label || p.id) + '</a></td><td>' + escapeHtml(p.traceCount) +
1045
+ '</td><td>' + escapeHtml(p.path) + '</td></tr>'
1046
+ ).join("");
1047
+ document.getElementById("content").innerHTML =
1048
+ '<h2>Projects</h2><table><thead><tr><th>Project</th><th>Runs</th><th>Path</th></tr></thead><tbody>' +
1049
+ (rows || '<tr><td colspan="3">No projects imported</td></tr>') + '</tbody></table>';
1050
+ document.querySelectorAll("[data-select-project]").forEach((el) => {
1051
+ el.addEventListener("click", (ev) => {
1052
+ ev.preventDefault();
1053
+ state.projectId = el.getAttribute("data-select-project");
1054
+ location.hash = "runs";
1055
+ renderRuns();
1056
+ });
1057
+ });
1058
+ }
1059
+
1060
+ async function renderRuns() {
1061
+ setPage("runs");
1062
+ if (!state.projectId && state.projects[0]) state.projectId = state.projects[0].id;
1063
+ if (!state.projectId) {
1064
+ document.getElementById("content").innerHTML = '<p>Select a project first.</p>';
1065
+ return;
1066
+ }
1067
+ const data = await api("/api/projects/" + encodeURIComponent(state.projectId) + "/runs");
1068
+ const rows = (data.runs || []).map((r) =>
1069
+ '<tr><td>' + escapeHtml(r.runId) + '</td><td>' + escapeHtml(r.status) +
1070
+ '</td><td>' + escapeHtml(r.name) + '</td><td>' + escapeHtml(r.durationMs) + '</td></tr>'
1071
+ ).join("");
1072
+ document.getElementById("content").innerHTML =
1073
+ '<h2>Runs \u2014 ' + escapeHtml(state.projectId) + '</h2>' +
1074
+ '<table><thead><tr><th>Run ID</th><th>Status</th><th>Name</th><th>Duration ms</th></tr></thead><tbody>' +
1075
+ rows + '</tbody></table>';
1076
+ }
1077
+
1078
+ async function renderSimple(title, path) {
1079
+ setPage(title.toLowerCase());
1080
+ if (!state.projectId && state.projects[0]) state.projectId = state.projects[0].id;
1081
+ const data = await api("/api/projects/" + encodeURIComponent(state.projectId) + "/" + path);
1082
+ document.getElementById("content").innerHTML =
1083
+ '<h2>' + escapeHtml(title) + '</h2><pre>' + escapeHtml(JSON.stringify(data, null, 2)) + '</pre>';
1084
+ }
1085
+
1086
+ async function renderImports() {
1087
+ setPage("imports");
1088
+ const health = await api("/api/health");
1089
+ document.getElementById("content").innerHTML =
1090
+ '<h2>Imports</h2><p>Registry: ' + escapeHtml(health.registryName) + '</p>' +
1091
+ '<pre>' + escapeHtml(JSON.stringify(health.warnings || [], null, 2)) + '</pre>';
1092
+ }
1093
+
1094
+ async function renderSearch() {
1095
+ setPage("search");
1096
+ if (!state.projectId && state.projects[0]) state.projectId = state.projects[0].id;
1097
+ document.getElementById("content").innerHTML =
1098
+ '<h2>Search</h2><p><input id="q" placeholder="run id or name" /> <button id="go">Search</button></p><pre id="out"></pre>';
1099
+ document.getElementById("go").onclick = async () => {
1100
+ const q = document.getElementById("q").value;
1101
+ const data = await api("/api/search?projectId=" + encodeURIComponent(state.projectId) + "&q=" + encodeURIComponent(q));
1102
+ document.getElementById("out").textContent = JSON.stringify(data, null, 2);
1103
+ };
1104
+ }
1105
+
1106
+ async function boot() {
1107
+ const health = await api("/api/health");
1108
+ const projects = await api("/api/projects");
1109
+ state.projects = projects.projects || health.projects || [];
1110
+ const page = (location.hash || "#projects").slice(1);
1111
+ if (page === "runs") return renderRuns();
1112
+ if (page === "sessions") return renderSimple("Sessions", "sessions");
1113
+ if (page === "suites") return renderSimple("Suites", "suites");
1114
+ if (page === "safety") return renderSimple("Safety", "redaction");
1115
+ if (page === "imports") return renderImports();
1116
+ if (page === "search") return renderSearch();
1117
+ return renderProjects();
1118
+ }
1119
+
1120
+ window.addEventListener("hashchange", () => boot().catch(showError));
1121
+ function showError(err) {
1122
+ document.getElementById("content").innerHTML = '<p class="error">' + escapeHtml(String(err)) + '</p>';
1123
+ }
1124
+ boot().catch(showError);
999
1125
  </script>
1000
1126
  </body>
1001
1127
  </html>`;