proteum 2.4.2 → 2.4.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/AGENTS.md +1 -1
- package/agents/project/AGENTS.md +11 -3
- package/agents/project/diagnostics.md +1 -1
- package/agents/project/root/AGENTS.md +4 -3
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/mcp.ts +24 -7
- package/cli/presentation/commands.ts +4 -3
- package/cli/presentation/help.ts +1 -1
- package/cli/presentation/mcp.ts +27 -0
- package/cli/utils/agents.ts +7 -5
- package/common/dev/database.ts +7 -4
- package/docs/agent-routing.md +1 -1
- package/docs/mcp.md +2 -2
- package/package.json +2 -1
- package/server/app/container/console/http-client-error-context.test.cjs +96 -0
- package/server/app/container/console/index.ts +62 -3
- package/server/app/devDatabase.ts +183 -0
- package/server/app/devDiagnostics.ts +2 -71
- package/tests/agents-utils.test.cjs +2 -0
- package/tests/cli-mcp-command.test.cjs +105 -0
- package/tests/mcp.test.cjs +13 -1
package/AGENTS.md
CHANGED
|
@@ -65,7 +65,7 @@ npx prisma migrate dev --config ./prisma.config.ts --name <migration name>
|
|
|
65
65
|
- Keep the developer-facing contract synchronized when framework work changes CLI commands, profiler capabilities, or the `proteum dev` banner. Update the live surfaces together in the same pass: CLI command/help definitions, profiler panels and dev-only endpoints, banner text/examples, and the most relevant agent docs that describe them, especially `AGENTS.md`, `agents/project/AGENTS.md`, `agents/project/root/AGENTS.md`, `agents/project/app-root/AGENTS.md`, `agents/project/diagnostics.md`, and any narrower `agents/project/**/AGENTS.md` file that mentions the changed workflow.
|
|
66
66
|
- Proteum MCP contract: `proteum mcp` is the machine-scope router agents register once, and `proteum dev` exposes each app runtime at `/__proteum/mcp`. `proteum dev` ensures one managed machine MCP daemon is running; do not start a second managed daemon. Agents should start with MCP `workflow_start` using `cwd` or a known `projectId`; ambiguous routing or offline app candidates use `project_resolve { cwd }`, and follow-up live app tools require the returned `projectId`. Dev-hosted app tools are already rooted to their own runtime. Keep MCP tools/resources compact, typed, capped, paginated for full trace detail, and read-only unless a future task explicitly expands the mutation contract. The database diagnostic exception is still read-only: MCP `db_query` and CLI `proteum db query` allow one capped `SELECT`, `SHOW`, or `EXPLAIN` statement only and return rows plus elapsed milliseconds. MCP payloads are compact single-line `proteum-mcp-v1` JSON, not pretty-printed human output. Do not implement MCP tools as thin CLI process wrappers when the data is available through manifest readers, tracked sessions, or dev runtime registries.
|
|
67
67
|
- Keep the same-system trace contract explicit when request instrumentation changes: `TRACE_*` controls the retained dev trace store plus the trace/perf CLI, dev-only HTTP endpoints, and bottom profiler, while `ENABLE_PROFILER` enables the reduced request-local `request.profiling` snapshot and `request.finished` hook payload without retaining finished requests globally unless dev trace is also enabled.
|
|
68
|
-
- Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the banner. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. Every `proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section before the dev loop begins.
|
|
68
|
+
- Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the welcome banner. Terminal `proteum mcp` may print a compact central MCP ready banner when it starts or reuses the managed daemon. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. Every `proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section before the dev loop begins.
|
|
69
69
|
- Keep core changes aligned with the explicit controller/page architecture in `agents/project/root/AGENTS.md` and its standalone composition in `agents/project/AGENTS.md`.
|
|
70
70
|
- Prefer removing framework magic when the same result can be expressed with explicit contracts, generated code, or typed context.
|
|
71
71
|
- Apply the pruning rules from `agents/project/optimizations.md`, especially for webpack plugins, Babel plugins, aliases, helpers, runtime services, and npm packages that are not meaningfully used by both apps.
|
package/agents/project/AGENTS.md
CHANGED
|
@@ -15,6 +15,12 @@ Managed compact root routers must use trigger -> canonical instruction file refe
|
|
|
15
15
|
|
|
16
16
|
## Fast Triggers
|
|
17
17
|
|
|
18
|
+
- If `cwd` is inside `/.codex/worktrees/`, run Worktree Preflight before implementation:
|
|
19
|
+
- Copy `.env` from the main worktree when missing.
|
|
20
|
+
- Run `npx proteum refresh`.
|
|
21
|
+
- Run `npm i` when dependencies are missing or stale.
|
|
22
|
+
- Run `npx proteum runtime status`.
|
|
23
|
+
- For runtime-visible work, start or reuse one tracked `npx proteum dev` session using the Task Lifecycle launch workflow.
|
|
18
24
|
- If you are working in a newly created Proteum worktree, before following the rest of these instructions:
|
|
19
25
|
- Copy `.env` from the main worktree.
|
|
20
26
|
- Run `npx proteum refresh`.
|
|
@@ -24,7 +30,7 @@ Managed compact root routers must use trigger -> canonical instruction file refe
|
|
|
24
30
|
- If the user pastes raw errors without asking for a fix, do not implement changes yet. First run the task-safe local reproduction path: identify the likely app, route, command, or request from the error, boot or reuse the relevant dev server with the elevated-permissions workflow in `Task Lifecycle`, reproduce the failing surface locally, and inspect server output, browser console output, diagnostics, traces, or the smallest relevant command result. If the error does not identify enough context to reproduce, say what is missing and use the available local evidence before guessing. Then list likely causes and, for each one, give probability, why, and how to fix it. After this, every time you implement a fix:
|
|
25
31
|
- test, re-run analysis and give a comparison table of before and after
|
|
26
32
|
- re-print the complete list of suggested fixes, but strike the ones we already implemented or not necessary anymore
|
|
27
|
-
- If the user asks to implement a feature, first inspect the relevant existing surface and state any implementation problem, pain point, attention point, or question you see. If
|
|
33
|
+
- If the user asks to implement a feature, first inspect the relevant existing surface and state any implementation problem, pain point, attention point, inconsistency, missing information, or question you see. If anything needs clarification or a decision, pause before editing, ask the user what decision to take, and resume only after the user answers.
|
|
28
34
|
- If the task is ambiguous, generated, connected, or multi-repo, start with MCP `workflow_start` and then MCP `orient { projectId, query }` only if the bootstrap did not return a sufficient owner or next action; use `npx proteum orient <query>` only when MCP is unavailable or terminal evidence is required.
|
|
29
35
|
- Treat Proteum CLI and MCP output as the workflow router. Treat instruction previews returned by MCP `workflow_start` or `instructions_resolve { projectId }` as the allowed instruction scope for read-only discovery and diagnostics. Read full file contents only before edits or git writes, when returned `fullRead`/`fullReadPolicy` requires it, or when the compact preview is insufficient. Do not read broad instruction folders or every managed instruction file up front.
|
|
30
36
|
- When a Proteum MCP client is available, first call MCP `workflow_start` with `cwd` or a known `projectId`. If it is ambiguous or returns offline app candidates, call `project_resolve { cwd }`, select the intended app root, start exactly one dev server from that app root when needed, then retry `workflow_start`. Pass the returned live `projectId` to every follow-up app-bound MCP tool. `npx proteum dev` ensures one managed machine MCP daemon is running; do not start a second managed daemon. Prefer MCP `runtime_status`, `orient`, `instructions_resolve`, `explain_summary`, `route_candidates`, `doctor`, `diagnose`, `trace_show`, `perf_request`, `logs_tail`, and `db_query` for read-only runtime/status/orientation/owner/route/trace/perf/log/database reads. Do not run CLI equivalents after a successful MCP result for the same read. Do not run broad source searches for route/page/controller ownership after MCP returns the owner. Use CLI commands when you need reproducible terminal validation, dev/build/check workflows, fallback repair, or output to share with a human.
|
|
@@ -56,18 +62,20 @@ Managed compact root routers must use trigger -> canonical instruction file refe
|
|
|
56
62
|
|
|
57
63
|
### Before Editing
|
|
58
64
|
|
|
65
|
+
- Before editing in a `.codex/worktrees` worktree, complete Worktree Preflight from Fast Triggers.
|
|
59
66
|
- Before changing any file, load root-level `CODING_STYLE.md` and any narrower area `AGENTS.md` that applies to the touched files. Do not spend response space explicitly acknowledging those reads unless the user asks.
|
|
60
67
|
|
|
61
68
|
### During Implementation
|
|
62
69
|
|
|
63
70
|
- After running `npx proteum create ...`, adapt the generated code to the real feature instead of leaving placeholder logic in place.
|
|
71
|
+
- If any inconsistency, ambiguity, conflicting source, missing information, or implementation detail needing clarification appears while coding, stop editing immediately, ask the user what decision to take, and resume only after the user answers. Do not silently choose a default or keep implementing under a guessed assumption.
|
|
64
72
|
- When starting a long-lived dev server for an agent task, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit task/thread-scoped session file such as `var/run/proteum/dev/agents/<task>.json`, inspect `npx proteum runtime status` first, then use its exact Start Dev next action so occupied router/HMR ports are avoided. Do not `curl` normal page routes to identify a port owner; use Proteum runtime status or dev-only `/__proteum/*` endpoints. After the server is ready, print the live server URL as a clickable Markdown link.
|
|
65
73
|
- Use `--replace-existing` only when restarting the exact session file started by the current thread/task. Never replace another live session that belongs to a user, another thread, or an unknown owner.
|
|
66
74
|
- Do not start a second `npx proteum dev` server in the same worktree, and do not start a second managed MCP daemon. If machine MCP routing fails, run `npx proteum mcp status` and `npx proteum runtime status` from the intended app root; if no live session exists, use the exact MCP offline or runtime-status next action instead of assuming the manifest default port. If the same app already responds on the configured port without live tracking, use or repair that runtime instead of starting another server. If a live session exists but runtime/MCP is unreachable, stop the listed session file first, then start dev again. Do not run diagnose, trace, or perf reads while runtime health is unreachable. Then retry MCP `workflow_start` and use the returned `projectId`.
|
|
67
75
|
- If the current app depends on local `file:` connected projects, boot every connected producer app too, each with its own task-scoped session file and free port, and run every one of those `proteum dev` processes with elevated permissions outside the sandbox before starting or verifying the consumer app.
|
|
68
76
|
- During `npx proteum dev`, the app exposes the read-only Proteum MCP runtime endpoint at `/__proteum/mcp`; use it for repeated agent reads instead of spawning equivalent diagnostics commands. For route/page/controller ownership, prefer MCP `workflow_start`, `route_candidates { projectId, query }`, or `explain_summary { projectId, query }` over broad `npx proteum explain --routes --controllers --full` dumps.
|
|
69
77
|
- For browser validation, use the browser MCP against the running app. Keep Playwright inside `npx proteum e2e --port <port>` for targeted/full end-to-end suites. Bootstrap protected browser MCP state with `npx proteum session`; bootstrap protected E2E runs with `npx proteum e2e --session-email <email> --session-role <role>`.
|
|
70
|
-
- Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the banner. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. Every `proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section before the dev loop begins.
|
|
78
|
+
- Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the welcome banner. Terminal `proteum mcp` may print a compact central MCP ready banner when it starts or reuses the managed daemon. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. Every `proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section before the dev loop begins.
|
|
71
79
|
|
|
72
80
|
### Before Finishing
|
|
73
81
|
|
|
@@ -332,7 +340,7 @@ Agents working in generated Proteum projects must use this delivery workflow for
|
|
|
332
340
|
4. Proteum check: refresh and validate generated framework contracts after route, page, controller, service, command, or config changes.
|
|
333
341
|
5. Validate unit + E2E: run the relevant unit tests and real-world journey E2E checks before calling the work complete.
|
|
334
342
|
|
|
335
|
-
Unit test expectation: production changes must always add or update focused unit tests
|
|
343
|
+
Unit test expectation: production changes must always add or update focused unit tests and maintain 100% whole-project Vitest unit coverage. Before claiming implementation work complete, run `npx vitest run --coverage` from the repository root and make sure global statements, branches, functions, and lines all meet the configured 100% thresholds. MCP endpoint-reference coverage, touched-path-only coverage, and focused test passes are not substitutes for whole-project Vitest coverage. Any excluded generated files, migrations, framework shims, unreachable defensive branches, or changes that cannot reasonably be unit-tested must be documented in the completion note, and agents must not claim the project has full unit coverage when an exception remains.
|
|
336
344
|
|
|
337
345
|
E2E expectation: real-world journeys must follow the project-local instructions in `tests/e2e/REAL_WORLD_JOURNEY_TESTS.md`. These tests should model complete user workflows, role transitions, permissions, state changes, and cross-view consistency rather than isolated happy paths.
|
|
338
346
|
|
|
@@ -18,7 +18,7 @@ This file is the canonical source of truth for diagnostics, temporary instrument
|
|
|
18
18
|
|
|
19
19
|
- For long-lived dev reproductions, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit task/thread-scoped session file, inspect `npx proteum runtime status` first, then use its exact next action so occupied router/HMR ports and untracked same-app runtimes are handled without page-body probes. After the server is ready, print the live server URL as a clickable Markdown link.
|
|
20
20
|
- Use `--replace-existing` only when restarting the exact session file started by the current thread/task. Never replace another live session that belongs to a user, another thread, or an unknown owner.
|
|
21
|
-
- Only the bare `npx proteum build` and bare `npx proteum dev` commands print the welcome banner and active Proteum installation method. Any extra argument or option skips the banner. Only `npx proteum dev` clears the interactive terminal before rendering and reports connected app names plus successful connected `/ping` checks in the ready banner; keep that in mind when capturing or comparing command logs during diagnosis. Every `npx proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section before the dev loop begins.
|
|
21
|
+
- Only the bare `npx proteum build` and bare `npx proteum dev` commands print the welcome banner and active Proteum installation method. Any extra argument or option skips the welcome banner. Terminal `npx proteum mcp` may print a compact central MCP ready banner when it starts or reuses the managed daemon. Only `npx proteum dev` clears the interactive terminal before rendering and reports connected app names plus successful connected `/ping` checks in the ready banner; keep that in mind when capturing or comparing command logs during diagnosis. Every `npx proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section before the dev loop begins.
|
|
22
22
|
- During `npx proteum dev`, the running app exposes the read-only Proteum MCP transport at `/__proteum/mcp`. Use it for runtime-adjacent agent reads instead of repeatedly spawning equivalent CLI diagnostics.
|
|
23
23
|
- If machine MCP routing fails, run `npx proteum mcp status` and `npx proteum runtime status` from the intended app root. If no live session exists, use the exact MCP offline or runtime-status next action so occupied router/HMR ports are avoided. If the same app already responds on the configured port without live tracking, use or repair that runtime instead of starting another server. Do not `curl` normal page routes to identify which app owns a port; use runtime status or Proteum dev-only endpoints. If a live session exists but runtime/MCP is unreachable, stop the listed session file first, then start dev again. Do not start a second dev server in the same worktree, and do not start a second managed MCP daemon. Do not run diagnose, trace, or perf reads while runtime health is unreachable. Then retry MCP `workflow_start` and use the returned `projectId`.
|
|
24
24
|
- For ownership or repo discovery questions, start with MCP `workflow_start`; use MCP `route_candidates { projectId, query }`, MCP `orient { projectId, query }`, and MCP `explain_summary { projectId, query }` only when the bootstrap owner summary is insufficient. Use `npx proteum orient <query>` or `npx proteum explain owner <query>` only when MCP is unavailable or terminal evidence is required.
|
|
@@ -15,7 +15,7 @@ Managed compact root routers must use trigger -> canonical instruction file refe
|
|
|
15
15
|
## Fast Triggers
|
|
16
16
|
|
|
17
17
|
- If the user pastes raw errors without asking for a fix, do not implement changes yet. First run the task-safe local reproduction path: identify the likely app, route, command, or request from the error, boot or reuse the relevant dev server with the elevated-permissions workflow in `Task Lifecycle`, reproduce the failing surface locally, and inspect server output, browser console output, diagnostics, traces, or the smallest relevant command result. If the error does not identify enough context to reproduce, say what is missing and use the available local evidence before guessing. Then list likely causes and, for each one, give probability, why, and how to fix it.
|
|
18
|
-
- If the user asks to implement a feature, first inspect the relevant existing surface and state any implementation problem, pain point, attention point, or question you see. If
|
|
18
|
+
- If the user asks to implement a feature, first inspect the relevant existing surface and state any implementation problem, pain point, attention point, inconsistency, missing information, or question you see. If anything needs clarification or a decision, pause before editing, ask the user what decision to take, and resume only after the user answers.
|
|
19
19
|
- If the task is ambiguous, generated, connected, or multi-repo, start with MCP `workflow_start` and then MCP `orient { projectId, query }` only if the bootstrap did not return a sufficient owner or next action; use `npx proteum orient <query>` only when MCP is unavailable or terminal evidence is required.
|
|
20
20
|
- Treat Proteum CLI and MCP output as the workflow router. Treat instruction previews returned by MCP `workflow_start` or `instructions_resolve { projectId }` as the allowed instruction scope for read-only discovery and diagnostics. Read full file contents only before edits or git writes, when returned `fullRead`/`fullReadPolicy` requires it, or when the compact preview is insufficient. Do not read broad instruction folders or every managed instruction file up front.
|
|
21
21
|
- When a Proteum MCP client is available, first call MCP `workflow_start` with `cwd` or a known `projectId`. If it is ambiguous or returns offline app candidates, call `project_resolve { cwd }`, select the intended app root, start exactly one dev server from that app root when needed, then retry `workflow_start`. Pass the returned live `projectId` to every follow-up app-bound MCP tool. `npx proteum dev` ensures one managed machine MCP daemon is running; do not start a second managed daemon. Prefer MCP `runtime_status`, `orient`, `instructions_resolve`, `explain_summary`, `route_candidates`, `doctor`, `diagnose`, `trace_show`, `perf_request`, `logs_tail`, and `db_query` for read-only runtime/status/orientation/owner/route/trace/perf/log/database reads. Do not run CLI equivalents after a successful MCP result for the same read. Do not run broad source searches for route/page/controller ownership after MCP returns the owner. Use CLI commands when you need reproducible terminal validation, dev/build/check workflows, fallback repair, or output to share with a human.
|
|
@@ -51,18 +51,19 @@ Managed compact root routers must use trigger -> canonical instruction file refe
|
|
|
51
51
|
### During Implementation
|
|
52
52
|
|
|
53
53
|
- After running `npx proteum create ...`, adapt the generated code to the real feature instead of leaving placeholder logic in place.
|
|
54
|
+
- If any inconsistency, ambiguity, conflicting source, missing information, or implementation detail needing clarification appears while coding, stop editing immediately, ask the user what decision to take, and resume only after the user answers. Do not silently choose a default or keep implementing under a guessed assumption.
|
|
54
55
|
- When starting a long-lived dev server for an agent task, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit task/thread-scoped session file such as `var/run/proteum/dev/agents/<task>.json`, inspect `npx proteum runtime status` first, then use its exact Start Dev next action so occupied router/HMR ports are avoided. Do not `curl` normal page routes to identify a port owner; use Proteum runtime status or dev-only `/__proteum/*` endpoints. After the server is ready, print the live server URL as a clickable Markdown link.
|
|
55
56
|
- Use `--replace-existing` only when restarting the exact session file started by the current thread/task. Never replace another live session that belongs to a user, another thread, or an unknown owner.
|
|
56
57
|
- Do not start a second `npx proteum dev` server in the same worktree, and do not start a second managed MCP daemon. If machine MCP routing fails, run `npx proteum mcp status` and `npx proteum runtime status` from the intended app root; if no live session exists, use the exact MCP offline or runtime-status next action instead of assuming the manifest default port. If the same app already responds on the configured port without live tracking, use or repair that runtime instead of starting another server. If a live session exists but runtime/MCP is unreachable, stop the listed session file first, then start dev again. Do not run diagnose, trace, or perf reads while runtime health is unreachable. Then retry MCP `workflow_start` and use the returned `projectId`.
|
|
57
58
|
- If the current app depends on local `file:` connected projects, boot every connected producer app too, each with its own task-scoped session file and free port, and run every one of those `proteum dev` processes with elevated permissions outside the sandbox before starting or verifying the consumer app.
|
|
58
59
|
- During `npx proteum dev`, the app exposes the read-only Proteum MCP runtime endpoint at `/__proteum/mcp`; use it for repeated agent reads instead of spawning equivalent diagnostics commands. For route/page/controller ownership, prefer MCP `workflow_start`, `route_candidates { projectId, query }`, or `explain_summary { projectId, query }` over broad `npx proteum explain --routes --controllers --full` dumps.
|
|
59
60
|
- For browser validation, use the browser MCP against the running app. Keep Playwright inside `npx proteum e2e --port <port>` for targeted/full end-to-end suites. Bootstrap protected browser MCP state with `npx proteum session`; bootstrap protected E2E runs with `npx proteum e2e --session-email <email> --session-role <role>`.
|
|
60
|
-
- Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the banner. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. Every `proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section before the dev loop begins.
|
|
61
|
+
- Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the welcome banner. Terminal `proteum mcp` may print a compact central MCP ready banner when it starts or reuses the managed daemon. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. Every `proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section before the dev loop begins.
|
|
61
62
|
|
|
62
63
|
### Before Finishing
|
|
63
64
|
|
|
64
65
|
- Before finishing, re-check touched files against root-level `CODING_STYLE.md` and any narrower area `AGENTS.md` that applied to the edit. Re-check against root-level `optimizations.md` only for touched client-side files. Re-check against root-level `diagnostics.md` only if the task involved an issue, diagnosis, runtime reproduction, or verification failure.
|
|
65
|
-
- For production changes, always add or update focused unit tests
|
|
66
|
+
- For production changes, always add or update focused unit tests and maintain 100% whole-project Vitest unit coverage. Before claiming implementation work complete, run `npx vitest run --coverage` from the repository root and make sure global statements, branches, functions, and lines all meet the configured 100% thresholds. MCP endpoint-reference coverage, touched-path-only coverage, and focused test passes are not substitutes for whole-project Vitest coverage. Document any generated files, migrations, framework shims, unreachable defensive branches, or changes that cannot reasonably be unit-tested as explicit exceptions, and do not claim the project has full unit coverage when an exception remains.
|
|
66
67
|
- Run the full project `npx proteum check` before finishing each feature or change. Targeted lint, typecheck, diagnose, request, browser, or E2E runs are still useful while iterating, but they do not replace the final full check. After implementing a new feature or changing existing feature behavior, always update the end-to-end coverage for that behavior and run the full Playwright test suite before finishing. For docs-only, wording-only, type-only, test-only, generated-output cleanup, or clearly local non-runtime refactors, skip Playwright unless the user explicitly asks for it or verification reveals a real issue.
|
|
67
68
|
- Before finishing a task, stop every `proteum dev` session started during the task and confirm cleanup with `npx proteum dev list --json` or an explicit `npx proteum dev stop --session-file <path>`.
|
|
68
69
|
- When you have finished your work, ask the user whether they want a commit message. After providing a commit message or after creating a commit, immediately follow it with this exact prompt and obey it:
|
|
@@ -9,7 +9,7 @@ Diagnostics source of truth: root-level `diagnostics.md`.
|
|
|
9
9
|
|
|
10
10
|
- Understand the real user flow and the main feature branches before writing tests.
|
|
11
11
|
- Test the current controller/page runtime model, not legacy `@Route` or `api.fetch(...)` behavior.
|
|
12
|
-
- For every production change, add or update focused unit tests
|
|
12
|
+
- For every production change, add or update focused unit tests and maintain 100% whole-project Vitest unit coverage. Before claiming implementation work complete, run `npx vitest run --coverage` from the repository root and make sure global statements, branches, functions, and lines all meet the configured 100% thresholds. MCP endpoint-reference coverage, touched-path-only coverage, and focused test passes are not substitutes for whole-project Vitest coverage. Document any generated files, migrations, framework shims, unreachable defensive branches, or changes that cannot reasonably be unit-tested as explicit exceptions, and do not claim the project has full unit coverage when an exception remains.
|
|
13
13
|
- Verify routing, controllers, SSR, and router plugins against a running app when behavior depends on real request handling.
|
|
14
14
|
- After implementing a new feature or changing existing feature behavior, update the end-to-end coverage for that behavior and run the full Playwright suite before finishing. Prefer `npx proteum e2e --port <port>` for Playwright runs so base URLs and auth tokens are passed through Proteum-managed child env instead of shell-leading environment assignments. Use a browser MCP repro against a running app during iteration when it is the fastest trustworthy loop.
|
|
15
15
|
- Exercise real URLs, generated controller calls, or real browser flows instead of re-deriving framework internals in tests.
|
package/cli/commands/mcp.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
resolveMachineMcpDaemonPort,
|
|
7
7
|
stopMachineMcpDaemonProcess,
|
|
8
8
|
} from '../runtime/mcpDaemon';
|
|
9
|
+
import { renderMcpDaemonBanner } from '../presentation/mcp';
|
|
9
10
|
|
|
10
11
|
const printJson = (payload: unknown) => {
|
|
11
12
|
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
@@ -53,11 +54,19 @@ const runDaemon = async () => {
|
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
console.info(
|
|
57
|
+
console.info(
|
|
58
|
+
await renderMcpDaemonBanner({
|
|
59
|
+
mcpUrl: existing.record.mcpUrl,
|
|
60
|
+
pid: existing.record.pid,
|
|
61
|
+
state: 'connected',
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
57
64
|
return;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
const port = resolveMachineMcpDaemonPort(typeof cli.args.port === 'string' ? cli.args.port : undefined);
|
|
68
|
+
const mcpUrl = `http://127.0.0.1:${port}/mcp`;
|
|
69
|
+
const healthUrl = `http://127.0.0.1:${port}/health`;
|
|
61
70
|
|
|
62
71
|
await startProteumMachineMcpRouterHttp({
|
|
63
72
|
port,
|
|
@@ -69,12 +78,18 @@ const runDaemon = async () => {
|
|
|
69
78
|
started: true,
|
|
70
79
|
daemon: {
|
|
71
80
|
pid: process.pid,
|
|
72
|
-
mcpUrl
|
|
73
|
-
healthUrl
|
|
81
|
+
mcpUrl,
|
|
82
|
+
healthUrl,
|
|
74
83
|
},
|
|
75
84
|
});
|
|
76
85
|
} else {
|
|
77
|
-
console.info(
|
|
86
|
+
console.info(
|
|
87
|
+
await renderMcpDaemonBanner({
|
|
88
|
+
mcpUrl,
|
|
89
|
+
pid: process.pid,
|
|
90
|
+
state: 'started',
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
78
93
|
}
|
|
79
94
|
};
|
|
80
95
|
|
|
@@ -91,9 +106,11 @@ const ensureDaemon = async () => {
|
|
|
91
106
|
|
|
92
107
|
if (result.inspection.record) {
|
|
93
108
|
console.info(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
:
|
|
109
|
+
await renderMcpDaemonBanner({
|
|
110
|
+
mcpUrl: result.inspection.record.mcpUrl,
|
|
111
|
+
pid: result.inspection.record.pid,
|
|
112
|
+
state: result.started ? 'started' : 'connected',
|
|
113
|
+
}),
|
|
97
114
|
);
|
|
98
115
|
}
|
|
99
116
|
};
|
|
@@ -459,16 +459,16 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
|
|
|
459
459
|
summary: 'Run one capped read-only database diagnostic query against a running Proteum dev server.',
|
|
460
460
|
usage: 'proteum db [query] <sql> [--limit <rows>] [--timeout <ms>] [--port <port>|--url <baseUrl>] [--full]',
|
|
461
461
|
bestFor:
|
|
462
|
-
'Inspecting live MySQL or
|
|
462
|
+
'Inspecting live MySQL, MariaDB, or PostgreSQL state during diagnosis without giving agents a write-capable SQL execution surface.',
|
|
463
463
|
examples: [
|
|
464
464
|
{ description: 'Run a small SELECT diagnostic', command: 'proteum db query "SELECT id, email FROM User LIMIT 5"' },
|
|
465
|
-
{ description: 'Inspect table metadata', command: 'proteum db "
|
|
465
|
+
{ description: 'Inspect table metadata', command: 'proteum db "SELECT table_name FROM information_schema.tables LIMIT 20"' },
|
|
466
466
|
{ description: 'Explain a query plan', command: 'proteum db query "EXPLAIN SELECT * FROM User WHERE id = 1"' },
|
|
467
467
|
],
|
|
468
468
|
notes: [
|
|
469
469
|
'Only SELECT, SHOW, and EXPLAIN statements are allowed.',
|
|
470
470
|
'The dev runtime executes the query with the app DATABASE_URL and returns rows, columns, elapsedMs, and cap metadata.',
|
|
471
|
-
'Multi-statement SQL, EXPLAIN ANALYZE, locking reads,
|
|
471
|
+
'Multi-statement SQL, EXPLAIN ANALYZE, locking reads, file-read functions, SELECT INTO OUTFILE, sleep, and benchmark functions are rejected.',
|
|
472
472
|
'Default output is compact `proteum-agent-v1` JSON with capped rows; use `--full` for the raw dev endpoint payload.',
|
|
473
473
|
],
|
|
474
474
|
status: 'experimental',
|
|
@@ -508,6 +508,7 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
|
|
|
508
508
|
],
|
|
509
509
|
notes: [
|
|
510
510
|
'`proteum dev` ensures one managed machine MCP daemon is running before the app dev loop starts.',
|
|
511
|
+
'Terminal `proteum mcp` prints a compact central MCP banner with the Streamable HTTP URL for client setup.',
|
|
511
512
|
'`proteum mcp` is a router, not an app dev server. It discovers live `proteum dev` sessions from the machine registry and can resolve offline app candidates from `cwd`.',
|
|
512
513
|
'Agents should call MCP `workflow_start` with `cwd` or a known `projectId`; use `project_resolve { cwd }` when routing is ambiguous or no live dev server exists yet.',
|
|
513
514
|
'When an offline app candidate is returned, start exactly one `proteum dev` from that app root before runtime diagnose, trace, or perf reads.',
|
package/cli/presentation/help.ts
CHANGED
|
@@ -135,7 +135,7 @@ export const renderCliOverview = async ({
|
|
|
135
135
|
indent: ' ',
|
|
136
136
|
nextIndent: ' ',
|
|
137
137
|
}),
|
|
138
|
-
wrapText('Only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and active Proteum installation method. Any extra argument or option skips the banner. `proteum dev` is the only command that clears the interactive terminal before rendering its session UI.', {
|
|
138
|
+
wrapText('Only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and active Proteum installation method. Any extra argument or option skips the welcome banner. `proteum mcp` may print a compact central MCP ready banner when it starts or reuses the daemon. `proteum dev` is the only command that clears the interactive terminal before rendering its session UI.', {
|
|
139
139
|
indent: ' ',
|
|
140
140
|
nextIndent: ' ',
|
|
141
141
|
}),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CliReact, renderInk } from './ink';
|
|
2
|
+
|
|
3
|
+
export type TMcpDaemonBannerState = 'started' | 'connected';
|
|
4
|
+
|
|
5
|
+
export const renderMcpDaemonBanner = async ({
|
|
6
|
+
mcpUrl,
|
|
7
|
+
pid,
|
|
8
|
+
state,
|
|
9
|
+
}: {
|
|
10
|
+
mcpUrl: string;
|
|
11
|
+
pid?: number;
|
|
12
|
+
state: TMcpDaemonBannerState;
|
|
13
|
+
}) =>
|
|
14
|
+
renderInk(({ Box, Text }) => {
|
|
15
|
+
const createElement = CliReact.createElement;
|
|
16
|
+
const summary =
|
|
17
|
+
state === 'started' ? 'Launched central MCP server.' : 'Connected to central MCP server.';
|
|
18
|
+
const pidLabel = pid ? ` pid ${pid}` : '';
|
|
19
|
+
|
|
20
|
+
return createElement(
|
|
21
|
+
Box,
|
|
22
|
+
{ borderStyle: 'round', borderColor: 'cyan', paddingX: 2, paddingY: 0, flexDirection: 'column' },
|
|
23
|
+
createElement(Text, { bold: true, backgroundColor: 'cyan', color: 'black' }, ' CENTRAL MCP READY '),
|
|
24
|
+
createElement(Text, { bold: true, color: 'cyan' }, `${summary}${pidLabel}`),
|
|
25
|
+
createElement(Text, { dimColor: true }, `Connect MCP client (HTTP): ${mcpUrl}`),
|
|
26
|
+
);
|
|
27
|
+
});
|
package/cli/utils/agents.ts
CHANGED
|
@@ -617,11 +617,12 @@ function renderEmbeddedProjectInstructions({ appRoot, coreRoot, includeMonorepoR
|
|
|
617
617
|
'5. Treat selected instruction previews returned by MCP as the instruction source for read-only discovery and diagnostics. Read full files only before edits or git writes, when the returned `fullRead`/`fullReadPolicy` requires it, or when the preview is insufficient.',
|
|
618
618
|
'6. Use `npx proteum runtime status` before starting a dev server only when MCP runtime status is unavailable, so an existing tracked session can be reused and the configured router/HMR ports can be checked without probing page bodies. If it says health is unreachable, do not run `diagnose`, `trace`, or `perf`; stop/repair/start the dev session first.',
|
|
619
619
|
'7. During `npx proteum dev`, Proteum ensures one managed machine MCP daemon is running and routes app-bound reads to the read-only runtime endpoint at `/__proteum/mcp` instead of spawning equivalent CLI diagnostics.',
|
|
620
|
-
'8.
|
|
621
|
-
'9. If
|
|
622
|
-
'10.
|
|
623
|
-
'11. Use
|
|
624
|
-
'12. Use
|
|
620
|
+
'8. Terminal `npx proteum mcp` prints a compact central MCP ready banner with the HTTP client URL when it starts or reuses the managed daemon.',
|
|
621
|
+
'9. If machine MCP routing fails, run `npx proteum mcp status` and `npx proteum runtime status`; if no live session exists, use the exact next action from MCP offline routing or runtime status instead of assuming the manifest default port. If the same app already responds on the configured port without live tracking, use or repair that runtime instead of starting another server.',
|
|
622
|
+
'10. If a live session exists but runtime/MCP is unreachable, stop the listed session file first, then start dev again. Do not start a second dev server in the same worktree or a second managed MCP daemon. Then retry MCP `workflow_start`.',
|
|
623
|
+
'11. Use MCP `diagnose { projectId, path }` for request-time issues before raw trace, perf, browser, or broad source search; use `npx proteum diagnose <target>` only as fallback or final terminal evidence.',
|
|
624
|
+
'12. Use `route_candidates`, `explain_summary`, or `npx proteum explain owner <query>` to pick routes. Do not run `npx proteum explain --routes --full` unless compact route/owner tools explicitly cannot answer the raw route-array question.',
|
|
625
|
+
'13. Use `--full`, `--manifest`, `--events`, or MCP `detail: "full"` only when compact output says the omitted detail is needed.',
|
|
625
626
|
'',
|
|
626
627
|
'CLI remains the reproducible surface for `dev`, `build`, `check`, `verify`, migrations, and final command evidence. MCP remains read-only and returns compact `proteum-mcp-v1` JSON.',
|
|
627
628
|
'',
|
|
@@ -650,6 +651,7 @@ function renderEmbeddedProjectInstructions({ appRoot, coreRoot, includeMonorepoR
|
|
|
650
651
|
'## Routing Table',
|
|
651
652
|
'',
|
|
652
653
|
'- Non-trivial coding tasks, feature docs, product intent, acceptance criteria, or docs updates: read `DOCUMENTATION.md`.',
|
|
654
|
+
'- GEO/SEO/crawler/structured-data/AI-source changes: read `DOCUMENTATION.md`, `CODING_STYLE.md`, `tests/AGENTS.md`, and update or create a docs page under `docs/` describing the public contract, routes, validation, and operational caveats.',
|
|
653
655
|
'- Raw errors, failing requests, traces, perf, or reproduction: read `diagnostics.md`.',
|
|
654
656
|
'- Implementation edits: read `CODING_STYLE.md` before editing.',
|
|
655
657
|
'- Client files or pages: read `client/AGENTS.md`; for page route/data/render work also read `client/pages/AGENTS.md`.',
|
package/common/dev/database.ts
CHANGED
|
@@ -199,15 +199,18 @@ const assertAllowedReadQueryShape = (sql: string) => {
|
|
|
199
199
|
throw new Error('SELECT INTO OUTFILE and SELECT INTO DUMPFILE are not allowed.');
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
if (/\
|
|
203
|
-
throw new Error('
|
|
202
|
+
if (/\b(?:load_file|pg_read_file|pg_read_binary_file|pg_ls_dir)\s*\(/.test(normalized)) {
|
|
203
|
+
throw new Error('Database file-read functions are not allowed in database diagnostics.');
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
if (
|
|
206
|
+
if (
|
|
207
|
+
/\bfor\s+(?:update|no\s+key\s+update|share|key\s+share)\b/.test(normalized) ||
|
|
208
|
+
/\block\s+in\s+share\s+mode\b/.test(normalized)
|
|
209
|
+
) {
|
|
207
210
|
throw new Error('Locking read statements are not allowed in database diagnostics.');
|
|
208
211
|
}
|
|
209
212
|
|
|
210
|
-
if (/\b(?:sleep|benchmark)\s*\(/.test(normalized)) {
|
|
213
|
+
if (/\b(?:sleep|benchmark|pg_sleep|pg_sleep_for|pg_sleep_until)\s*\(/.test(normalized)) {
|
|
211
214
|
throw new Error('Sleep and benchmark functions are not allowed in database diagnostics.');
|
|
212
215
|
}
|
|
213
216
|
};
|
package/docs/agent-routing.md
CHANGED
|
@@ -66,7 +66,7 @@ Use MCP for repeated reads when a client is available:
|
|
|
66
66
|
proteum mcp
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
The machine router discovers live `proteum dev` sessions and offline Proteum app roots under a cwd. `proteum dev` ensures one managed machine MCP daemon is running; terminal `proteum mcp` starts or reuses that daemon, while MCP clients can use stdio. Agents should call MCP `workflow_start` with `cwd` or a known `projectId`, use `project_resolve { cwd }` when routing is ambiguous or offline, and pass the returned live `projectId` to every follow-up app-bound MCP tool. Offline candidates include port-inspected next actions, so agents should follow those instead of guessing the manifest default port. The router forwards to the selected dev-hosted `/__proteum/mcp` endpoint and strips routing fields before the app sees the call.
|
|
69
|
+
The machine router discovers live `proteum dev` sessions and offline Proteum app roots under a cwd. `proteum dev` ensures one managed machine MCP daemon is running; terminal `proteum mcp` starts or reuses that daemon and prints a compact central MCP banner with the HTTP client URL, while MCP clients can use stdio. Agents should call MCP `workflow_start` with `cwd` or a known `projectId`, use `project_resolve { cwd }` when routing is ambiguous or offline, and pass the returned live `projectId` to every follow-up app-bound MCP tool. Offline candidates include port-inspected next actions, so agents should follow those instead of guessing the manifest default port. The router forwards to the selected dev-hosted `/__proteum/mcp` endpoint and strips routing fields before the app sees the call.
|
|
70
70
|
|
|
71
71
|
If machine MCP routing returns offline candidates, choose the intended app root and follow that candidate's next action from the app root, not from the monorepo wrapper. If machine MCP routing fails, run `proteum mcp status` and `proteum runtime status` from the intended app root; if no live session exists, use the exact Start Dev next action from runtime status so occupied router/HMR ports are avoided. If the same app already responds on the configured port without live tracking, use or repair that runtime instead of starting another server. Do not `curl` normal page routes to identify which app owns a port; use runtime status or Proteum dev-only endpoints. If a live session exists but runtime/MCP is unreachable, stop the listed session file first, then start dev again. Do not run diagnose, trace, or perf reads while runtime health is unreachable. Do not start a second dev server in the same worktree, and do not start a second managed MCP daemon. Then retry MCP `workflow_start`.
|
|
72
72
|
|
package/docs/mcp.md
CHANGED
|
@@ -15,7 +15,7 @@ Start the router from any directory:
|
|
|
15
15
|
proteum mcp
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
When run from a terminal, `proteum mcp` starts or reuses the managed local daemon at `http://127.0.0.1:3769/mcp`. When an MCP client launches it over pipes, use stdio:
|
|
18
|
+
When run from a terminal, `proteum mcp` starts or reuses the managed local daemon at `http://127.0.0.1:3769/mcp`. The terminal output prints a compact `CENTRAL MCP READY` banner with the one-line client setup instruction, `Connect MCP client (HTTP): <mcp-url>`. When an MCP client launches it over pipes, use stdio:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
proteum mcp --stdio
|
|
@@ -171,7 +171,7 @@ db_query { projectId, sql, limit? }
|
|
|
171
171
|
|
|
172
172
|
After an MCP read succeeds, do not run the equivalent CLI command for the same state, and do not run broad source searches for ownership that MCP already returned. CLI output is for fallback, validation, command evidence, and human-shareable reproductions.
|
|
173
173
|
|
|
174
|
-
Database diagnostics are intentionally read-only. `db_query` and `proteum db query` accept only one `SELECT`, `SHOW`, or `EXPLAIN` statement, return rows, columns, elapsed milliseconds, and cap metadata, and reject multi-statement SQL, `EXPLAIN ANALYZE`, locking reads, file reads/writes, sleep, and benchmark functions.
|
|
174
|
+
Database diagnostics are intentionally read-only. `db_query` and `proteum db query` support MySQL, MariaDB, PostgreSQL, and PostgreSQL-compatible `DATABASE_URL` protocols. They accept only one `SELECT`, `SHOW`, or `EXPLAIN` statement, return rows, columns, elapsed milliseconds, and cap metadata, and reject multi-statement SQL, `EXPLAIN ANALYZE`, locking reads, file reads/writes, sleep, and benchmark functions.
|
|
175
175
|
|
|
176
176
|
|
|
177
177
|
## Benchmark
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proteum",
|
|
3
3
|
"description": "LLM-first Opinionated Typescript Framework for web applications.",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.4",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"node-cmd": "^5.0.0",
|
|
77
77
|
"null-loader": "^4.0.1",
|
|
78
78
|
"path-to-regexp": "^6.2.0",
|
|
79
|
+
"pg": "^8.21.0",
|
|
79
80
|
"postcss-loader": "^8.2.0",
|
|
80
81
|
"preact": "^10.27.1",
|
|
81
82
|
"preact-render-to-string": "^6.6.1",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const coreRoot = path.resolve(__dirname, '../../../..');
|
|
5
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
6
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
7
|
+
require('ts-node/register/transpile-only');
|
|
8
|
+
|
|
9
|
+
const moduleAlias = require('module-alias');
|
|
10
|
+
moduleAlias.addAliases({
|
|
11
|
+
'@client': path.join(coreRoot, 'client'),
|
|
12
|
+
'@common': path.join(coreRoot, 'common'),
|
|
13
|
+
'@server': path.join(coreRoot, 'server'),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const { default: Console, getHttpClientErrorContext, normalizeBugReportError } = require('./index.ts');
|
|
17
|
+
|
|
18
|
+
test('wraps got HTTP errors as anomalies with original error context', () => {
|
|
19
|
+
const error = new Error('Response code 422 (Unprocessable Entity)');
|
|
20
|
+
error.name = 'HTTPError';
|
|
21
|
+
error.code = 'ERR_NON_2XX_3XX_RESPONSE';
|
|
22
|
+
error.timings = {
|
|
23
|
+
phases: {
|
|
24
|
+
total: 321,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
error.request = {
|
|
28
|
+
options: {
|
|
29
|
+
method: 'GET',
|
|
30
|
+
url: 'https://api.example.test/ip/2001:db8::1',
|
|
31
|
+
headers: {
|
|
32
|
+
'x-key': 'secret-token',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
error.response = {
|
|
37
|
+
statusCode: 422,
|
|
38
|
+
statusMessage: 'Unprocessable Entity',
|
|
39
|
+
headers: {
|
|
40
|
+
'set-cookie': 'session=secret',
|
|
41
|
+
},
|
|
42
|
+
body: {
|
|
43
|
+
message: 'Invalid IP address',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const wrapped = normalizeBugReportError(error);
|
|
48
|
+
|
|
49
|
+
assert.equal(wrapped.message, 'HTTP client request failed.');
|
|
50
|
+
assert.equal(wrapped.originalError, error);
|
|
51
|
+
assert.deepEqual(wrapped.dataForDebugging, {
|
|
52
|
+
code: 'ERR_NON_2XX_3XX_RESPONSE',
|
|
53
|
+
statusCode: 422,
|
|
54
|
+
statusMessage: 'Unprocessable Entity',
|
|
55
|
+
method: 'GET',
|
|
56
|
+
url: 'https://api.example.test/ip/2001:db8::1',
|
|
57
|
+
timings: {
|
|
58
|
+
phases: {
|
|
59
|
+
total: 321,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
request: {
|
|
63
|
+
options: {
|
|
64
|
+
method: 'GET',
|
|
65
|
+
url: 'https://api.example.test/ip/2001:db8::1',
|
|
66
|
+
headers: {
|
|
67
|
+
'x-key': 'secret-token',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
response: {
|
|
72
|
+
statusCode: 422,
|
|
73
|
+
statusMessage: 'Unprocessable Entity',
|
|
74
|
+
headers: {
|
|
75
|
+
'set-cookie': 'session=secret',
|
|
76
|
+
},
|
|
77
|
+
body: {
|
|
78
|
+
message: 'Invalid IP address',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
options: null,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('ignores normal application errors', () => {
|
|
86
|
+
assert.equal(getHttpClientErrorContext(new Error('Something else failed')), null);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('renders circular JSON contexts in bug report HTML', () => {
|
|
90
|
+
const context = { name: 'request' };
|
|
91
|
+
context.self = context;
|
|
92
|
+
|
|
93
|
+
const html = Console.prototype.jsonToHTML.call({ printHtml: (value) => value }, context);
|
|
94
|
+
|
|
95
|
+
assert.match(html, /Circular/);
|
|
96
|
+
});
|
|
@@ -7,6 +7,7 @@ import { serialize } from 'v8';
|
|
|
7
7
|
import { formatWithOptions } from 'util';
|
|
8
8
|
import md5 from 'md5';
|
|
9
9
|
import dayjs from 'dayjs';
|
|
10
|
+
import stringify from 'fast-safe-stringify';
|
|
10
11
|
|
|
11
12
|
// Npm
|
|
12
13
|
import { Logger, IMeta, ILogObj, ISettings } from 'tslog';
|
|
@@ -17,7 +18,7 @@ import Ansi2Html from 'ansi-to-html';
|
|
|
17
18
|
import type ApplicationContainer from '..';
|
|
18
19
|
import context from '@server/context';
|
|
19
20
|
import type { TDevConsoleLogChannel, TDevConsoleLogEntry, TDevConsoleLogLevel } from '@common/dev/console';
|
|
20
|
-
import type
|
|
21
|
+
import { Anomaly, type ServerBug, type TCatchedError } from '@common/errors';
|
|
21
22
|
import type { TTraceCallOrigin, TTraceSqlQueryKind } from '@common/dev/requestTrace';
|
|
22
23
|
import type ServerRequest from '@server/services/router/request';
|
|
23
24
|
|
|
@@ -74,6 +75,13 @@ export type TDbQueryLog = ChannelInfos & { date: Date; query: string; time: numb
|
|
|
74
75
|
export type TLogLevel = keyof typeof logLevels;
|
|
75
76
|
|
|
76
77
|
export type TJsonLog = { time: Date; level: TLogLevel; args: unknown[]; channel: ChannelInfos };
|
|
78
|
+
type TGotLikeError = Error & {
|
|
79
|
+
code?: unknown;
|
|
80
|
+
timings?: unknown;
|
|
81
|
+
request?: unknown;
|
|
82
|
+
response?: unknown;
|
|
83
|
+
options?: unknown;
|
|
84
|
+
};
|
|
77
85
|
|
|
78
86
|
/*----------------------------------
|
|
79
87
|
- CONST
|
|
@@ -111,6 +119,57 @@ var ansi2Html = new Ansi2Html({
|
|
|
111
119
|
|
|
112
120
|
type TWrappedConsole = typeof console & { _wrapped?: boolean };
|
|
113
121
|
|
|
122
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
|
123
|
+
return typeof value === 'object' && value !== null;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const firstString = (...values: Array<unknown>): string | null => {
|
|
127
|
+
for (const value of values) {
|
|
128
|
+
if (typeof value === 'string' && value.trim()) return value;
|
|
129
|
+
if (value instanceof URL) return value.toString();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const getHttpClientErrorContext = (error: Error): object | null => {
|
|
136
|
+
const gotError = error as TGotLikeError;
|
|
137
|
+
const errorRecord = gotError as unknown as Record<string, unknown>;
|
|
138
|
+
const code = typeof gotError.code === 'string' ? gotError.code : null;
|
|
139
|
+
const response = isRecord(gotError.response) ? gotError.response : null;
|
|
140
|
+
const request = isRecord(gotError.request) ? gotError.request : null;
|
|
141
|
+
const requestOptions = request && isRecord(request.options) ? request.options : null;
|
|
142
|
+
const fallbackOptions = isRecord(gotError.options) ? gotError.options : null;
|
|
143
|
+
const options = requestOptions || fallbackOptions;
|
|
144
|
+
|
|
145
|
+
if (error.name !== 'HTTPError' && code !== 'ERR_NON_2XX_3XX_RESPONSE' && !response) return null;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
code,
|
|
149
|
+
statusCode: response && typeof response.statusCode === 'number' ? response.statusCode : null,
|
|
150
|
+
statusMessage: response && typeof response.statusMessage === 'string' ? response.statusMessage : null,
|
|
151
|
+
method: options && typeof options.method === 'string' ? options.method : null,
|
|
152
|
+
url: firstString(
|
|
153
|
+
response ? response.requestUrl : null,
|
|
154
|
+
response ? response.url : null,
|
|
155
|
+
request ? request.requestUrl : null,
|
|
156
|
+
options ? options.url : null,
|
|
157
|
+
errorRecord.url,
|
|
158
|
+
),
|
|
159
|
+
timings: gotError.timings || null,
|
|
160
|
+
request: request || null,
|
|
161
|
+
response: response || null,
|
|
162
|
+
options: fallbackOptions,
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const normalizeBugReportError = (error: TCatchedError): TCatchedError => {
|
|
167
|
+
const httpClientErrorContext = getHttpClientErrorContext(error);
|
|
168
|
+
if (!httpClientErrorContext) return error;
|
|
169
|
+
|
|
170
|
+
return new Anomaly('HTTP client request failed.', httpClientErrorContext, error);
|
|
171
|
+
};
|
|
172
|
+
|
|
114
173
|
/*----------------------------------
|
|
115
174
|
- LOGGER
|
|
116
175
|
----------------------------------*/
|
|
@@ -307,7 +366,7 @@ export default class Console {
|
|
|
307
366
|
// On envoi l'email avant l'insertion dans bla bdd
|
|
308
367
|
// Car cette denrière a plus de chances de provoquer une erreur
|
|
309
368
|
//const logs = this.logs.filter(e => e.channel.channelId === channelId).slice(-100);
|
|
310
|
-
const inspection = this.getDetailledError(error);
|
|
369
|
+
const inspection = this.getDetailledError(normalizeBugReportError(error));
|
|
311
370
|
|
|
312
371
|
// Genertae unique error hash
|
|
313
372
|
const hash = md5(inspection.stacktraces[0]);
|
|
@@ -480,7 +539,7 @@ Logs: ${
|
|
|
480
539
|
public jsonToHTML(json: unknown): string {
|
|
481
540
|
if (!json) return 'No data';
|
|
482
541
|
|
|
483
|
-
const coloredJson = highlight(
|
|
542
|
+
const coloredJson = highlight(stringify(json, null, 4), { language: 'json', ignoreIllegals: true });
|
|
484
543
|
|
|
485
544
|
const html = ansi2Html.toHtml(coloredJson);
|
|
486
545
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import mysql from 'mysql2/promise';
|
|
2
|
+
import { Client as PostgresClient } from 'pg';
|
|
3
|
+
import { performance } from 'perf_hooks';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
TDatabaseReadQueryColumn,
|
|
7
|
+
TDatabaseReadQueryResponse,
|
|
8
|
+
TDatabaseReadQueryRow,
|
|
9
|
+
TDatabaseReadQueryValue,
|
|
10
|
+
} from '@common/dev/database';
|
|
11
|
+
import { parseMariaDbDatabaseUrl } from '@server/services/prisma/mariadb';
|
|
12
|
+
|
|
13
|
+
type TDatabaseProtocol = 'mariadb' | 'postgresql';
|
|
14
|
+
|
|
15
|
+
const normalizeDatabaseValue = (value: unknown): TDatabaseReadQueryValue => {
|
|
16
|
+
if (value === null || value === undefined) return null;
|
|
17
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
|
|
18
|
+
if (typeof value === 'bigint') return value.toString();
|
|
19
|
+
if (value instanceof Date) return value.toISOString();
|
|
20
|
+
if (Buffer.isBuffer(value)) return `[Buffer ${value.byteLength} bytes]`;
|
|
21
|
+
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const normalizeDatabaseRow = (row: unknown): TDatabaseReadQueryRow => {
|
|
26
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) return {};
|
|
27
|
+
|
|
28
|
+
return Object.fromEntries(
|
|
29
|
+
Object.entries(row).map(([key, value]) => [key, normalizeDatabaseValue(value)]),
|
|
30
|
+
) as TDatabaseReadQueryRow;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const databaseProtocolFromUrl = (databaseUrl: string): TDatabaseProtocol => {
|
|
34
|
+
const { protocol } = new URL(databaseUrl);
|
|
35
|
+
|
|
36
|
+
if (protocol === 'mysql:' || protocol === 'mariadb:') return 'mariadb';
|
|
37
|
+
if (protocol === 'postgres:' || protocol === 'postgresql:') return 'postgresql';
|
|
38
|
+
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Unsupported DATABASE_URL protocol "${protocol}". Proteum database diagnostics support mysql://, mariadb://, postgres://, and postgresql://.`,
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const columnsFromMariaDbFields = (fields: unknown): TDatabaseReadQueryColumn[] =>
|
|
45
|
+
Array.isArray(fields)
|
|
46
|
+
? fields.map((field) => {
|
|
47
|
+
const candidate = field as { name?: unknown; table?: unknown; type?: unknown };
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
name: typeof candidate.name === 'string' ? candidate.name : '',
|
|
51
|
+
...(typeof candidate.table === 'string' && candidate.table ? { table: candidate.table } : {}),
|
|
52
|
+
...(typeof candidate.type === 'number' || typeof candidate.type === 'string' ? { type: candidate.type } : {}),
|
|
53
|
+
};
|
|
54
|
+
})
|
|
55
|
+
: [];
|
|
56
|
+
|
|
57
|
+
const readMariaDb = async ({
|
|
58
|
+
databaseUrl,
|
|
59
|
+
limit,
|
|
60
|
+
sql,
|
|
61
|
+
timeoutMs,
|
|
62
|
+
}: {
|
|
63
|
+
databaseUrl: string;
|
|
64
|
+
limit: number;
|
|
65
|
+
sql: string;
|
|
66
|
+
timeoutMs: number;
|
|
67
|
+
}) => {
|
|
68
|
+
const connectionConfig = parseMariaDbDatabaseUrl(databaseUrl);
|
|
69
|
+
const connection = await mysql.createConnection({
|
|
70
|
+
host: connectionConfig.host,
|
|
71
|
+
port: connectionConfig.port,
|
|
72
|
+
user: connectionConfig.user,
|
|
73
|
+
password: connectionConfig.password,
|
|
74
|
+
database: connectionConfig.database,
|
|
75
|
+
connectTimeout: connectionConfig.connectTimeout,
|
|
76
|
+
multipleStatements: false,
|
|
77
|
+
supportBigNumbers: true,
|
|
78
|
+
bigNumberStrings: true,
|
|
79
|
+
dateStrings: true,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await connection.query('START TRANSACTION READ ONLY');
|
|
84
|
+
const [rows, fields] = await connection.query({ sql, timeout: timeoutMs });
|
|
85
|
+
await connection.rollback();
|
|
86
|
+
|
|
87
|
+
const rowList = Array.isArray(rows) ? rows : [];
|
|
88
|
+
const normalizedRows = rowList.map(normalizeDatabaseRow);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
columns: columnsFromMariaDbFields(fields),
|
|
92
|
+
limited: normalizedRows.length > limit,
|
|
93
|
+
rowCount: normalizedRows.length,
|
|
94
|
+
rows: normalizedRows.slice(0, limit),
|
|
95
|
+
};
|
|
96
|
+
} catch (error) {
|
|
97
|
+
try {
|
|
98
|
+
await connection.rollback();
|
|
99
|
+
} catch (_rollbackError) {}
|
|
100
|
+
|
|
101
|
+
throw error;
|
|
102
|
+
} finally {
|
|
103
|
+
await connection.end();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const readPostgreSql = async ({
|
|
108
|
+
databaseUrl,
|
|
109
|
+
limit,
|
|
110
|
+
sql,
|
|
111
|
+
timeoutMs,
|
|
112
|
+
}: {
|
|
113
|
+
databaseUrl: string;
|
|
114
|
+
limit: number;
|
|
115
|
+
sql: string;
|
|
116
|
+
timeoutMs: number;
|
|
117
|
+
}) => {
|
|
118
|
+
const client = new PostgresClient({
|
|
119
|
+
connectionString: databaseUrl,
|
|
120
|
+
statement_timeout: timeoutMs,
|
|
121
|
+
query_timeout: timeoutMs,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await client.connect();
|
|
126
|
+
await client.query('BEGIN READ ONLY');
|
|
127
|
+
await client.query('SET TRANSACTION READ ONLY');
|
|
128
|
+
const result = await client.query(sql);
|
|
129
|
+
await client.query('ROLLBACK');
|
|
130
|
+
|
|
131
|
+
const normalizedRows = result.rows.map(normalizeDatabaseRow);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
columns: result.fields.map((field) => ({
|
|
135
|
+
name: field.name,
|
|
136
|
+
...(field.tableID ? { table: String(field.tableID) } : {}),
|
|
137
|
+
type: field.dataTypeID,
|
|
138
|
+
})),
|
|
139
|
+
limited: normalizedRows.length > limit,
|
|
140
|
+
rowCount: normalizedRows.length,
|
|
141
|
+
rows: normalizedRows.slice(0, limit),
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
try {
|
|
145
|
+
await client.query('ROLLBACK');
|
|
146
|
+
} catch (_rollbackError) {}
|
|
147
|
+
|
|
148
|
+
throw error;
|
|
149
|
+
} finally {
|
|
150
|
+
try {
|
|
151
|
+
await client.end();
|
|
152
|
+
} catch (_endError) {}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const runDatabaseReadQuery = async ({
|
|
157
|
+
databaseUrl,
|
|
158
|
+
kind,
|
|
159
|
+
limit,
|
|
160
|
+
sql,
|
|
161
|
+
timeoutMs,
|
|
162
|
+
}: {
|
|
163
|
+
databaseUrl: string;
|
|
164
|
+
kind: TDatabaseReadQueryResponse['kind'];
|
|
165
|
+
limit: number;
|
|
166
|
+
sql: string;
|
|
167
|
+
timeoutMs: number;
|
|
168
|
+
}): Promise<TDatabaseReadQueryResponse> => {
|
|
169
|
+
const startedAt = performance.now();
|
|
170
|
+
const response =
|
|
171
|
+
databaseProtocolFromUrl(databaseUrl) === 'postgresql'
|
|
172
|
+
? await readPostgreSql({ databaseUrl, limit, sql, timeoutMs })
|
|
173
|
+
: await readMariaDb({ databaseUrl, limit, sql, timeoutMs });
|
|
174
|
+
const elapsedMs = Math.max(0, Math.round(performance.now() - startedAt));
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
...response,
|
|
178
|
+
elapsedMs,
|
|
179
|
+
kind,
|
|
180
|
+
limit,
|
|
181
|
+
sql,
|
|
182
|
+
};
|
|
183
|
+
};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
-
import mysql from 'mysql2/promise';
|
|
3
2
|
import path from 'path';
|
|
4
|
-
import { performance } from 'perf_hooks';
|
|
5
3
|
|
|
6
4
|
import type { Application } from './index';
|
|
5
|
+
import { runDatabaseReadQuery } from './devDatabase';
|
|
7
6
|
import type { TDevConsoleLogLevel, TDevConsoleLogsResponse } from '@common/dev/console';
|
|
8
7
|
import {
|
|
9
8
|
normalizeDatabaseReadLimit,
|
|
@@ -11,8 +10,6 @@ import {
|
|
|
11
10
|
validateDatabaseReadQuery,
|
|
12
11
|
type TDatabaseReadQueryInput,
|
|
13
12
|
type TDatabaseReadQueryResponse,
|
|
14
|
-
type TDatabaseReadQueryRow,
|
|
15
|
-
type TDatabaseReadQueryValue,
|
|
16
13
|
} from '@common/dev/database';
|
|
17
14
|
import {
|
|
18
15
|
buildDoctorResponse,
|
|
@@ -41,31 +38,12 @@ import {
|
|
|
41
38
|
} from '@common/dev/inspection';
|
|
42
39
|
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
43
40
|
import type { TRequestTrace } from '@common/dev/requestTrace';
|
|
44
|
-
import { parseMariaDbDatabaseUrl } from '@server/services/prisma/mariadb';
|
|
45
41
|
|
|
46
42
|
const isExplainSectionName = (value: string): value is TExplainSectionName =>
|
|
47
43
|
explainSectionNames.includes(value as TExplainSectionName);
|
|
48
44
|
const isConsoleLogLevel = (value: string): value is TDevConsoleLogLevel =>
|
|
49
45
|
['silly', 'log', 'info', 'warn', 'error'].includes(value);
|
|
50
46
|
|
|
51
|
-
const normalizeDatabaseValue = (value: unknown): TDatabaseReadQueryValue => {
|
|
52
|
-
if (value === null || value === undefined) return null;
|
|
53
|
-
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
|
|
54
|
-
if (typeof value === 'bigint') return value.toString();
|
|
55
|
-
if (value instanceof Date) return value.toISOString();
|
|
56
|
-
if (Buffer.isBuffer(value)) return `[Buffer ${value.byteLength} bytes]`;
|
|
57
|
-
|
|
58
|
-
return JSON.stringify(value);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const normalizeDatabaseRow = (row: unknown): TDatabaseReadQueryRow => {
|
|
62
|
-
if (!row || typeof row !== 'object' || Array.isArray(row)) return {};
|
|
63
|
-
|
|
64
|
-
return Object.fromEntries(
|
|
65
|
-
Object.entries(row).map(([key, value]) => [key, normalizeDatabaseValue(value)]),
|
|
66
|
-
) as TDatabaseReadQueryRow;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
47
|
export default class DevDiagnosticsRegistry {
|
|
70
48
|
public constructor(private app: Application) {}
|
|
71
49
|
|
|
@@ -129,55 +107,8 @@ export default class DevDiagnosticsRegistry {
|
|
|
129
107
|
const { kind, sql } = validateDatabaseReadQuery(rawSql);
|
|
130
108
|
const limit = normalizeDatabaseReadLimit(rawLimit);
|
|
131
109
|
const timeoutMs = normalizeDatabaseReadTimeoutMs(rawTimeoutMs);
|
|
132
|
-
const connectionConfig = parseMariaDbDatabaseUrl(databaseUrl);
|
|
133
|
-
const connection = await mysql.createConnection({
|
|
134
|
-
host: connectionConfig.host,
|
|
135
|
-
port: connectionConfig.port,
|
|
136
|
-
user: connectionConfig.user,
|
|
137
|
-
password: connectionConfig.password,
|
|
138
|
-
database: connectionConfig.database,
|
|
139
|
-
connectTimeout: connectionConfig.connectTimeout,
|
|
140
|
-
multipleStatements: false,
|
|
141
|
-
supportBigNumbers: true,
|
|
142
|
-
bigNumberStrings: true,
|
|
143
|
-
dateStrings: true,
|
|
144
|
-
});
|
|
145
|
-
const startedAt = performance.now();
|
|
146
110
|
|
|
147
|
-
|
|
148
|
-
await connection.query('START TRANSACTION READ ONLY');
|
|
149
|
-
const [rows, fields] = await connection.query({ sql, timeout: timeoutMs });
|
|
150
|
-
await connection.rollback();
|
|
151
|
-
|
|
152
|
-
const rowList = Array.isArray(rows) ? rows : [];
|
|
153
|
-
const normalizedRows = rowList.map(normalizeDatabaseRow);
|
|
154
|
-
const elapsedMs = Math.max(0, Math.round(performance.now() - startedAt));
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
columns: Array.isArray(fields)
|
|
158
|
-
? fields.map((field) => ({
|
|
159
|
-
name: field.name,
|
|
160
|
-
table: field.table || undefined,
|
|
161
|
-
type: field.type,
|
|
162
|
-
}))
|
|
163
|
-
: [],
|
|
164
|
-
elapsedMs,
|
|
165
|
-
kind,
|
|
166
|
-
limit,
|
|
167
|
-
limited: normalizedRows.length > limit,
|
|
168
|
-
rowCount: normalizedRows.length,
|
|
169
|
-
rows: normalizedRows.slice(0, limit),
|
|
170
|
-
sql,
|
|
171
|
-
};
|
|
172
|
-
} catch (error) {
|
|
173
|
-
try {
|
|
174
|
-
await connection.rollback();
|
|
175
|
-
} catch (_rollbackError) {}
|
|
176
|
-
|
|
177
|
-
throw error;
|
|
178
|
-
} finally {
|
|
179
|
-
await connection.end();
|
|
180
|
-
}
|
|
111
|
+
return runDatabaseReadQuery({ databaseUrl, kind, limit, sql, timeoutMs });
|
|
181
112
|
}
|
|
182
113
|
|
|
183
114
|
private resolveRequestTrace({ path, requestId }: { path?: string; requestId?: string }): TRequestTrace | undefined {
|
|
@@ -102,12 +102,14 @@ test('standalone configure creates tracked instruction files with routing contra
|
|
|
102
102
|
assert.match(agentsContent, /Read full files only before edits or git writes/);
|
|
103
103
|
assert.match(agentsContent, /explain_summary/);
|
|
104
104
|
assert.match(agentsContent, /\/__proteum\/mcp/);
|
|
105
|
+
assert.match(agentsContent, /central MCP ready banner/);
|
|
105
106
|
assert.match(agentsContent, /proteum-mcp-v1/);
|
|
106
107
|
assert.match(agentsContent, /## Triggered Instruction Reads/);
|
|
107
108
|
assert.match(agentsContent, /Git lifecycle/);
|
|
108
109
|
assert.match(agentsContent, /read Root contract fallback before any git write/);
|
|
109
110
|
assert.match(agentsContent, /add or update focused unit tests/);
|
|
110
111
|
assert.match(agentsContent, /read Root contract fallback, `CODING_STYLE\.md`, `tests\/AGENTS\.md`/);
|
|
112
|
+
assert.match(agentsContent, /GEO\/SEO\/crawler\/structured-data\/AI-source changes/);
|
|
111
113
|
assert.match(agentsContent, /MCP-selected previews are enough/);
|
|
112
114
|
assert.doesNotMatch(agentsContent, /Conventional Commits/);
|
|
113
115
|
assert.match(agentsContent, /They are not deleted/);
|
|
@@ -85,6 +85,63 @@ const runCli = async (args, { cwd }) =>
|
|
|
85
85
|
child.once('close', (status) => resolve({ status, stdout, stderr }));
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
const waitForChildOutput = async (child, predicate, timeoutMs = 10000) =>
|
|
89
|
+
await new Promise((resolve, reject) => {
|
|
90
|
+
let output = '';
|
|
91
|
+
let settled = false;
|
|
92
|
+
let timer;
|
|
93
|
+
const cleanup = () => {
|
|
94
|
+
if (timer) clearTimeout(timer);
|
|
95
|
+
child.stdout.off('data', handleData);
|
|
96
|
+
child.stderr.off('data', handleData);
|
|
97
|
+
child.off('close', handleClose);
|
|
98
|
+
};
|
|
99
|
+
const settle = (callback) => {
|
|
100
|
+
if (settled) return;
|
|
101
|
+
settled = true;
|
|
102
|
+
cleanup();
|
|
103
|
+
callback();
|
|
104
|
+
};
|
|
105
|
+
const handleData = (chunk) => {
|
|
106
|
+
output += chunk.toString();
|
|
107
|
+
if (predicate(output)) settle(() => resolve(output));
|
|
108
|
+
};
|
|
109
|
+
const handleClose = (status, signal) => {
|
|
110
|
+
settle(() => reject(new Error(`Child exited before expected output. status=${status} signal=${signal}\n${output}`)));
|
|
111
|
+
};
|
|
112
|
+
timer = setTimeout(() => {
|
|
113
|
+
child.kill('SIGTERM');
|
|
114
|
+
settle(() => reject(new Error(`Timed out waiting for expected output.\n${output}`)));
|
|
115
|
+
}, timeoutMs);
|
|
116
|
+
|
|
117
|
+
child.stdout.on('data', handleData);
|
|
118
|
+
child.stderr.on('data', handleData);
|
|
119
|
+
child.once('close', handleClose);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const writeLiveDaemonRecord = (registryDir, { port }) => {
|
|
123
|
+
const timestamp = new Date().toISOString();
|
|
124
|
+
|
|
125
|
+
writeFile(
|
|
126
|
+
path.join(registryDir, 'router.json'),
|
|
127
|
+
JSON.stringify(
|
|
128
|
+
{
|
|
129
|
+
version: 1,
|
|
130
|
+
pid: process.pid,
|
|
131
|
+
port,
|
|
132
|
+
host: '127.0.0.1',
|
|
133
|
+
mcpUrl: `http://127.0.0.1:${port}/mcp`,
|
|
134
|
+
healthUrl: `http://127.0.0.1:${port}/health`,
|
|
135
|
+
startedAt: timestamp,
|
|
136
|
+
updatedAt: timestamp,
|
|
137
|
+
command: [process.execPath, cliBin, 'mcp', '--daemon'],
|
|
138
|
+
},
|
|
139
|
+
null,
|
|
140
|
+
2,
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
88
145
|
test('top-level help lists the machine-scope mcp router', () => {
|
|
89
146
|
const result = spawnSync(process.execPath, [cliBin, '--help'], {
|
|
90
147
|
cwd: coreRoot,
|
|
@@ -110,6 +167,54 @@ test('mcp help describes projectId routing', () => {
|
|
|
110
167
|
assert.match(output, /--stdio/);
|
|
111
168
|
});
|
|
112
169
|
|
|
170
|
+
test('mcp daemon launch prints a central MCP connection banner', async () => {
|
|
171
|
+
const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-daemon-launch-'));
|
|
172
|
+
const reserveServer = http.createServer((req, res) => res.end('reserved'));
|
|
173
|
+
const port = await listen(reserveServer);
|
|
174
|
+
await closeServer(reserveServer);
|
|
175
|
+
const child = spawn(process.execPath, [cliBin, 'mcp', '--daemon', '--port', String(port)], {
|
|
176
|
+
cwd: coreRoot,
|
|
177
|
+
env: { ...process.env, PROTEUM_MACHINE_MCP_DIR: registryDir },
|
|
178
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
179
|
+
});
|
|
180
|
+
let closed = false;
|
|
181
|
+
child.once('close', () => {
|
|
182
|
+
closed = true;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const output = await waitForChildOutput(child, (value) =>
|
|
187
|
+
value.includes(`Connect MCP client (HTTP): http://127.0.0.1:${port}/mcp`),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
assert.match(output, /CENTRAL MCP READY/);
|
|
191
|
+
assert.match(output, /Launched central MCP server/);
|
|
192
|
+
} finally {
|
|
193
|
+
if (!closed) {
|
|
194
|
+
child.kill('SIGTERM');
|
|
195
|
+
await new Promise((resolve) => child.once('close', resolve));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('mcp daemon reuse prints a central MCP connection banner', () => {
|
|
201
|
+
const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-daemon-reuse-'));
|
|
202
|
+
const port = 37977;
|
|
203
|
+
writeLiveDaemonRecord(registryDir, { port });
|
|
204
|
+
|
|
205
|
+
const result = spawnSync(process.execPath, [cliBin, 'mcp', '--daemon', '--port', String(port)], {
|
|
206
|
+
cwd: coreRoot,
|
|
207
|
+
encoding: 'utf8',
|
|
208
|
+
env: { ...process.env, PROTEUM_MACHINE_MCP_DIR: registryDir },
|
|
209
|
+
});
|
|
210
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
211
|
+
|
|
212
|
+
assert.equal(result.status, 0);
|
|
213
|
+
assert.match(output, /CENTRAL MCP READY/);
|
|
214
|
+
assert.match(output, /Connected to central MCP server/);
|
|
215
|
+
assert.match(output, new RegExp(`Connect MCP client \\(HTTP\\): http://127\\.0\\.0\\.1:${port}/mcp`));
|
|
216
|
+
});
|
|
217
|
+
|
|
113
218
|
test('db help describes read-only SQL diagnostics', () => {
|
|
114
219
|
const result = spawnSync(process.execPath, [cliBin, 'db', '--help'], {
|
|
115
220
|
cwd: coreRoot,
|
package/tests/mcp.test.cjs
CHANGED
|
@@ -25,6 +25,7 @@ const {
|
|
|
25
25
|
normalizeDatabaseReadLimit,
|
|
26
26
|
validateDatabaseReadQuery,
|
|
27
27
|
} = require('../common/dev/database.ts');
|
|
28
|
+
const { databaseProtocolFromUrl } = require('../server/app/devDatabase.ts');
|
|
28
29
|
const { createProteumMachineMcpServer } = require('../cli/mcp/router.ts');
|
|
29
30
|
const {
|
|
30
31
|
createDevSessionRecord,
|
|
@@ -142,7 +143,18 @@ test('database read query policy allows only capped SELECT SHOW and EXPLAIN diag
|
|
|
142
143
|
assert.throws(() => validateDatabaseReadQuery('UPDATE User SET role = "admin"'), /Only SELECT, SHOW, and EXPLAIN/);
|
|
143
144
|
assert.throws(() => validateDatabaseReadQuery('SELECT 1; DROP TABLE User'), /Only one read-only SQL statement/);
|
|
144
145
|
assert.throws(() => validateDatabaseReadQuery('EXPLAIN ANALYZE SELECT * FROM User'), /EXPLAIN ANALYZE/);
|
|
145
|
-
assert.throws(() => validateDatabaseReadQuery('SELECT LOAD_FILE("/etc/passwd")'), /
|
|
146
|
+
assert.throws(() => validateDatabaseReadQuery('SELECT LOAD_FILE("/etc/passwd")'), /file-read/);
|
|
147
|
+
assert.throws(() => validateDatabaseReadQuery("SELECT pg_read_file('/etc/passwd')"), /file-read/);
|
|
148
|
+
assert.throws(() => validateDatabaseReadQuery('SELECT * FROM "User" FOR SHARE'), /Locking read/);
|
|
149
|
+
assert.throws(() => validateDatabaseReadQuery('SELECT pg_sleep(1)'), /Sleep and benchmark/);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('database diagnostics support MySQL MariaDB and PostgreSQL URLs', () => {
|
|
153
|
+
assert.equal(databaseProtocolFromUrl('mysql://user:pass@localhost:3306/app'), 'mariadb');
|
|
154
|
+
assert.equal(databaseProtocolFromUrl('mariadb://user:pass@localhost:3306/app'), 'mariadb');
|
|
155
|
+
assert.equal(databaseProtocolFromUrl('postgres://user:pass@localhost:5432/app'), 'postgresql');
|
|
156
|
+
assert.equal(databaseProtocolFromUrl('postgresql://user:pass@localhost:5432/app'), 'postgresql');
|
|
157
|
+
assert.throws(() => databaseProtocolFromUrl('sqlite://local.db'), /postgresql/);
|
|
146
158
|
});
|
|
147
159
|
|
|
148
160
|
test('instruction routing promotes triggered full instruction files', () => {
|