crumbtrail-node 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Crumbtrail contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,242 @@
1
+ # crumbtrail-node
2
+
3
+ Local Crumbtrail HTTP server, MCP server, Express middleware, and package CLI.
4
+
5
+ This package is the local self-host runtime boundary for Crumbtrail. It owns the server process that receives session events, writes local artifacts, post-processes sessions, and exposes MCP-readable evidence.
6
+
7
+ ## Runtime boundary
8
+
9
+ The package runtime entrypoint is the built CLI binary:
10
+
11
+ ```bash
12
+ crumbtrail-server --host 127.0.0.1 --port 9898 --output ~/.crumbtrail/sessions
13
+ ```
14
+
15
+ In this repository, the same boundary is exercised from built output with:
16
+
17
+ ```bash
18
+ pnpm --filter crumbtrail-node verify:package-runtime
19
+ ```
20
+
21
+ The verifier builds `crumbtrail-node`, starts `dist/cli.cjs` from a temporary runtime directory, probes `GET /health`, verifies static file serving, checks safe startup diagnostics, checks degraded health when the output directory becomes unavailable, and shuts the process down. A passing run prints:
22
+
23
+ ```text
24
+ CRUMBTRAIL_PACKAGE_RUNTIME_PASS cli=dist/cli.cjs ...
25
+ ```
26
+
27
+ ## Local configuration contract
28
+
29
+ | Flag | Default | Validation | Purpose |
30
+ | ----------------------- | ---------------------------------: | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
31
+ | `--host` | `127.0.0.1` | Must be non-empty. | Interface for the local server to bind. |
32
+ | `--port` | `9898` | Must be an integer from 1 to 65535. | HTTP port. |
33
+ | `--output` | `~/.crumbtrail/sessions` | Must be a non-empty local path. | Directory where session artifacts are written. |
34
+ | `--static` | unset | If set, path must exist and be a directory. | Optional static directory to serve alongside API/session routes. |
35
+ | `--allow-origin` | localhost origins only | Must be an `http` or `https` origin containing only scheme, host, and optional port. Repeatable. | Additional browser origins allowed by CORS. |
36
+ | `--auth-token` | unset (or `CRUMBTRAIL_AUTH_TOKEN`) | Presence is reported, but token content is never logged. | Optional token required for `/api/*` routes. The `--auth-token` flag wins; otherwise a non-blank `CRUMBTRAIL_AUTH_TOKEN` env var is used. |
37
+ | `--mcp` | `false` | Boolean flag. | Run MCP server mode against the output directory instead of HTTP mode. |
38
+ | `--ai` | `false` | Boolean flag. | Opt into AI diagnosis after finalization. |
39
+ | `--ai-model` | unset | Parsed as an opaque model string. | Model override for AI diagnosis. |
40
+ | `--ai-allow-auto-model` | `false` | Boolean flag. | Allow provider auto-model selection. |
41
+
42
+ Invalid config fails before the server binds and prints a bounded message like:
43
+
44
+ ```text
45
+ crumbtrail-server config error [invalid_port]: Invalid --port: expected an integer from 1 to 65535.
46
+ ```
47
+
48
+ Startup diagnostics report the resolved listening URL, session output directory, static directory when configured, allowed-origin count, auth protection enabled state, and AI opt-in state. They do not print auth token contents.
49
+
50
+ ## Health diagnostics
51
+
52
+ HTTP mode exposes:
53
+
54
+ ```bash
55
+ curl http://127.0.0.1:9898/health
56
+ ```
57
+
58
+ A healthy response has this shape:
59
+
60
+ ```json
61
+ {
62
+ "ok": true,
63
+ "status": "ready",
64
+ "service": "crumbtrail-node",
65
+ "version": "0.1.0",
66
+ "timestamp": "2026-06-29T00:00:00.000Z",
67
+ "uptimeMs": 1234,
68
+ "config": {
69
+ "host": "127.0.0.1",
70
+ "port": 9898,
71
+ "outputDir": "/Users/example/.crumbtrail/sessions",
72
+ "staticDir": "./examples/basic",
73
+ "authEnabled": true,
74
+ "allowedOriginCount": 1,
75
+ "aiEnabled": false,
76
+ "mcpMode": false
77
+ },
78
+ "checks": {
79
+ "outputDir": {
80
+ "path": "/Users/example/.crumbtrail/sessions",
81
+ "exists": true,
82
+ "writable": true
83
+ },
84
+ "staticDir": {
85
+ "configured": true,
86
+ "path": "./examples/basic",
87
+ "exists": true
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ If the output directory becomes unavailable while the server is running, `/health` returns HTTP 200 with `ok: false`, `status: "degraded"`, and a bounded filesystem error under `checks.outputDir.error`. This is intentional: health is an inspection surface, not a mutating API.
94
+
95
+ Health output reports auth and allowed-origin configuration as booleans/counts. It must not include auth token contents or raw allowed-origin values.
96
+
97
+ ## Self-host quickstart proof
98
+
99
+ Run the packaged local server plus full-stack Express example proof from the repository root:
100
+
101
+ ```bash
102
+ pnpm verify:self-host
103
+ ```
104
+
105
+ The command builds `crumbtrail-core` and `crumbtrail-node`, starts built `dist/cli.cjs`, checks `/health`, triggers the deliberate Express demo failure, finalizes artifacts, and verifies linked `events.ndjson`, `index.json`, `llm.json`, `llm.md`, and MCP context. See [`examples/full-stack-express/README.md`](../../examples/full-stack-express/README.md) for expected output and troubleshooting.
106
+
107
+ ## Fresh-install validation
108
+
109
+ Run the same local self-host behavior through a temporary standalone install:
110
+
111
+ ```bash
112
+ pnpm verify:fresh-install
113
+ ```
114
+
115
+ The verifier builds and packs `crumbtrail-core` and `crumbtrail-node`, installs the packed tarballs into a temporary npm project, resolves the installed `crumbtrail-server` binary, waits for ready `/health`, captures a deliberate failed request session, verifies `events.ndjson`, `index.json`, `llm.json`, `llm.md`, and shuts down cleanly. Passing output includes phase-specific status for package metadata/build, temp install, binary startup, health readiness, self-host artifact proof, and shutdown.
116
+
117
+ For final package validation, run all three packaged-runtime surfaces together:
118
+
119
+ ```bash
120
+ pnpm --filter crumbtrail-node verify:package-runtime && pnpm verify:self-host && pnpm verify:fresh-install
121
+ ```
122
+
123
+ ## CLI subcommands
124
+
125
+ The same `crumbtrail-server` binary exposes subcommands beyond `serve`. Every subcommand accepts
126
+ `--help` / `-h` for focused help, and `crumbtrail-server --version` / `-v` prints the package version.
127
+
128
+ ```bash
129
+ crumbtrail-server --version # print crumbtrail-node version
130
+ crumbtrail-server serve --help # focused help for any subcommand
131
+ crumbtrail-server fix-context <sessionId> --json # ranked, correlated, LLM-ready fix-context.v1 bundle
132
+ crumbtrail-server fix-context <sessionId> # human-readable summary
133
+ crumbtrail-server inspect <sessionId> # hot-plane-only session summary
134
+ crumbtrail-server inspect <sessionId> --json # machine-readable summary
135
+ crumbtrail-server scan ./src --strict # coverage scanner (CI gate); findings carry a suggested fix
136
+ crumbtrail-server doctor --port 9898 # verify capture + correlation + MCP-readability locally
137
+ ```
138
+
139
+ `fix-context` and `inspect` accept either a bare session id (resolved under the sessions dir,
140
+ override with `--output`) or a path to a session directory. Both read hot-plane artifacts
141
+ only and never open the raw event log. `inspect` reports duration, event/error/failed-request
142
+ counts, candidate count, truncation state, and on-disk artifact sizes.
143
+
144
+ ## MCP evidence tools
145
+
146
+ `crumbtrail-server serve --mcp` runs the stdio MCP server against the sessions directory. The
147
+ evidence-first tools are:
148
+
149
+ - `getFixContext(sessionId)` — the `fix-context.v1` bundle (`ranked_candidates`,
150
+ `primary_window` with `frontend` / `backend` / `db_diffs`, `environment`, `repro_hint`).
151
+ - `getSessionManifest(sessionId)` (alias `get_session_manifest`) — manifest metadata,
152
+ markers, timeline, candidates, `accessPattern`. Synthesizes a manifest from `index.json`
153
+ for older sessions.
154
+ - `getWindow(sessionId, t0, t1)` (alias `get_window`) — cold events inside an absolute-ms
155
+ window; bounded and capped (default/max 500) with truncation reporting. The only tool that
156
+ reads the cold stream.
157
+ - `getEvidence(sessionId, ref)` (alias `get_evidence`) — resolve a candidate id,
158
+ interactive-element signature, or request/event id from hot-plane artifacts.
159
+
160
+ ## Database diffing
161
+
162
+ Four engine shims wrap a duck-typed driver object the host injects (no driver dependency is
163
+ ever imported) so INSERT/UPDATE/DELETE statements executed inside a request scope record a
164
+ `k:'db.diff'` event (`{ engine, op, table, pk, after, before?, requestId }`):
165
+
166
+ | Engine | Wrap | After-image strategy |
167
+ | -------- | ---------------------------------------- | ------------------------------------------------------------------------------ |
168
+ | postgres | `instrumentPgClient(client, options)` | appends `RETURNING *` |
169
+ | mysql | `instrumentMysqlClient(client, options)` | post-`SELECT` by `insertId` / pk (no SQL rewriting) |
170
+ | mssql | `instrumentMssqlPool(pool, options)` | injects `OUTPUT INSERTED.*` / `DELETED.*` (rows stripped from the host result) |
171
+ | sqlite | `instrumentSqliteDatabase(db, options)` | post-`SELECT` by `lastInsertRowid` / pk (fully synchronous) |
172
+
173
+ All four take the same `InstrumentDbClientOptions` and share the same guarantees: the host
174
+ query never fails and never runs twice because of instrumentation — parse/correlation/capture/
175
+ emit failures degrade to "no diff emitted", and statements the shim cannot confidently handle
176
+ (multi-statement batches, comment-wedged SQL on mssql, multi-row MySQL inserts) fall back to an
177
+ image-less `db.diff` (`pk: null`, `rowCount`) so the write stays visible to differencing.
178
+ Sensitive columns are dropped before any event rests (`DEFAULT_SENSITIVE_DB_COLUMNS` =
179
+ `password`, `token`, `secret`, `api_key`, `ssn`; extend with `redactColumns`).
180
+ `captureBefore: true` also records UPDATE pre-images (and is how MySQL/SQLite before-images are
181
+ sourced); `captureReads: true` opts into capped `db.read` row capture. The events correlate by
182
+ `requestId` (= the request's trace id), so they land in the same evidence window, fill
183
+ `primary_window.db_diffs` in the fix-context bundle, and feed session db differencing across
184
+ all engines. Per-engine wiring examples: `docs/integrations/databases.md`.
185
+
186
+ ## Headless job-run sessions
187
+
188
+ Queue workers, cron jobs, and batch runs can create a session without a browser:
189
+
190
+ ```ts
191
+ import { startHeadlessSession } from "crumbtrail-node";
192
+
193
+ const session = await startHeadlessSession({
194
+ endpoint: "http://127.0.0.1:9898",
195
+ sessionId: `job-${Date.now()}`,
196
+ metadata: {
197
+ app: "billing-worker",
198
+ release: process.env.RELEASE,
199
+ build: process.env.GIT_SHA,
200
+ },
201
+ });
202
+
203
+ await session.record({
204
+ t: Date.now(),
205
+ k: "con",
206
+ d: { lv: "info", msg: "job started" },
207
+ });
208
+ await session.end();
209
+ ```
210
+
211
+ If the job already exports OpenTelemetry, stamp spans/logs with the same
212
+ `crumbtrail.session.id`; Crumbtrail files those signals into the same agent-readable
213
+ session as logs and row diffs.
214
+
215
+ ## Two-plane storage (operator note)
216
+
217
+ Finalized sessions are written across two planes under
218
+ `<output>/<sessionId>/`. The **hot plane** holds the small, redacted, AI-readable summaries an
219
+ LLM reads first — `manifest.json` (the entry point), `bundle.json`/`llm.json`, `index.json`,
220
+ `candidates.jsonl`, plus `llm.md`/`timeline.md` and `search.jsonl`. The **cold plane** holds
221
+ the full chronological event stream, zstd-compressed as `events.ndjson.zst`, alongside
222
+ `signatures.json` (the interactive-element signature dictionary) and any media
223
+ (`recording.webm`, `audio.webm`, `frames/`). Redaction runs **before** the cold write
224
+ (`cold.transcode.redaction: "sanitized-before-cold-write"`), and the cold event stream is
225
+ opened only when raw chronological evidence is required (zstd needs Node ≥ 22.15.0). The
226
+ manifest's `accessPattern` field documents this read order for tools and operators.
227
+
228
+ ## Public API boundary
229
+
230
+ The package exports the server and integration primitives used by local self-host integrations:
231
+
232
+ - `createServer`
233
+ - `SessionManager`
234
+ - `McpServer`
235
+ - `createCrumbtrailExpressMiddleware`
236
+ - `createCrumbtrailExpressErrorMiddleware`
237
+
238
+ The `src/__tests__/package-boundary.test.ts` suite locks the package metadata, built CLI path, public exports, and default CLI configuration. The `src/__tests__/config.test.ts` and `src/__tests__/cli.test.ts` suites lock config validation and safe startup diagnostics. The `src/__tests__/health.test.ts` and server health tests lock health payload safety and degraded output-directory behavior.
239
+
240
+ ## What this does not claim yet
241
+
242
+ This package is not yet a production/cloud hosting story. M003 proves local self-host packaging and fresh-install validation; later work can still expand deployment guides and hosted operations.
@@ -0,0 +1,201 @@
1
+ import "./chunk-QGM4M3NI.js";
2
+
3
+ // ../core/dist/bug-widget-LGIRLS76.js
4
+ var WIDGET_CSS = `
5
+ :host {
6
+ all: initial;
7
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
8
+ font-size: 14px;
9
+ color: #e0e0e0;
10
+ }
11
+
12
+ .bl-trigger {
13
+ position: fixed;
14
+ bottom: 20px;
15
+ right: 20px;
16
+ width: 48px;
17
+ height: 48px;
18
+ border-radius: 50%;
19
+ background: #dc2626;
20
+ border: none;
21
+ cursor: pointer;
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
26
+ z-index: 2147483647;
27
+ transition: transform 0.15s ease, background 0.15s ease;
28
+ }
29
+ .bl-trigger:hover {
30
+ transform: scale(1.1);
31
+ background: #ef4444;
32
+ }
33
+ .bl-trigger svg {
34
+ width: 24px;
35
+ height: 24px;
36
+ fill: white;
37
+ }
38
+
39
+ .bl-popover {
40
+ position: fixed;
41
+ bottom: 80px;
42
+ right: 20px;
43
+ width: 320px;
44
+ background: #1e1e1e;
45
+ border: 1px solid #333;
46
+ border-radius: 12px;
47
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
48
+ z-index: 2147483647;
49
+ padding: 16px;
50
+ display: none;
51
+ }
52
+ .bl-popover.open {
53
+ display: block;
54
+ }
55
+
56
+ .bl-popover h3 {
57
+ margin: 0 0 12px;
58
+ font-size: 15px;
59
+ font-weight: 600;
60
+ color: #f5f5f5;
61
+ }
62
+
63
+ .bl-note {
64
+ width: 100%;
65
+ min-height: 60px;
66
+ background: #2a2a2a;
67
+ border: 1px solid #444;
68
+ border-radius: 8px;
69
+ color: #e0e0e0;
70
+ padding: 8px 10px;
71
+ font-size: 13px;
72
+ font-family: inherit;
73
+ resize: vertical;
74
+ box-sizing: border-box;
75
+ }
76
+ .bl-note::placeholder {
77
+ color: #777;
78
+ }
79
+ .bl-note:focus {
80
+ outline: none;
81
+ border-color: #dc2626;
82
+ }
83
+
84
+ .bl-actions {
85
+ display: flex;
86
+ gap: 8px;
87
+ margin-top: 12px;
88
+ align-items: center;
89
+ }
90
+
91
+ .bl-btn {
92
+ padding: 8px 16px;
93
+ border-radius: 8px;
94
+ border: none;
95
+ cursor: pointer;
96
+ font-size: 13px;
97
+ font-weight: 500;
98
+ transition: background 0.15s ease;
99
+ }
100
+ .bl-submit {
101
+ background: #dc2626;
102
+ color: white;
103
+ flex: 1;
104
+ }
105
+ .bl-submit:hover {
106
+ background: #ef4444;
107
+ }
108
+ .bl-submit:disabled {
109
+ opacity: 0.5;
110
+ cursor: not-allowed;
111
+ }
112
+
113
+ .bl-status {
114
+ font-size: 11px;
115
+ color: #777;
116
+ margin-top: 8px;
117
+ }
118
+
119
+ .bl-hint {
120
+ font-size: 11px;
121
+ color: #555;
122
+ margin-top: 8px;
123
+ text-align: center;
124
+ }
125
+ `;
126
+ var BUG_SVG = `<svg viewBox="0 0 24 24"><path d="M20 8h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5s-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"/></svg>`;
127
+ function mountWidget(logger) {
128
+ const host = document.createElement("div");
129
+ host.id = "crumbtrail-widget";
130
+ const shadow = host.attachShadow({ mode: "closed" });
131
+ const style = document.createElement("style");
132
+ style.textContent = WIDGET_CSS;
133
+ shadow.appendChild(style);
134
+ const trigger = document.createElement("button");
135
+ trigger.className = "bl-trigger";
136
+ trigger.innerHTML = BUG_SVG;
137
+ trigger.title = "Flag a bug (Ctrl+Shift+B)";
138
+ shadow.appendChild(trigger);
139
+ const popover = document.createElement("div");
140
+ popover.className = "bl-popover";
141
+ popover.innerHTML = `
142
+ <h3>Flag a Bug</h3>
143
+ <textarea class="bl-note" placeholder="What went wrong? (optional)"></textarea>
144
+ <div class="bl-actions">
145
+ <button class="bl-btn bl-submit">Send Bug Report</button>
146
+ </div>
147
+ <div class="bl-status"></div>
148
+ <div class="bl-hint">Ctrl+Shift+B to toggle</div>
149
+ `;
150
+ shadow.appendChild(popover);
151
+ const noteInput = popover.querySelector(".bl-note");
152
+ const submitBtn = popover.querySelector(".bl-submit");
153
+ const statusEl = popover.querySelector(".bl-status");
154
+ let isOpen = false;
155
+ function toggle() {
156
+ isOpen = !isOpen;
157
+ popover.classList.toggle("open", isOpen);
158
+ if (isOpen) {
159
+ noteInput.focus();
160
+ } else {
161
+ reset();
162
+ }
163
+ }
164
+ function reset() {
165
+ noteInput.value = "";
166
+ statusEl.textContent = "";
167
+ }
168
+ trigger.addEventListener("click", toggle);
169
+ submitBtn.addEventListener("click", async () => {
170
+ submitBtn.disabled = true;
171
+ statusEl.textContent = "Sending...";
172
+ try {
173
+ const { bugId } = await logger.flagBug({
174
+ note: noteInput.value || void 0
175
+ });
176
+ statusEl.textContent = `Saved: ${bugId}`;
177
+ setTimeout(() => {
178
+ toggle();
179
+ submitBtn.disabled = false;
180
+ }, 1500);
181
+ } catch {
182
+ statusEl.textContent = "Failed to send";
183
+ submitBtn.disabled = false;
184
+ }
185
+ });
186
+ function onKeyDown(e) {
187
+ if (e.ctrlKey && e.shiftKey && e.key === "B") {
188
+ e.preventDefault();
189
+ toggle();
190
+ }
191
+ }
192
+ document.addEventListener("keydown", onKeyDown);
193
+ document.body.appendChild(host);
194
+ return () => {
195
+ document.removeEventListener("keydown", onKeyDown);
196
+ host.remove();
197
+ };
198
+ }
199
+ export {
200
+ mountWidget
201
+ };