proteum 2.3.0 → 2.4.2
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 +8 -3
- package/README.md +20 -15
- package/agents/project/AGENTS.md +16 -10
- package/agents/project/DOCUMENTATION.md +1326 -0
- package/agents/project/app-root/AGENTS.md +2 -2
- package/agents/project/diagnostics.md +10 -9
- package/agents/project/optimizations.md +1 -1
- package/agents/project/root/AGENTS.md +15 -8
- package/agents/project/server/services/AGENTS.md +1 -0
- package/agents/project/tests/AGENTS.md +1 -0
- package/cli/commands/db.ts +160 -0
- package/cli/commands/dev.ts +148 -25
- package/cli/commands/diagnose.ts +2 -0
- package/cli/commands/explain.ts +38 -9
- package/cli/commands/mcp.ts +126 -9
- package/cli/commands/orient.ts +44 -17
- package/cli/commands/runtime.ts +100 -17
- package/cli/mcp/router.ts +1028 -0
- package/cli/presentation/commands.ts +56 -25
- package/cli/presentation/help.ts +1 -1
- package/cli/runtime/commands.ts +163 -21
- package/cli/runtime/devSessions.ts +328 -2
- package/cli/runtime/mcpDaemon.ts +288 -0
- package/cli/runtime/ports.ts +151 -0
- package/cli/utils/agents.ts +94 -17
- package/cli/utils/appRoots.ts +232 -0
- package/common/dev/database.ts +226 -0
- package/common/dev/diagnostics.ts +1 -1
- package/common/dev/inspection.ts +8 -1
- package/common/dev/mcpPayloads.ts +456 -17
- package/common/dev/mcpServer.ts +51 -0
- package/docs/agent-routing.md +32 -21
- package/docs/dev-commands.md +1 -1
- package/docs/dev-sessions.md +3 -1
- package/docs/diagnostics.md +21 -20
- package/docs/mcp.md +114 -50
- package/docs/migrate-from-2.1.3.md +3 -5
- package/docs/request-tracing.md +3 -3
- package/package.json +10 -3
- package/server/app/devDiagnostics.ts +92 -0
- package/server/app/devMcp.ts +55 -0
- package/server/services/prisma/mariadb.ts +7 -3
- package/server/services/router/http/index.ts +25 -0
- package/server/services/router/request/ip.test.cjs +0 -1
- package/tests/agents-utils.test.cjs +58 -3
- package/tests/cli-mcp-command.test.cjs +327 -0
- package/tests/codex-mcp-usage.test.cjs +307 -0
- package/tests/dev-sessions.test.cjs +113 -0
- package/tests/dev-transpile-watch.test.cjs +0 -1
- package/tests/eslint-rules.test.cjs +0 -1
- package/tests/inspection.test.cjs +0 -1
- package/tests/mcp.test.cjs +769 -2
- package/tests/router-cache-config.test.cjs +0 -1
- package/vitest.config.mjs +9 -0
- package/cli/mcp/provider.ts +0 -365
- package/cli/mcp/stdio.ts +0 -16
|
@@ -316,7 +316,7 @@ Only bare `proteum build` and bare `proteum dev` runs print a banner. Other CLI
|
|
|
316
316
|
- `npx proteum explain`
|
|
317
317
|
- `npx proteum doctor`
|
|
318
318
|
- `npx proteum connect`
|
|
319
|
-
- `npx proteum mcp
|
|
319
|
+
- `npx proteum mcp` as the managed machine-scope MCP router; `proteum dev` ensures one daemon is running, agents call `workflow_start` first, use offline candidates to choose the correct app root when needed, then pass the returned live `projectId` to repeated reads against a live dev app
|
|
320
320
|
|
|
321
321
|
CLI output uses compact `proteum-agent-v1` JSON for reproducible command evidence. MCP output uses compact `proteum-mcp-v1` JSON for repeated runtime reads. Use `npx proteum explain --manifest`, `npx proteum diagnose <target> --full`, or `npx proteum trace show <requestId> --events` only when the compact output is insufficient.
|
|
322
322
|
|
|
@@ -328,7 +328,7 @@ Current `proteum dev` supports tracked session management:
|
|
|
328
328
|
- `npx proteum dev list --json`
|
|
329
329
|
- `npx proteum dev stop --session-file var/run/proteum/dev/app.json`
|
|
330
330
|
|
|
331
|
-
If
|
|
331
|
+
`proteum dev` fails fast when another live tracked session already exists for the same app root. If runtime/MCP is unreachable, stop the listed session file first, then start dev again instead of launching a second server in the same worktree.
|
|
332
332
|
|
|
333
333
|
### New diagnostics are available
|
|
334
334
|
|
|
@@ -337,8 +337,7 @@ These are new capabilities, not migration requirements, but they are the fastest
|
|
|
337
337
|
- `npx proteum connect --strict`
|
|
338
338
|
- `npx proteum explain --connected --controllers`
|
|
339
339
|
- `npx proteum runtime status`
|
|
340
|
-
- `npx proteum mcp`
|
|
341
|
-
- `npx proteum mcp --url http://localhost:<port>`
|
|
340
|
+
- `npx proteum mcp status` plus MCP `workflow_start`, `project_resolve`, or `projects_list`
|
|
342
341
|
- `npx proteum diagnose / --port <port>`
|
|
343
342
|
- `npx proteum perf top --port <port>`
|
|
344
343
|
- `npx proteum trace latest --port <port>`
|
|
@@ -379,7 +378,6 @@ Then boot the app and verify the live runtime:
|
|
|
379
378
|
```bash
|
|
380
379
|
npx proteum dev --port 3010
|
|
381
380
|
npx proteum runtime status
|
|
382
|
-
npx proteum mcp --url http://localhost:3010
|
|
383
381
|
npx proteum diagnose / --port 3010
|
|
384
382
|
npx proteum trace latest --port 3010
|
|
385
383
|
```
|
package/docs/request-tracing.md
CHANGED
|
@@ -33,12 +33,12 @@ proteum perf memory --since 1h --group-by controller
|
|
|
33
33
|
|
|
34
34
|
Default trace output is compact `proteum-agent-v1` JSON with counts, failed calls, error events, hot calls, and hot SQL. Use `--events` or `--full` only when raw event details, payload summaries, or SQL text are needed.
|
|
35
35
|
|
|
36
|
-
When an MCP client is available, use MCP `trace_latest`, `trace_show`, `perf_top`, and `perf_request` for repeated reads against the same running app. Keep the CLI commands for reproducible terminal evidence and final verification logs.
|
|
36
|
+
When an MCP client is available, call `workflow_start` with `cwd` or a known `projectId`, then use the returned live `projectId` with MCP `trace_latest`, `trace_show`, `perf_top`, and `perf_request` for repeated reads against the same running app. If `workflow_start` returns offline app candidates or unreachable runtime health, start or repair exactly one `proteum dev` server from the intended app root before trace or perf reads. Keep the CLI commands for reproducible terminal evidence and final verification logs.
|
|
37
37
|
|
|
38
38
|
Before reproducing a bug or starting a new test pass:
|
|
39
39
|
|
|
40
|
-
- run `proteum runtime status` to reuse a tracked dev session when possible
|
|
41
|
-
- use MCP `runtime_status` for repeated status checks against the same running server
|
|
40
|
+
- run `proteum runtime status` to reuse a tracked dev session when possible and to get an exact Start Dev action when the configured router/HMR ports are occupied
|
|
41
|
+
- use MCP `runtime_status` with the selected `projectId` for repeated status checks against the same running server
|
|
42
42
|
- if a server is already running, inspect `proteum trace requests`, `proteum trace latest`, and compact `proteum diagnose <path>` first so you can capture past errors without dumping raw events
|
|
43
43
|
|
|
44
44
|
Typical debugging flow:
|
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
|
+
"version": "2.4.2",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
"keywords": [
|
|
13
13
|
"framework"
|
|
14
14
|
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:codex-mcp": "PROTEUM_RUN_CODEX_MCP_USAGE_TEST=1 vitest run tests/codex-mcp-usage.test.cjs",
|
|
18
|
+
"test:integration": "vitest run tests/dev-transpile-watch.test.cjs",
|
|
19
|
+
"test:unit": "vitest run --exclude tests/dev-transpile-watch.test.cjs"
|
|
20
|
+
},
|
|
15
21
|
"bin": {
|
|
16
22
|
"proteum": "cli/bin.js"
|
|
17
23
|
},
|
|
@@ -104,13 +110,14 @@
|
|
|
104
110
|
"@types/fs-extra": "^9.0.12",
|
|
105
111
|
"@types/markdown-it": "^12.2.3",
|
|
106
112
|
"@types/mime-types": "^2.1.1",
|
|
107
|
-
"@types/node": "^
|
|
113
|
+
"@types/node": "^20.19.40",
|
|
108
114
|
"@types/nodemailer": "^6.4.4",
|
|
109
115
|
"@types/pg": "^8.6.1",
|
|
110
116
|
"@types/pg-escape": "^0.2.1",
|
|
111
117
|
"@types/prompts": "^2.0.14",
|
|
112
118
|
"@types/sharp": "^0.31.1",
|
|
113
119
|
"@types/universal-analytics": "^0.4.5",
|
|
114
|
-
"schema-dts": "^1.1.2"
|
|
120
|
+
"schema-dts": "^1.1.2",
|
|
121
|
+
"vitest": "^4.1.5"
|
|
115
122
|
}
|
|
116
123
|
}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
+
import mysql from 'mysql2/promise';
|
|
2
3
|
import path from 'path';
|
|
4
|
+
import { performance } from 'perf_hooks';
|
|
3
5
|
|
|
4
6
|
import type { Application } from './index';
|
|
5
7
|
import type { TDevConsoleLogLevel, TDevConsoleLogsResponse } from '@common/dev/console';
|
|
8
|
+
import {
|
|
9
|
+
normalizeDatabaseReadLimit,
|
|
10
|
+
normalizeDatabaseReadTimeoutMs,
|
|
11
|
+
validateDatabaseReadQuery,
|
|
12
|
+
type TDatabaseReadQueryInput,
|
|
13
|
+
type TDatabaseReadQueryResponse,
|
|
14
|
+
type TDatabaseReadQueryRow,
|
|
15
|
+
type TDatabaseReadQueryValue,
|
|
16
|
+
} from '@common/dev/database';
|
|
6
17
|
import {
|
|
7
18
|
buildDoctorResponse,
|
|
8
19
|
explainSectionNames,
|
|
@@ -30,12 +41,31 @@ import {
|
|
|
30
41
|
} from '@common/dev/inspection';
|
|
31
42
|
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
32
43
|
import type { TRequestTrace } from '@common/dev/requestTrace';
|
|
44
|
+
import { parseMariaDbDatabaseUrl } from '@server/services/prisma/mariadb';
|
|
33
45
|
|
|
34
46
|
const isExplainSectionName = (value: string): value is TExplainSectionName =>
|
|
35
47
|
explainSectionNames.includes(value as TExplainSectionName);
|
|
36
48
|
const isConsoleLogLevel = (value: string): value is TDevConsoleLogLevel =>
|
|
37
49
|
['silly', 'log', 'info', 'warn', 'error'].includes(value);
|
|
38
50
|
|
|
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
|
+
|
|
39
69
|
export default class DevDiagnosticsRegistry {
|
|
40
70
|
public constructor(private app: Application) {}
|
|
41
71
|
|
|
@@ -88,6 +118,68 @@ export default class DevDiagnosticsRegistry {
|
|
|
88
118
|
return { logs: this.app.container.Console.listLogs(limit, isConsoleLogLevel(minimumLevel) ? minimumLevel : 'log') };
|
|
89
119
|
}
|
|
90
120
|
|
|
121
|
+
public async databaseReadQuery({
|
|
122
|
+
limit: rawLimit,
|
|
123
|
+
sql: rawSql,
|
|
124
|
+
timeoutMs: rawTimeoutMs,
|
|
125
|
+
}: TDatabaseReadQueryInput): Promise<TDatabaseReadQueryResponse> {
|
|
126
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
127
|
+
if (!databaseUrl) throw new Error('DATABASE_URL is required before running database diagnostics.');
|
|
128
|
+
|
|
129
|
+
const { kind, sql } = validateDatabaseReadQuery(rawSql);
|
|
130
|
+
const limit = normalizeDatabaseReadLimit(rawLimit);
|
|
131
|
+
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
|
+
|
|
147
|
+
try {
|
|
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
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
91
183
|
private resolveRequestTrace({ path, requestId }: { path?: string; requestId?: string }): TRequestTrace | undefined {
|
|
92
184
|
if (requestId) return this.app.container.Trace.getRequest(requestId);
|
|
93
185
|
if (!path) return this.app.container.Trace.getLatestRequest();
|
package/server/app/devMcp.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { buildContractsDoctorResponse } from '@common/dev/contractsDoctor';
|
|
|
2
2
|
import { buildDoctorResponse } from '@common/dev/diagnostics';
|
|
3
3
|
import { buildOrientationResponse, explainOwner } from '@common/dev/inspection';
|
|
4
4
|
import {
|
|
5
|
+
compactDatabaseReadQueryResponse,
|
|
5
6
|
buildRuntimeStatusPayload,
|
|
6
7
|
compactDiagnoseResponse,
|
|
7
8
|
compactDoctorResponse,
|
|
@@ -10,7 +11,9 @@ import {
|
|
|
10
11
|
compactOrientationResponse,
|
|
11
12
|
compactPerfRequestResponse,
|
|
12
13
|
compactPerfTopResponse,
|
|
14
|
+
compactRouteCandidatesResponse,
|
|
13
15
|
compactTraceResponse,
|
|
16
|
+
compactWorkflowStartResponse,
|
|
14
17
|
resolveInstructionRouting,
|
|
15
18
|
} from '@common/dev/mcpPayloads';
|
|
16
19
|
import type { TProteumMcpProvider } from '@common/dev/mcpServer';
|
|
@@ -64,6 +67,42 @@ export const createRuntimeProteumMcpProvider = ({
|
|
|
64
67
|
},
|
|
65
68
|
});
|
|
66
69
|
},
|
|
70
|
+
async workflowStart({ file, query, route, task }) {
|
|
71
|
+
const manifest = readManifest();
|
|
72
|
+
const doctor = buildDoctorResponse(manifest);
|
|
73
|
+
const contracts = buildContractsDoctorResponse(manifest);
|
|
74
|
+
const ownerQuery = [route, file, query]
|
|
75
|
+
.map((value) => value?.trim())
|
|
76
|
+
.find((value): value is string => Boolean(value));
|
|
77
|
+
|
|
78
|
+
return compactWorkflowStartResponse({
|
|
79
|
+
contracts,
|
|
80
|
+
doctor,
|
|
81
|
+
file,
|
|
82
|
+
health: {
|
|
83
|
+
reachable: true,
|
|
84
|
+
doctor: doctor.summary,
|
|
85
|
+
contracts: contracts.summary,
|
|
86
|
+
},
|
|
87
|
+
manifest,
|
|
88
|
+
owner: ownerQuery ? explainOwner(manifest, ownerQuery) : undefined,
|
|
89
|
+
query,
|
|
90
|
+
route,
|
|
91
|
+
runtime: {
|
|
92
|
+
publicUrl,
|
|
93
|
+
routerPort,
|
|
94
|
+
source: 'proteum-dev-runtime',
|
|
95
|
+
mcpUrl: `${publicUrl}/__proteum/mcp`,
|
|
96
|
+
traceEnabled: app.container.Trace.isDevTraceEnabled(),
|
|
97
|
+
profilerEnabled: app.container.Trace.isProfilingEnabled(),
|
|
98
|
+
connectedProjects: Object.entries(app.connectedProjects || {}).map(([namespace, project]) => ({
|
|
99
|
+
namespace,
|
|
100
|
+
urlInternal: (project as { urlInternal?: string }).urlInternal,
|
|
101
|
+
})),
|
|
102
|
+
},
|
|
103
|
+
task,
|
|
104
|
+
});
|
|
105
|
+
},
|
|
67
106
|
async orient({ query }) {
|
|
68
107
|
return compactOrientationResponse(buildOrientationResponse(readManifest(), query));
|
|
69
108
|
},
|
|
@@ -80,6 +119,13 @@ export const createRuntimeProteumMcpProvider = ({
|
|
|
80
119
|
query: normalizedQuery,
|
|
81
120
|
});
|
|
82
121
|
},
|
|
122
|
+
async routeCandidates({ limit, query }) {
|
|
123
|
+
return compactRouteCandidatesResponse({
|
|
124
|
+
limit,
|
|
125
|
+
manifest: readManifest(),
|
|
126
|
+
query,
|
|
127
|
+
});
|
|
128
|
+
},
|
|
83
129
|
async doctor({ contracts = true }) {
|
|
84
130
|
const manifest = readManifest();
|
|
85
131
|
|
|
@@ -144,6 +190,15 @@ export const createRuntimeProteumMcpProvider = ({
|
|
|
144
190
|
response: diagnostics().readLogs(limit, level),
|
|
145
191
|
});
|
|
146
192
|
},
|
|
193
|
+
async dbQuery({ limit, sql, timeoutMs }) {
|
|
194
|
+
return compactDatabaseReadQueryResponse(
|
|
195
|
+
await diagnostics().databaseReadQuery({
|
|
196
|
+
limit,
|
|
197
|
+
sql,
|
|
198
|
+
timeoutMs,
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
},
|
|
147
202
|
async readResource(uri) {
|
|
148
203
|
if (uri === 'proteum://runtime/status') return await provider.runtimeStatus({});
|
|
149
204
|
if (uri === 'proteum://instructions/router') return await provider.instructionsResolve({});
|
|
@@ -19,7 +19,7 @@ const decodeUrlSegment = (value: string) => {
|
|
|
19
19
|
return decodeURIComponent(value);
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
export const
|
|
22
|
+
export const parseMariaDbDatabaseUrl = (databaseUrl: string) => {
|
|
23
23
|
const url = new URL(databaseUrl);
|
|
24
24
|
|
|
25
25
|
if (url.protocol !== 'mysql:' && url.protocol !== 'mariadb:')
|
|
@@ -34,7 +34,7 @@ export const createMariaDbAdapter = (databaseUrl: string) => {
|
|
|
34
34
|
const connectTimeoutSeconds = parseInteger(url.searchParams.get('connect_timeout'));
|
|
35
35
|
const idleTimeoutSeconds = parseInteger(url.searchParams.get('max_idle_connection_lifetime'));
|
|
36
36
|
|
|
37
|
-
return
|
|
37
|
+
return {
|
|
38
38
|
host: url.hostname,
|
|
39
39
|
port: parseInteger(url.port) ?? defaultPort,
|
|
40
40
|
user: decodeUrlSegment(url.username),
|
|
@@ -43,5 +43,9 @@ export const createMariaDbAdapter = (databaseUrl: string) => {
|
|
|
43
43
|
connectTimeout: connectTimeoutSeconds ? connectTimeoutSeconds * 1_000 : defaultConnectTimeout,
|
|
44
44
|
idleTimeout: idleTimeoutSeconds ?? defaultIdleTimeout,
|
|
45
45
|
...(connectionLimit !== undefined ? { connectionLimit } : {}),
|
|
46
|
-
}
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const createMariaDbAdapter = (databaseUrl: string) => {
|
|
50
|
+
return new PrismaMariaDb(parseMariaDbDatabaseUrl(databaseUrl));
|
|
47
51
|
};
|
|
@@ -606,6 +606,31 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
606
606
|
}
|
|
607
607
|
});
|
|
608
608
|
|
|
609
|
+
routes.post('/__proteum/db/query', async (req, res) => {
|
|
610
|
+
const sql = typeof req.body?.sql === 'string' ? req.body.sql : '';
|
|
611
|
+
const rawLimit = Number(req.body?.limit);
|
|
612
|
+
const rawTimeoutMs = Number(req.body?.timeoutMs);
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
res.json(
|
|
616
|
+
await this.app.getDevDiagnostics().databaseReadQuery({
|
|
617
|
+
sql,
|
|
618
|
+
limit: Number.isFinite(rawLimit) ? rawLimit : undefined,
|
|
619
|
+
timeoutMs: Number.isFinite(rawTimeoutMs) ? rawTimeoutMs : undefined,
|
|
620
|
+
}),
|
|
621
|
+
);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
624
|
+
const isUsageError =
|
|
625
|
+
message.includes('SQL') ||
|
|
626
|
+
message.includes('allowed') ||
|
|
627
|
+
message.includes('not allowed') ||
|
|
628
|
+
message.includes('DATABASE_URL');
|
|
629
|
+
|
|
630
|
+
res.status(isUsageError ? 400 : 500).json({ error: message });
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
609
634
|
routes.get('/__proteum/diagnose', (req, res) => {
|
|
610
635
|
const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
|
|
611
636
|
const readNumber = (value: unknown, fallback: number) => {
|
|
@@ -2,7 +2,6 @@ const assert = require('node:assert/strict');
|
|
|
2
2
|
const fs = require('node:fs');
|
|
3
3
|
const os = require('node:os');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
-
const test = require('node:test');
|
|
6
5
|
|
|
7
6
|
const coreRoot = path.resolve(__dirname, '..');
|
|
8
7
|
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
@@ -24,12 +23,14 @@ const createCoreFixture = () => {
|
|
|
24
23
|
|
|
25
24
|
writeFile(path.join(agentsRoot, 'AGENTS.md'), '# Root Contract\n\n- Root rule\n');
|
|
26
25
|
writeFile(path.join(agentsRoot, 'CODING_STYLE.md'), '# Coding Style\n\n- Style rule\n');
|
|
26
|
+
writeFile(path.join(agentsRoot, 'DOCUMENTATION.md'), '# Documentation\n\n- Documentation rule\n');
|
|
27
27
|
writeFile(path.join(agentsRoot, 'diagnostics.md'), '# Diagnostics\n\n- Diagnostics rule\n');
|
|
28
28
|
writeFile(path.join(agentsRoot, 'optimizations.md'), '# Optimizations\n\n- Optimization rule\n');
|
|
29
29
|
writeFile(path.join(agentsRoot, 'client', 'AGENTS.md'), '# Client Rules\n\n- Client rule\n');
|
|
30
30
|
writeFile(path.join(agentsRoot, 'client', 'pages', 'AGENTS.md'), '# Page Rules\n\n- Page rule\n');
|
|
31
31
|
writeFile(path.join(agentsRoot, 'server', 'routes', 'AGENTS.md'), '# Route Rules\n\n- Route rule\n');
|
|
32
32
|
writeFile(path.join(agentsRoot, 'server', 'services', 'AGENTS.md'), '# Service Rules\n\n- Service rule\n');
|
|
33
|
+
writeFile(path.join(agentsRoot, 'tests', 'AGENTS.md'), '# Test Rules\n\n- Test rule\n');
|
|
33
34
|
writeFile(path.join(agentsRoot, 'tests', 'e2e', 'AGENTS.md'), '# E2E Rules\n\n- E2E rule\n');
|
|
34
35
|
writeFile(
|
|
35
36
|
path.join(agentsRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'),
|
|
@@ -53,6 +54,7 @@ const createAppFixture = () => {
|
|
|
53
54
|
'# Proteum-managed instruction files',
|
|
54
55
|
'/AGENTS.md',
|
|
55
56
|
'/CODING_STYLE.md',
|
|
57
|
+
'/DOCUMENTATION.md',
|
|
56
58
|
'# End Proteum-managed instruction files',
|
|
57
59
|
'/.proteum',
|
|
58
60
|
'',
|
|
@@ -62,23 +64,61 @@ const createAppFixture = () => {
|
|
|
62
64
|
return appRoot;
|
|
63
65
|
};
|
|
64
66
|
|
|
67
|
+
test('project instruction sources require unit tests for applicable production changes', () => {
|
|
68
|
+
const projectAgentsRoot = path.join(coreRoot, 'agents', 'project');
|
|
69
|
+
|
|
70
|
+
assert.match(
|
|
71
|
+
fs.readFileSync(path.join(projectAgentsRoot, 'AGENTS.md'), 'utf8'),
|
|
72
|
+
/production changes must always add or update focused unit tests/,
|
|
73
|
+
);
|
|
74
|
+
assert.match(
|
|
75
|
+
fs.readFileSync(path.join(projectAgentsRoot, 'root', 'AGENTS.md'), 'utf8'),
|
|
76
|
+
/always add or update focused unit tests/,
|
|
77
|
+
);
|
|
78
|
+
assert.match(
|
|
79
|
+
fs.readFileSync(path.join(projectAgentsRoot, 'tests', 'AGENTS.md'), 'utf8'),
|
|
80
|
+
/For every production change, add or update focused unit tests/,
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
65
84
|
test('standalone configure creates tracked instruction files with routing contract and split docs', () => {
|
|
66
85
|
const coreRoot = createCoreFixture();
|
|
67
86
|
const appRoot = createAppFixture();
|
|
68
87
|
const result = configureProjectAgentInstructions({ appRoot, coreRoot });
|
|
69
88
|
const agentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
|
|
70
89
|
const codingStyleContent = fs.readFileSync(path.join(appRoot, 'CODING_STYLE.md'), 'utf8');
|
|
90
|
+
const documentationContent = fs.readFileSync(path.join(appRoot, 'DOCUMENTATION.md'), 'utf8');
|
|
71
91
|
const gitignoreContent = fs.readFileSync(path.join(appRoot, '.gitignore'), 'utf8');
|
|
72
92
|
|
|
73
93
|
assert.equal(result.blocked.length, 0);
|
|
74
94
|
assert.match(agentsContent, /^# Proteum Instructions/m);
|
|
75
95
|
assert.match(agentsContent, /<!-- proteum-instructions:start -->/);
|
|
76
96
|
assert.match(agentsContent, /## Agent Routing Contract/);
|
|
77
|
-
assert.match(agentsContent, /npx proteum
|
|
97
|
+
assert.match(agentsContent, /npx proteum runtime status/);
|
|
98
|
+
assert.match(agentsContent, /MCP `workflow_start`/);
|
|
99
|
+
assert.match(agentsContent, /project_resolve \{ cwd \}/);
|
|
100
|
+
assert.match(agentsContent, /instructions_resolve \{ projectId \}/);
|
|
101
|
+
assert.match(agentsContent, /Do not run CLI equivalents after a successful MCP result/);
|
|
102
|
+
assert.match(agentsContent, /Read full files only before edits or git writes/);
|
|
103
|
+
assert.match(agentsContent, /explain_summary/);
|
|
104
|
+
assert.match(agentsContent, /\/__proteum\/mcp/);
|
|
105
|
+
assert.match(agentsContent, /proteum-mcp-v1/);
|
|
106
|
+
assert.match(agentsContent, /## Triggered Instruction Reads/);
|
|
107
|
+
assert.match(agentsContent, /Git lifecycle/);
|
|
108
|
+
assert.match(agentsContent, /read Root contract fallback before any git write/);
|
|
109
|
+
assert.match(agentsContent, /add or update focused unit tests/);
|
|
110
|
+
assert.match(agentsContent, /read Root contract fallback, `CODING_STYLE\.md`, `tests\/AGENTS\.md`/);
|
|
111
|
+
assert.match(agentsContent, /MCP-selected previews are enough/);
|
|
112
|
+
assert.doesNotMatch(agentsContent, /Conventional Commits/);
|
|
113
|
+
assert.match(agentsContent, /They are not deleted/);
|
|
78
114
|
assert.doesNotMatch(agentsContent, /## Source: CODING_STYLE\.md/);
|
|
79
115
|
assert.match(codingStyleContent, /## Source: CODING_STYLE\.md/);
|
|
80
116
|
assert.match(codingStyleContent, /## Coding Style/);
|
|
81
117
|
assert.doesNotMatch(codingStyleContent, /## Source: client\/AGENTS\.md/);
|
|
118
|
+
assert.match(documentationContent, /## Source: DOCUMENTATION\.md/);
|
|
119
|
+
assert.match(documentationContent, /## Documentation/);
|
|
120
|
+
assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'AGENTS.md')), true);
|
|
121
|
+
assert.match(fs.readFileSync(path.join(appRoot, 'tests', 'AGENTS.md'), 'utf8'), /Test rule/);
|
|
82
122
|
assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'AGENTS.md')), true);
|
|
83
123
|
assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md')), true);
|
|
84
124
|
assert.match(fs.readFileSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /Journey rule/);
|
|
@@ -86,6 +126,7 @@ test('standalone configure creates tracked instruction files with routing contra
|
|
|
86
126
|
assert.doesNotMatch(agentsContent, /Before reading or applying instructions from this file/);
|
|
87
127
|
assert.doesNotMatch(gitignoreContent, /Proteum-managed instruction files/);
|
|
88
128
|
assert.doesNotMatch(gitignoreContent, /^\/AGENTS\.md$/m);
|
|
129
|
+
assert.doesNotMatch(gitignoreContent, /^\/DOCUMENTATION\.md$/m);
|
|
89
130
|
});
|
|
90
131
|
|
|
91
132
|
test('configure preserves project content outside the managed section', () => {
|
|
@@ -176,6 +217,10 @@ test('monorepo configure writes root and app instruction files', () => {
|
|
|
176
217
|
|
|
177
218
|
fs.mkdirSync(path.join(monorepoRoot, '.git'));
|
|
178
219
|
fs.mkdirSync(path.join(appRoot, 'client'), { recursive: true });
|
|
220
|
+
fs.mkdirSync(path.join(appRoot, 'server'), { recursive: true });
|
|
221
|
+
writeFile(path.join(appRoot, 'package.json'), '{"name":"product"}\n');
|
|
222
|
+
writeFile(path.join(appRoot, 'identity.config.ts'), 'export default {};\n');
|
|
223
|
+
writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
|
|
179
224
|
|
|
180
225
|
configureProjectAgentInstructions({ appRoot, coreRoot });
|
|
181
226
|
|
|
@@ -184,16 +229,26 @@ test('monorepo configure writes root and app instruction files', () => {
|
|
|
184
229
|
assert.equal(result.mode, 'monorepo');
|
|
185
230
|
assert.equal(resolveProjectAgentMonorepoRoot(appRoot), fs.realpathSync(monorepoRoot));
|
|
186
231
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Agent Routing Contract/);
|
|
232
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Known Proteum Apps/);
|
|
233
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /apps\/product/);
|
|
234
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /Do not start `npx proteum dev` from this root/);
|
|
187
235
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'CODING_STYLE.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
|
|
236
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'DOCUMENTATION.md'), 'utf8'), /## Source: DOCUMENTATION\.md/);
|
|
188
237
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'diagnostics.md'), 'utf8'), /## Source: diagnostics\.md/);
|
|
189
238
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'optimizations.md'), 'utf8'), /## Source: optimizations\.md/);
|
|
239
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'AGENTS.md'), 'utf8'), /## Source: tests\/AGENTS\.md/);
|
|
190
240
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'AGENTS.md'), 'utf8'), /## Source: tests\/e2e\/AGENTS\.md/);
|
|
191
241
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: tests\/e2e\/REAL_WORLD_JOURNEY_TESTS\.md/);
|
|
192
242
|
assert.doesNotMatch(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
|
|
243
|
+
assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'AGENTS.md')), false);
|
|
193
244
|
assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'AGENTS.md')), false);
|
|
194
|
-
|
|
245
|
+
const appAgentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
|
|
246
|
+
assert.match(appAgentsContent, /## Agent Routing Contract/);
|
|
247
|
+
assert.doesNotMatch(appAgentsContent, /## Known Proteum Apps/);
|
|
248
|
+
assert.doesNotMatch(appAgentsContent, /Do not start `npx proteum dev` from this root/);
|
|
195
249
|
assert.match(fs.readFileSync(path.join(appRoot, 'client', 'AGENTS.md'), 'utf8'), /## Source: client\/AGENTS\.md/);
|
|
196
250
|
assert.equal(fs.existsSync(path.join(appRoot, 'CODING_STYLE.md')), false);
|
|
251
|
+
assert.equal(fs.existsSync(path.join(appRoot, 'DOCUMENTATION.md')), false);
|
|
197
252
|
assert.equal(fs.existsSync(path.join(appRoot, 'diagnostics.md')), false);
|
|
198
253
|
assert.equal(fs.existsSync(path.join(appRoot, 'optimizations.md')), false);
|
|
199
254
|
assert.equal(result.removed.some((entry) => entry.endsWith('/apps/product/CODING_STYLE.md')), true);
|