sentinel-verify 1.0.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/LICENSE +21 -0
  3. package/README.md +127 -0
  4. package/dist/cli/commands/hook.js +133 -0
  5. package/dist/cli/commands/hook.js.map +1 -0
  6. package/dist/cli/commands/init.js +21 -0
  7. package/dist/cli/commands/init.js.map +1 -0
  8. package/dist/cli/commands/testHallucination.js +35 -0
  9. package/dist/cli/commands/testHallucination.js.map +1 -0
  10. package/dist/cli/commands/testSecurity.js +35 -0
  11. package/dist/cli/commands/testSecurity.js.map +1 -0
  12. package/dist/cli/commands/testStyle.js +41 -0
  13. package/dist/cli/commands/testStyle.js.map +1 -0
  14. package/dist/cli/commands/testVerify.js +36 -0
  15. package/dist/cli/commands/testVerify.js.map +1 -0
  16. package/dist/cli/diffInput.js +17 -0
  17. package/dist/cli/diffInput.js.map +1 -0
  18. package/dist/cli/index.js +93 -0
  19. package/dist/cli/index.js.map +1 -0
  20. package/dist/config/env.js +16 -0
  21. package/dist/config/env.js.map +1 -0
  22. package/dist/config/sentinelrc.js +104 -0
  23. package/dist/config/sentinelrc.js.map +1 -0
  24. package/dist/dashboard/page.js +270 -0
  25. package/dist/dashboard/page.js.map +1 -0
  26. package/dist/dashboard/server.js +65 -0
  27. package/dist/dashboard/server.js.map +1 -0
  28. package/dist/mcp/server.js +141 -0
  29. package/dist/mcp/server.js.map +1 -0
  30. package/dist/providers/anthropic.js +72 -0
  31. package/dist/providers/anthropic.js.map +1 -0
  32. package/dist/providers/index.js +37 -0
  33. package/dist/providers/index.js.map +1 -0
  34. package/dist/providers/ollama.js +102 -0
  35. package/dist/providers/ollama.js.map +1 -0
  36. package/dist/providers/types.js +3 -0
  37. package/dist/providers/types.js.map +1 -0
  38. package/dist/storage/db.js +123 -0
  39. package/dist/storage/db.js.map +1 -0
  40. package/dist/verify/combine.js +42 -0
  41. package/dist/verify/combine.js.map +1 -0
  42. package/dist/verify/diff.js +27 -0
  43. package/dist/verify/diff.js.map +1 -0
  44. package/dist/verify/evidence.js +25 -0
  45. package/dist/verify/evidence.js.map +1 -0
  46. package/dist/verify/hallucination.js +299 -0
  47. package/dist/verify/hallucination.js.map +1 -0
  48. package/dist/verify/security.js +274 -0
  49. package/dist/verify/security.js.map +1 -0
  50. package/dist/verify/style.js +489 -0
  51. package/dist/verify/style.js.map +1 -0
  52. package/dist/verify/taskMatch.js +107 -0
  53. package/dist/verify/taskMatch.js.map +1 -0
  54. package/dist/version.js +3 -0
  55. package/dist/version.js.map +1 -0
  56. package/package.json +43 -0
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ // Purpose: CLI entry point for the `sentinel` command — wires subcommands (init, mcp, test-verify) to their implementations.
3
+ import { Command } from "commander";
4
+ import { runInit } from "./commands/init.js";
5
+ import { runTestVerify } from "./commands/testVerify.js";
6
+ import { runTestHallucination } from "./commands/testHallucination.js";
7
+ import { runTestSecurity } from "./commands/testSecurity.js";
8
+ import { runTestStyle } from "./commands/testStyle.js";
9
+ import { startMcpServer } from "../mcp/server.js";
10
+ import { startDashboard, DEFAULT_DASHBOARD_PORT } from "../dashboard/server.js";
11
+ import { runInstallHook, runUninstallHook, runHook } from "./commands/hook.js";
12
+ import { SENTINEL_VERSION } from "../version.js";
13
+ const program = new Command();
14
+ program
15
+ .name("sentinel")
16
+ .description("Fully local verification layer for AI coding agents.")
17
+ .version(SENTINEL_VERSION);
18
+ program
19
+ .command("init")
20
+ .description("Set up Sentinel in the current project (creates a local .sentinel/ folder).")
21
+ .action(() => {
22
+ runInit(process.cwd());
23
+ });
24
+ program
25
+ .command("mcp")
26
+ .description("Start the Sentinel MCP server on stdio (used by your AI coding agent, not run by hand).")
27
+ .action(async () => {
28
+ await startMcpServer();
29
+ });
30
+ program
31
+ .command("test-verify")
32
+ .description("Manually run the verify_task_match check against a diff, without an agent.")
33
+ .requiredOption("--task <description>", "The task the change was supposed to accomplish.")
34
+ .option("--diff-file <path>", "Path to a diff file. If omitted, the diff is read from stdin.")
35
+ .action(async (options) => {
36
+ await runTestVerify(process.cwd(), options);
37
+ });
38
+ program
39
+ .command("test-hallucination")
40
+ .description("Manually run the check_hallucination check against a diff, without an agent.")
41
+ .option("--diff-file <path>", "Path to a diff file. If omitted, the diff is read from stdin.")
42
+ .action(async (options) => {
43
+ await runTestHallucination(process.cwd(), options);
44
+ });
45
+ program
46
+ .command("test-security")
47
+ .description("Manually run the check_security check against a diff, without an agent.")
48
+ .option("--diff-file <path>", "Path to a diff file. If omitted, the diff is read from stdin.")
49
+ .action(async (options) => {
50
+ await runTestSecurity(process.cwd(), options);
51
+ });
52
+ program
53
+ .command("test-style")
54
+ .description("Manually run the check_style_consistency check against a diff, without an agent.")
55
+ .option("--diff-file <path>", "Path to a diff file. If omitted, the diff is read from stdin.")
56
+ .action(async (options) => {
57
+ await runTestStyle(process.cwd(), options);
58
+ });
59
+ program
60
+ .command("dashboard")
61
+ .description("Open a local-only dashboard showing this project's verification history.")
62
+ .option("--port <number>", "Port to listen on (127.0.0.1 only).", String(DEFAULT_DASHBOARD_PORT))
63
+ .action((options) => {
64
+ const port = Number(options.port);
65
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
66
+ throw new Error(`Invalid port "${options.port}" — use a number between 1 and 65535.`);
67
+ }
68
+ startDashboard(process.cwd(), port);
69
+ });
70
+ program
71
+ .command("install-hook")
72
+ .description("Install a git pre-commit hook that checks staged changes before every commit.")
73
+ .action(() => {
74
+ runInstallHook();
75
+ });
76
+ program
77
+ .command("uninstall-hook")
78
+ .description("Remove the Sentinel pre-commit hook (refuses to touch hooks Sentinel didn't create).")
79
+ .action(() => {
80
+ runUninstallHook();
81
+ });
82
+ program
83
+ .command("hook-run", { hidden: true })
84
+ .description("Internal: executed by the pre-commit hook to check the staged diff.")
85
+ .action(async () => {
86
+ await runHook();
87
+ });
88
+ program.parseAsync().catch((error) => {
89
+ const message = error instanceof Error ? error.message : String(error);
90
+ console.error(`sentinel: ${message}`);
91
+ process.exit(1);
92
+ });
93
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ // Purpose: loads a git-ignored .env file from the project directory so API keys never need to live in the repo.
2
+ import { join } from "node:path";
3
+ /**
4
+ * Loads environment variables from <projectDir>/.env if the file exists.
5
+ * Variables already set in the real environment always win — the .env file
6
+ * only fills in gaps. Silently does nothing when no .env file is present.
7
+ */
8
+ export function loadLocalEnv(projectDir) {
9
+ try {
10
+ process.loadEnvFile(join(projectDir, ".env"));
11
+ }
12
+ catch {
13
+ // No .env file — that's fine; keys may come from the shell environment.
14
+ }
15
+ }
16
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/config/env.ts"],"names":[],"mappings":"AAAA,gHAAgH;AAEhH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,UAAkB;IAC7C,IAAI,CAAC;QACH,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;IAC1E,CAAC;AACH,CAAC"}
@@ -0,0 +1,104 @@
1
+ // Purpose: loads and validates the optional .sentinelrc project config — tech-stack context, style overrides, per-tool toggles, and custom rules.
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { z } from "zod/v4";
5
+ export const TOOL_KEYS = [
6
+ "verify_task_match",
7
+ "check_hallucination",
8
+ "check_security",
9
+ "check_style_consistency",
10
+ ];
11
+ /** Tools that can run in the pre-commit hook — verify_task_match needs a task description, which doesn't exist at commit time. */
12
+ export const HOOK_TOOL_KEYS = [
13
+ "check_hallucination",
14
+ "check_security",
15
+ "check_style_consistency",
16
+ ];
17
+ /** What the pre-commit hook runs when .sentinelrc doesn't say otherwise. */
18
+ export const DEFAULT_HOOK_TOOLS = ["check_security"];
19
+ // Strict objects: unknown keys are validation errors, so typos fail loudly
20
+ // instead of being silently ignored.
21
+ const styleSchema = z
22
+ .strictObject({
23
+ indent: z.union([z.literal("tabs"), z.number().int().min(1).max(8)]).optional(),
24
+ quotes: z.enum(["single", "double"]).optional(),
25
+ semicolons: z.enum(["always", "never"]).optional(),
26
+ naming: z.enum(["camelCase", "snake_case"]).optional(),
27
+ moduleStyle: z.enum(["esm", "commonjs"]).optional(),
28
+ })
29
+ .optional();
30
+ const configSchema = z.strictObject({
31
+ /** Free-text description of the project's tech stack, fed to all model prompts. */
32
+ context: z.string().max(2000, "context must be at most 2000 characters").optional(),
33
+ /** Style conventions that REPLACE inference for the given dimension. */
34
+ style: styleSchema,
35
+ /** Per-tool toggles; a missing entry means enabled. */
36
+ tools: z
37
+ .strictObject({
38
+ verify_task_match: z.boolean().optional(),
39
+ check_hallucination: z.boolean().optional(),
40
+ check_security: z.boolean().optional(),
41
+ check_style_consistency: z.boolean().optional(),
42
+ })
43
+ .optional(),
44
+ /** Project-specific policies, in plain language, fed to the model layer. */
45
+ rules: z
46
+ .array(z.string().max(500, "each rule must be at most 500 characters"))
47
+ .max(20, "at most 20 rules")
48
+ .optional(),
49
+ /** Pre-commit hook settings. */
50
+ hook: z
51
+ .strictObject({
52
+ /** Which tools the hook runs (default: check_security only). verify_task_match cannot run in a hook. */
53
+ tools: z.array(z.enum(HOOK_TOOL_KEYS)).min(1, "hook.tools must list at least one tool").optional(),
54
+ })
55
+ .optional(),
56
+ });
57
+ const CONFIG_FILENAMES = [".sentinelrc", ".sentinelrc.json"];
58
+ /**
59
+ * Loads .sentinelrc from the project root. Returns null when no config file
60
+ * exists (zero-config default). Throws a clear error when the file is
61
+ * malformed — a broken config must never be silently ignored.
62
+ */
63
+ export function loadSentinelrc(projectDir) {
64
+ const filename = CONFIG_FILENAMES.find((name) => existsSync(join(projectDir, name)));
65
+ if (!filename)
66
+ return null;
67
+ const path = join(projectDir, filename);
68
+ let raw;
69
+ try {
70
+ raw = JSON.parse(readFileSync(path, "utf8"));
71
+ }
72
+ catch (error) {
73
+ throw new Error(`${filename} is not valid JSON: ${error instanceof Error ? error.message : String(error)}. ` +
74
+ `Fix the file (or delete it to run with defaults).`);
75
+ }
76
+ const parsed = configSchema.safeParse(raw);
77
+ if (!parsed.success) {
78
+ throw new Error(`${filename} is invalid:\n${z.prettifyError(parsed.error)}\n` +
79
+ `Fix the file (or delete it to run with defaults).`);
80
+ }
81
+ return parsed.data;
82
+ }
83
+ /** True unless the config explicitly disables the tool. */
84
+ export function isToolEnabled(config, tool) {
85
+ return config?.tools?.[tool] !== false;
86
+ }
87
+ /**
88
+ * Renders the config's context and rules as prompt blocks, prepended to each
89
+ * tool's user prompt. Empty string when there is nothing to add — prompts
90
+ * stay byte-identical to zero-config behavior.
91
+ */
92
+ export function configPromptBlocks(config) {
93
+ let blocks = "";
94
+ if (config?.context) {
95
+ blocks += `<project_context>\n${config.context}\n</project_context>\n\n`;
96
+ }
97
+ if (config?.rules && config.rules.length > 0) {
98
+ blocks += `<project_rules>\nProject-specific rules — flag clear violations:\n${config.rules
99
+ .map((rule) => `- ${rule}`)
100
+ .join("\n")}\n</project_rules>\n\n`;
101
+ }
102
+ return blocks;
103
+ }
104
+ //# sourceMappingURL=sentinelrc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentinelrc.js","sourceRoot":"","sources":["../../src/config/sentinelrc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,270 @@
1
+ // Purpose: the dashboard's single self-contained HTML page — inline CSS/JS only, no external assets, no telemetry; all data comes from the local /api/data endpoint.
2
+ export const DASHBOARD_HTML = `<!doctype html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <title>Sentinel — local dashboard</title>
8
+ <style>
9
+ :root {
10
+ --surface: #fcfcfb;
11
+ --surface-2: #f2f1ee;
12
+ --border: #e3e2de;
13
+ --text-primary: #0b0b0b;
14
+ --text-secondary: #52514e;
15
+ --accent: #2a78d6;
16
+ --good: #0ca30c;
17
+ --warning: #fab219;
18
+ --critical: #d03b3b;
19
+ }
20
+ @media (prefers-color-scheme: dark) {
21
+ :root {
22
+ --surface: #1a1a19;
23
+ --surface-2: #242423;
24
+ --border: #383835;
25
+ --text-primary: #ffffff;
26
+ --text-secondary: #c3c2b7;
27
+ --accent: #3987e5;
28
+ }
29
+ }
30
+ * { box-sizing: border-box; }
31
+ body {
32
+ margin: 0; padding: 24px;
33
+ background: var(--surface); color: var(--text-primary);
34
+ font: 14px/1.5 system-ui, -apple-system, sans-serif;
35
+ max-width: 960px; margin-inline: auto;
36
+ }
37
+ header { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 20px; }
38
+ h1 { font-size: 18px; margin: 0; }
39
+ h2 { font-size: 13px; margin: 28px 0 10px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.06em; }
40
+ .muted { color: var(--text-secondary); font-size: 12px; }
41
+ .spacer { flex: 1; }
42
+ button {
43
+ background: var(--surface-2); color: var(--text-primary);
44
+ border: 1px solid var(--border); border-radius: 6px;
45
+ padding: 5px 12px; font: inherit; font-size: 13px; cursor: pointer;
46
+ }
47
+ button:hover { border-color: var(--text-secondary); }
48
+
49
+ .tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
50
+ .tile { background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; }
51
+ .tile .num { font-size: 26px; font-weight: 650; letter-spacing: -0.02em; }
52
+ .tile .lbl { font-size: 12px; color: var(--text-secondary); margin-top: 2px; }
53
+
54
+ .meter-row { display: grid; grid-template-columns: 170px 1fr 110px; gap: 10px; align-items: center; margin: 8px 0; }
55
+ .meter-label { font-size: 13px; }
56
+ .meter-track { background: var(--surface-2); border-radius: 4px; height: 14px; position: relative; overflow: hidden; }
57
+ .meter-fill { background: var(--accent); height: 100%; border-radius: 4px 0 0 4px; min-width: 0; }
58
+ .meter-value { font-size: 12px; color: var(--text-secondary); text-align: right; font-variant-numeric: tabular-nums; }
59
+
60
+ table { width: 100%; border-collapse: collapse; }
61
+ th { text-align: left; font-size: 11px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; padding: 6px 8px; border-bottom: 1px solid var(--border); }
62
+ td { padding: 7px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
63
+ tr.row { cursor: pointer; }
64
+ tr.row:hover td { background: var(--surface-2); }
65
+ .chip { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
66
+ .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
67
+ .time { font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--text-secondary); }
68
+
69
+ .details { background: var(--surface-2); border-radius: 8px; padding: 12px 14px; }
70
+ .details dt { font-size: 11px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-top: 10px; }
71
+ .details dt:first-child { margin-top: 0; }
72
+ .details dd { margin: 2px 0 0; }
73
+ .issue { border-left: 3px solid var(--border); padding: 4px 10px; margin: 8px 0; }
74
+ .issue .loc { font-family: ui-monospace, monospace; font-size: 12px; word-break: break-all; }
75
+ .issue .src { font-size: 11px; color: var(--text-secondary); }
76
+ .empty { text-align: center; padding: 48px 0; color: var(--text-secondary); }
77
+ </style>
78
+ </head>
79
+ <body>
80
+ <header>
81
+ <h1>Sentinel</h1>
82
+ <span class="muted">local verification dashboard — data never leaves this machine</span>
83
+ <span class="spacer"></span>
84
+ <span class="muted" id="updated"></span>
85
+ <button id="refresh">Refresh</button>
86
+ </header>
87
+ <div id="app"><div class="empty">Loading…</div></div>
88
+
89
+ <script>
90
+ "use strict";
91
+
92
+ const TOOL_NAMES = {
93
+ verify_task_match: "Task match",
94
+ check_hallucination: "Hallucination",
95
+ check_security: "Security",
96
+ check_style_consistency: "Style",
97
+ };
98
+ const CLEAN = new Set(["match", "no_issues"]);
99
+ const WARN = new Set(["partial_match"]);
100
+ const EVIDENCE = "evidence_provided";
101
+
102
+ // All rendering uses createElement/textContent — stored data (diff snippets,
103
+ // reasons) is never injected as HTML.
104
+ function el(tag, className, text) {
105
+ const node = document.createElement(tag);
106
+ if (className) node.className = className;
107
+ if (text !== undefined) node.textContent = text;
108
+ return node;
109
+ }
110
+
111
+ function verdictChip(verdict) {
112
+ const chip = el("span", "chip");
113
+ const dot = el("span", "dot");
114
+ // Evidence handoffs are informational (accent), not good/bad — the calling
115
+ // agent judged, not Sentinel.
116
+ dot.style.background = verdict === EVIDENCE
117
+ ? "var(--accent)"
118
+ : CLEAN.has(verdict)
119
+ ? "var(--good)" : WARN.has(verdict) ? "var(--warning)" : "var(--critical)";
120
+ chip.append(dot, el("span", null, verdict.replace(/_/g, " ")));
121
+ return chip;
122
+ }
123
+
124
+ function fmtTime(iso) {
125
+ const d = new Date(iso);
126
+ return isNaN(d) ? iso : d.toLocaleString();
127
+ }
128
+
129
+ let expandedId = null;
130
+
131
+ function renderDetails(row) {
132
+ const box = el("div", "details");
133
+ const dl = el("dl");
134
+ const add = (label, value) => {
135
+ dl.append(el("dt", null, label), el("dd", null, value));
136
+ };
137
+ if (row.task) add("Task", row.task);
138
+ add("Reason", row.reason);
139
+ let issues = null;
140
+ try { issues = row.details ? JSON.parse(row.details) : null; } catch { /* show raw below */ }
141
+ if (Array.isArray(issues) && issues.length > 0) {
142
+ dl.append(el("dt", null, "Issues (" + issues.length + ")"));
143
+ const dd = el("dd");
144
+ for (const issue of issues) {
145
+ const item = el("div", "issue");
146
+ item.append(el("div", "loc", issue.location ?? ""));
147
+ item.append(el("div", null, issue.reason ?? ""));
148
+ item.append(el("div", "src", "source: " + (issue.source ?? "?")));
149
+ dd.append(item);
150
+ }
151
+ dl.append(dd);
152
+ } else if (row.details && issues === null) {
153
+ add("Details", row.details);
154
+ }
155
+ add("Checked by", row.provider + " (" + row.model + ")");
156
+ box.append(dl);
157
+ return box;
158
+ }
159
+
160
+ function render(data) {
161
+ const app = document.getElementById("app");
162
+ app.textContent = "";
163
+
164
+ const totalRuns = data.summary.reduce((n, s) => n + s.total, 0);
165
+ const totalFlagged = data.summary.reduce((n, s) => n + s.flagged, 0);
166
+
167
+ if (totalRuns === 0) {
168
+ app.append(el("div", "empty",
169
+ "No verifications yet. Run a check (e.g. git diff | sentinel test-security) and refresh."));
170
+ return;
171
+ }
172
+
173
+ // Overview tiles
174
+ app.append(el("h2", null, "Overview"));
175
+ const tiles = el("div", "tiles");
176
+ const tile = (num, lbl) => {
177
+ const t = el("div", "tile");
178
+ t.append(el("div", "num", String(num)), el("div", "lbl", lbl));
179
+ return t;
180
+ };
181
+ tiles.append(tile(totalRuns, "Total verifications"), tile(totalFlagged, "Flagged"));
182
+ const totalEvidence = data.summary.reduce((n, s) => n + (s.evidence ?? 0), 0);
183
+ if (totalEvidence > 0) tiles.append(tile(totalEvidence, "Agent evidence handoffs"));
184
+ for (const s of data.summary) {
185
+ tiles.append(tile(s.total, TOOL_NAMES[s.tool] ?? s.tool));
186
+ }
187
+ app.append(tiles);
188
+
189
+ // Flag rate meters
190
+ app.append(el("h2", null, "Flag rate per tool"));
191
+ for (const s of data.summary) {
192
+ const pct = s.total === 0 ? 0 : Math.round((s.flagged / s.total) * 100);
193
+ const row = el("div", "meter-row");
194
+ row.append(el("div", "meter-label", TOOL_NAMES[s.tool] ?? s.tool));
195
+ const track = el("div", "meter-track");
196
+ const fill = el("div", "meter-fill");
197
+ fill.style.width = pct + "%";
198
+ track.append(fill);
199
+ row.append(track);
200
+ row.append(el("div", "meter-value", s.flagged + " / " + s.total + " (" + pct + "%)"));
201
+ app.append(row);
202
+ }
203
+
204
+ // History
205
+ app.append(el("h2", null, "Recent verifications (click a row for details)"));
206
+ const table = el("table");
207
+ const thead = el("thead");
208
+ const headRow = el("tr");
209
+ for (const h of ["When", "Tool", "Verdict", "Summary"]) headRow.append(el("th", null, h));
210
+ thead.append(headRow);
211
+ const tbody = el("tbody");
212
+ for (const row of data.history) {
213
+ const tr = el("tr", "row");
214
+ tr.append(el("td", "time", fmtTime(row.created_at)));
215
+ tr.append(el("td", null, TOOL_NAMES[row.tool] ?? row.tool));
216
+ const verdictTd = el("td");
217
+ verdictTd.append(verdictChip(row.verdict));
218
+ tr.append(verdictTd);
219
+ tr.append(el("td", "muted", row.reason.length > 90 ? row.reason.slice(0, 90) + "…" : row.reason));
220
+ tr.setAttribute("role", "button");
221
+ tr.setAttribute("tabindex", "0");
222
+ tr.setAttribute("aria-expanded", String(expandedId === row.id));
223
+ const toggle = () => {
224
+ expandedId = expandedId === row.id ? null : row.id;
225
+ render(data);
226
+ };
227
+ tr.addEventListener("click", toggle);
228
+ tr.addEventListener("keydown", (event) => {
229
+ if (event.key === "Enter" || event.key === " ") {
230
+ event.preventDefault();
231
+ toggle();
232
+ }
233
+ });
234
+ tbody.append(tr);
235
+ if (expandedId === row.id) {
236
+ const dtr = el("tr");
237
+ const dtd = el("td");
238
+ dtd.colSpan = 4;
239
+ dtd.append(renderDetails(row));
240
+ dtr.append(dtd);
241
+ tbody.append(dtr);
242
+ }
243
+ }
244
+ table.append(thead, tbody);
245
+ app.append(table);
246
+ }
247
+
248
+ async function load() {
249
+ try {
250
+ const res = await fetch("/api/data");
251
+ if (!res.ok) throw new Error("HTTP " + res.status);
252
+ const data = await res.json();
253
+ render(data);
254
+ document.getElementById("updated").textContent =
255
+ "updated " + new Date().toLocaleTimeString() + " · auto-refreshes every 10s";
256
+ } catch (error) {
257
+ document.getElementById("app").textContent = "";
258
+ document.getElementById("app").append(
259
+ el("div", "empty", "Could not load data (" + error.message + "). Is the server still running?"));
260
+ }
261
+ }
262
+
263
+ document.getElementById("refresh").addEventListener("click", load);
264
+ setInterval(load, 10_000);
265
+ load();
266
+ </script>
267
+ </body>
268
+ </html>
269
+ `;
270
+ //# sourceMappingURL=page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/dashboard/page.ts"],"names":[],"mappings":"AAAA,qKAAqK;AAErK,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2Q7B,CAAC"}
@@ -0,0 +1,65 @@
1
+ // Purpose: the local dashboard server — binds to 127.0.0.1 only and serves the self-contained page plus a JSON endpoint over the local SQLite store.
2
+ import { createServer } from "node:http";
3
+ import { openDatabase, getToolSummary, getRecentVerifications } from "../storage/db.js";
4
+ import { DASHBOARD_HTML } from "./page.js";
5
+ export const DEFAULT_DASHBOARD_PORT = 4747;
6
+ /**
7
+ * Starts the dashboard on 127.0.0.1:<port> and runs until Ctrl+C.
8
+ * Data is read fresh from .sentinel/sentinel.db on every request, so checks
9
+ * running in parallel show up on the next refresh.
10
+ */
11
+ export function startDashboard(projectDir, port) {
12
+ const server = createServer((req, res) => {
13
+ const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
14
+ if (path === "/") {
15
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
16
+ res.end(DASHBOARD_HTML);
17
+ return;
18
+ }
19
+ if (path === "/api/data") {
20
+ try {
21
+ const db = openDatabase(projectDir);
22
+ try {
23
+ const payload = {
24
+ summary: getToolSummary(db),
25
+ history: getRecentVerifications(db),
26
+ };
27
+ res.writeHead(200, { "content-type": "application/json" });
28
+ res.end(JSON.stringify(payload));
29
+ }
30
+ finally {
31
+ db.close();
32
+ }
33
+ }
34
+ catch (error) {
35
+ res.writeHead(500, { "content-type": "application/json" });
36
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
37
+ }
38
+ return;
39
+ }
40
+ res.writeHead(404, { "content-type": "text/plain" });
41
+ res.end("Not found");
42
+ });
43
+ server.on("error", (error) => {
44
+ if (error.code === "EADDRINUSE") {
45
+ console.error(`Port ${port} is already in use. Pick another one: sentinel dashboard --port ${port + 1}`);
46
+ }
47
+ else {
48
+ console.error(`Dashboard failed to start: ${error.message}`);
49
+ }
50
+ process.exit(1);
51
+ });
52
+ server.listen(port, "127.0.0.1", () => {
53
+ console.log(`Sentinel dashboard running at http://127.0.0.1:${port}`);
54
+ console.log("Local only — nothing is exposed to the network. Press Ctrl+C to stop.");
55
+ });
56
+ const shutdown = () => {
57
+ console.log("\nStopping dashboard…");
58
+ server.close(() => process.exit(0));
59
+ // Failsafe: exit even if a connection lingers (e.g. an open browser tab).
60
+ setTimeout(() => process.exit(0), 1000).unref();
61
+ };
62
+ process.on("SIGINT", shutdown);
63
+ process.on("SIGTERM", shutdown);
64
+ }
65
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"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"}