proteum 2.2.9 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -2
- package/README.md +49 -11
- package/agents/project/AGENTS.md +43 -5
- package/agents/project/diagnostics.md +6 -2
- package/agents/project/optimizations.md +1 -0
- package/agents/project/root/AGENTS.md +14 -5
- package/agents/project/tests/AGENTS.md +6 -0
- package/agents/project/tests/e2e/AGENTS.md +13 -0
- package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
- package/cli/commands/connect.ts +40 -4
- package/cli/commands/diagnose.ts +136 -5
- package/cli/commands/doctor.ts +24 -4
- package/cli/commands/explain.ts +105 -6
- package/cli/commands/mcp.ts +16 -0
- package/cli/commands/orient.ts +66 -3
- package/cli/commands/perf.ts +118 -13
- package/cli/commands/runtime.ts +151 -0
- package/cli/commands/trace.ts +116 -21
- package/cli/mcp/provider.ts +365 -0
- package/cli/mcp/stdio.ts +16 -0
- package/cli/presentation/commands.ts +77 -20
- package/cli/presentation/devSession.ts +2 -0
- package/cli/runtime/commands.ts +95 -12
- package/cli/utils/agentOutput.ts +46 -0
- package/cli/utils/agents.ts +116 -49
- package/common/dev/inspection.ts +14 -6
- package/common/dev/mcpPayloads.ts +736 -0
- package/common/dev/mcpServer.ts +254 -0
- package/docs/agent-routing.md +126 -0
- package/docs/dev-commands.md +2 -0
- package/docs/dev-sessions.md +2 -1
- package/docs/diagnostics.md +68 -23
- package/docs/mcp.md +149 -0
- package/docs/migrate-from-2.1.3.md +15 -5
- package/docs/request-tracing.md +12 -6
- package/package.json +2 -1
- package/server/app/devMcp.ts +159 -0
- package/server/services/router/http/cache.ts +116 -0
- package/server/services/router/http/index.ts +94 -35
- package/server/services/router/index.ts +8 -11
- package/tests/agents-utils.test.cjs +36 -13
- package/tests/dev-transpile-watch.test.cjs +117 -8
- package/tests/inspection.test.cjs +67 -0
- package/tests/mcp.test.cjs +127 -0
- package/tests/router-cache-config.test.cjs +74 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import got from 'got';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { UsageError } from 'clipanion';
|
|
5
|
+
|
|
6
|
+
import cli from '..';
|
|
7
|
+
import { readProteumManifest } from '../compiler/common/proteumManifest';
|
|
8
|
+
import { listDevSessionInspections, type TDevSessionInspection } from '../runtime/devSessions';
|
|
9
|
+
import { printAgentResponse, printJson, quoteCommandArgument } from '../utils/agentOutput';
|
|
10
|
+
import type { TDoctorResponse } from '@common/dev/diagnostics';
|
|
11
|
+
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
12
|
+
|
|
13
|
+
type TRuntimeAction = 'status';
|
|
14
|
+
|
|
15
|
+
const allowedActions = new Set<TRuntimeAction>(['status']);
|
|
16
|
+
|
|
17
|
+
const getAction = () => {
|
|
18
|
+
const action = typeof cli.args.action === 'string' && cli.args.action ? cli.args.action : 'status';
|
|
19
|
+
if (!allowedActions.has(action as TRuntimeAction)) {
|
|
20
|
+
throw new UsageError(`Unsupported runtime action "${action}". Expected one of: ${[...allowedActions].join(', ')}.`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return action as TRuntimeAction;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const readManifestIfAvailable = (): TProteumManifest | undefined => {
|
|
27
|
+
const manifestFilepath = path.join(cli.paths.appRoot, '.proteum', 'manifest.json');
|
|
28
|
+
if (!fs.existsSync(manifestFilepath)) return undefined;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
return readProteumManifest(cli.paths.appRoot);
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getSessionUrl = (inspection: TDevSessionInspection) => {
|
|
38
|
+
if (!inspection.record) return '';
|
|
39
|
+
if (inspection.record.publicUrl) return inspection.record.publicUrl.replace(/\/+$/, '');
|
|
40
|
+
return `http://localhost:${inspection.record.routerPort}`;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const probeDoctor = async (baseUrl: string) => {
|
|
44
|
+
if (!baseUrl) return { reachable: false, error: 'No dev URL is registered.' };
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const response = await got(`${baseUrl}/__proteum/doctor`, {
|
|
48
|
+
responseType: 'json',
|
|
49
|
+
retry: { limit: 0 },
|
|
50
|
+
throwHttpErrors: false,
|
|
51
|
+
timeout: { request: 1200 },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (response.statusCode >= 400) {
|
|
55
|
+
return { reachable: false, statusCode: response.statusCode, error: `Doctor returned HTTP ${response.statusCode}.` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const doctor = response.body as TDoctorResponse;
|
|
59
|
+
return {
|
|
60
|
+
reachable: true,
|
|
61
|
+
statusCode: response.statusCode,
|
|
62
|
+
doctor: doctor.summary,
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
reachable: false,
|
|
67
|
+
error: error instanceof Error ? error.message : String(error),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const compactSession = (inspection: TDevSessionInspection) => ({
|
|
73
|
+
sessionFilePath: inspection.sessionFilePath,
|
|
74
|
+
live: inspection.live,
|
|
75
|
+
stale: inspection.stale,
|
|
76
|
+
invalid: inspection.invalid,
|
|
77
|
+
parseError: inspection.parseError,
|
|
78
|
+
pid: inspection.record?.pid,
|
|
79
|
+
routerPort: inspection.record?.routerPort,
|
|
80
|
+
publicUrl: inspection.record?.publicUrl,
|
|
81
|
+
state: inspection.record?.state,
|
|
82
|
+
startedAt: inspection.record?.startedAt,
|
|
83
|
+
updatedAt: inspection.record?.updatedAt,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const run = async () => {
|
|
87
|
+
const action = getAction();
|
|
88
|
+
if (action !== 'status') return;
|
|
89
|
+
|
|
90
|
+
const manifest = readManifestIfAvailable();
|
|
91
|
+
const sessions = await listDevSessionInspections({
|
|
92
|
+
appRoot: cli.paths.appRoot,
|
|
93
|
+
sessionFilePath: typeof cli.args.sessionFile === 'string' && cli.args.sessionFile ? cli.args.sessionFile : undefined,
|
|
94
|
+
});
|
|
95
|
+
const liveSessions = sessions.filter((inspection) => inspection.live && inspection.record);
|
|
96
|
+
const selectedSession =
|
|
97
|
+
liveSessions.find((inspection) => inspection.record?.state === 'ready') || liveSessions[0] || sessions.find((inspection) => inspection.record);
|
|
98
|
+
const selectedBaseUrl = selectedSession ? getSessionUrl(selectedSession) : '';
|
|
99
|
+
const health = selectedSession && selectedSession.live ? await probeDoctor(selectedBaseUrl) : { reachable: false, error: 'No live tracked dev session.' };
|
|
100
|
+
|
|
101
|
+
const payload = {
|
|
102
|
+
appRoot: cli.paths.appRoot,
|
|
103
|
+
manifest: manifest
|
|
104
|
+
? {
|
|
105
|
+
identifier: manifest.app.identity.identifier,
|
|
106
|
+
name: manifest.app.identity.name,
|
|
107
|
+
routerPort: manifest.env.resolved.routerPort,
|
|
108
|
+
diagnostics: {
|
|
109
|
+
errors: manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length,
|
|
110
|
+
warnings: manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length,
|
|
111
|
+
},
|
|
112
|
+
counts: {
|
|
113
|
+
connectedProjects: manifest.connectedProjects.length,
|
|
114
|
+
controllers: manifest.controllers.length,
|
|
115
|
+
routes: manifest.routes.client.length + manifest.routes.server.length,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
: undefined,
|
|
119
|
+
selected: selectedSession ? compactSession(selectedSession) : undefined,
|
|
120
|
+
sessions: sessions.map(compactSession),
|
|
121
|
+
health,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (cli.args.full === true) {
|
|
125
|
+
printJson(payload);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
printAgentResponse({
|
|
130
|
+
summary: selectedSession
|
|
131
|
+
? `${selectedSession.live ? 'live' : 'stale'} dev session on ${selectedSession.record?.routerPort || 'unknown port'}; health=${health.reachable ? 'reachable' : 'unreachable'}`
|
|
132
|
+
: 'No tracked Proteum dev session found.',
|
|
133
|
+
data: payload,
|
|
134
|
+
nextActions: selectedSession?.record
|
|
135
|
+
? [
|
|
136
|
+
{
|
|
137
|
+
label: 'Diagnose Root',
|
|
138
|
+
command: `proteum diagnose ${quoteCommandArgument('/')} --port ${selectedSession.record.routerPort}`,
|
|
139
|
+
reason: 'Use the selected runtime for the smallest request-level diagnostic pass.',
|
|
140
|
+
},
|
|
141
|
+
]
|
|
142
|
+
: [
|
|
143
|
+
{
|
|
144
|
+
label: 'Start Dev',
|
|
145
|
+
command: 'proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port <free-port>',
|
|
146
|
+
reason: 'Create a tracked dev session before request-time diagnostics.',
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
fullDetailCommand: 'proteum runtime status --full',
|
|
150
|
+
});
|
|
151
|
+
};
|
package/cli/commands/trace.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import { UsageError } from 'clipanion';
|
|
5
5
|
|
|
6
6
|
import cli from '..';
|
|
7
|
+
import { compactList, printAgentResponse, printJson, quoteCommandArgument, truncateForAgent } from '../utils/agentOutput';
|
|
7
8
|
import type {
|
|
8
9
|
TRequestTrace,
|
|
9
10
|
TRequestTraceArmResponse,
|
|
@@ -164,23 +165,114 @@ const renderTrace = (request: TRequestTrace) =>
|
|
|
164
165
|
),
|
|
165
166
|
].join('\n');
|
|
166
167
|
|
|
167
|
-
const
|
|
168
|
-
|
|
168
|
+
const compactCall = (call: TRequestTrace['calls'][number]) => ({
|
|
169
|
+
id: call.id,
|
|
170
|
+
origin: call.origin,
|
|
171
|
+
label: call.label,
|
|
172
|
+
method: call.method,
|
|
173
|
+
path: call.path,
|
|
174
|
+
statusCode: call.statusCode,
|
|
175
|
+
durationMs: call.durationMs,
|
|
176
|
+
errorMessage: call.errorMessage ? truncateForAgent(call.errorMessage) : undefined,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const compactSql = (query: TRequestTrace['sqlQueries'][number]) => ({
|
|
180
|
+
id: query.id,
|
|
181
|
+
caller: query.callerLabel || `${query.callerMethod} ${query.callerPath}`,
|
|
182
|
+
kind: query.kind,
|
|
183
|
+
operation: query.operation,
|
|
184
|
+
model: query.model,
|
|
185
|
+
durationMs: query.durationMs,
|
|
186
|
+
fingerprint: query.fingerprint,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const compactEvent = (event: TRequestTrace['events'][number]) => ({
|
|
190
|
+
index: event.index,
|
|
191
|
+
elapsedMs: event.elapsedMs,
|
|
192
|
+
type: event.type,
|
|
193
|
+
detailKeys: Object.keys(event.details),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const buildTraceFullDetailCommand = (request: TRequestTrace) =>
|
|
197
|
+
[
|
|
198
|
+
'proteum trace show',
|
|
199
|
+
quoteCommandArgument(request.id),
|
|
200
|
+
typeof cli.args.port === 'string' && cli.args.port ? `--port ${cli.args.port}` : '',
|
|
201
|
+
typeof cli.args.url === 'string' && cli.args.url ? `--url ${quoteCommandArgument(cli.args.url)}` : '',
|
|
202
|
+
'--events',
|
|
203
|
+
]
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
.join(' ');
|
|
206
|
+
|
|
207
|
+
const printCompactTrace = (request: TRequestTrace) => {
|
|
208
|
+
const failedCalls = request.calls.filter((call) => call.errorMessage || (call.statusCode !== undefined && call.statusCode >= 400));
|
|
209
|
+
const errorEvents = request.events.filter((event) => event.type === 'error');
|
|
210
|
+
const hotCalls = [...request.calls].sort((left, right) => (right.durationMs || 0) - (left.durationMs || 0));
|
|
211
|
+
const hotSql = [...request.sqlQueries].sort((left, right) => right.durationMs - left.durationMs);
|
|
212
|
+
|
|
213
|
+
printAgentResponse({
|
|
214
|
+
summary: `${request.id}: ${request.method} ${request.path} status=${request.statusCode ?? 'pending'} durationMs=${request.durationMs ?? 'pending'} events=${request.events.length} calls=${request.calls.length} sql=${request.sqlQueries.length}`,
|
|
215
|
+
data: {
|
|
216
|
+
request: {
|
|
217
|
+
id: request.id,
|
|
218
|
+
method: request.method,
|
|
219
|
+
path: request.path,
|
|
220
|
+
statusCode: request.statusCode,
|
|
221
|
+
durationMs: request.durationMs,
|
|
222
|
+
capture: request.capture,
|
|
223
|
+
user: request.user,
|
|
224
|
+
errorMessage: request.errorMessage,
|
|
225
|
+
droppedEvents: request.droppedEvents,
|
|
226
|
+
persistedFilepath: request.persistedFilepath,
|
|
227
|
+
},
|
|
228
|
+
counts: {
|
|
229
|
+
calls: request.calls.length,
|
|
230
|
+
events: request.events.length,
|
|
231
|
+
sqlQueries: request.sqlQueries.length,
|
|
232
|
+
},
|
|
233
|
+
failedCalls: compactList(failedCalls, 5).map(compactCall),
|
|
234
|
+
errorEvents: compactList(errorEvents, 5).map(compactEvent),
|
|
235
|
+
hotCalls: compactList(hotCalls, 5).map(compactCall),
|
|
236
|
+
hotSql: compactList(hotSql, 5).map(compactSql),
|
|
237
|
+
},
|
|
238
|
+
nextActions: [
|
|
239
|
+
{
|
|
240
|
+
label: 'Diagnose Request',
|
|
241
|
+
command: `proteum diagnose ${quoteCommandArgument(request.path)}`,
|
|
242
|
+
reason: 'Collapse this trace with owner lookup, diagnostics, suspects, and server logs.',
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
label: 'Perf Request',
|
|
246
|
+
command: `proteum perf request ${quoteCommandArgument(request.id)}`,
|
|
247
|
+
reason: 'Inspect request timing, SQL, render, and memory rollups without full events.',
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
fullDetailCommand: buildTraceFullDetailCommand(request),
|
|
251
|
+
omitted: [
|
|
252
|
+
{
|
|
253
|
+
reason: 'Full event details, payload summaries, raw SQL, and call bodies are omitted by default.',
|
|
254
|
+
command: buildTraceFullDetailCommand(request),
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
});
|
|
169
258
|
};
|
|
170
259
|
|
|
171
260
|
export const run = async () => {
|
|
172
261
|
const action = getAction();
|
|
173
262
|
const requestId = typeof cli.args.id === 'string' ? cli.args.id : '';
|
|
174
|
-
const
|
|
263
|
+
const shouldPrintFull = cli.args.full === true || cli.args.events === true;
|
|
264
|
+
const shouldPrintHuman = cli.args.human === true;
|
|
175
265
|
|
|
176
266
|
if (action === 'requests') {
|
|
177
267
|
const response = await requestJson<TRequestTraceListResponse>('/__proteum/trace/requests');
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
268
|
+
if (shouldPrintFull) printJson(response);
|
|
269
|
+
else if (shouldPrintHuman) console.log(['Proteum trace', ...response.requests.map(renderTraceSummary)].join('\n'));
|
|
270
|
+
else
|
|
271
|
+
printAgentResponse({
|
|
272
|
+
summary: `${response.requests.length} request traces`,
|
|
273
|
+
data: { requests: compactList(response.requests, 20), totalReturned: response.requests.length },
|
|
274
|
+
fullDetailCommand: 'proteum trace requests --full',
|
|
275
|
+
});
|
|
184
276
|
return;
|
|
185
277
|
}
|
|
186
278
|
|
|
@@ -191,23 +283,20 @@ export const run = async () => {
|
|
|
191
283
|
json: { capture },
|
|
192
284
|
});
|
|
193
285
|
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
console.log(`Armed next request trace with capture=${response.capture}.`);
|
|
286
|
+
if (shouldPrintHuman) console.log(`Armed next request trace with capture=${response.capture}.`);
|
|
287
|
+
else printAgentResponse({ summary: `Armed next request trace with capture=${response.capture}.`, data: response });
|
|
200
288
|
return;
|
|
201
289
|
}
|
|
202
290
|
|
|
203
291
|
if (action === 'latest') {
|
|
204
292
|
const response = await requestJson<TRequestTraceResponse>('/__proteum/trace/latest');
|
|
205
|
-
if (
|
|
293
|
+
if (shouldPrintFull) {
|
|
206
294
|
printJson(response);
|
|
207
295
|
return;
|
|
208
296
|
}
|
|
209
297
|
|
|
210
|
-
console.log(renderTrace(response.request));
|
|
298
|
+
if (shouldPrintHuman) console.log(renderTrace(response.request));
|
|
299
|
+
else printCompactTrace(response.request);
|
|
211
300
|
return;
|
|
212
301
|
}
|
|
213
302
|
|
|
@@ -218,12 +307,13 @@ export const run = async () => {
|
|
|
218
307
|
const response = await requestJson<TRequestTraceResponse>(`/__proteum/trace/requests/${requestId}`);
|
|
219
308
|
|
|
220
309
|
if (action === 'show') {
|
|
221
|
-
if (
|
|
310
|
+
if (shouldPrintFull) {
|
|
222
311
|
printJson(response);
|
|
223
312
|
return;
|
|
224
313
|
}
|
|
225
314
|
|
|
226
|
-
console.log(renderTrace(response.request));
|
|
315
|
+
if (shouldPrintHuman) console.log(renderTrace(response.request));
|
|
316
|
+
else printCompactTrace(response.request);
|
|
227
317
|
return;
|
|
228
318
|
}
|
|
229
319
|
|
|
@@ -235,10 +325,15 @@ export const run = async () => {
|
|
|
235
325
|
fs.ensureDirSync(path.dirname(output));
|
|
236
326
|
fs.writeJSONSync(output, response.request, { spaces: 2 });
|
|
237
327
|
|
|
238
|
-
if (
|
|
328
|
+
if (shouldPrintFull) {
|
|
239
329
|
printJson({ output, request: response.request });
|
|
240
330
|
return;
|
|
241
331
|
}
|
|
242
332
|
|
|
243
|
-
console.log(`Exported trace ${response.request.id} to ${output}`);
|
|
333
|
+
if (shouldPrintHuman) console.log(`Exported trace ${response.request.id} to ${output}`);
|
|
334
|
+
else
|
|
335
|
+
printAgentResponse({
|
|
336
|
+
summary: `Exported trace ${response.request.id}.`,
|
|
337
|
+
data: { output, requestId: response.request.id },
|
|
338
|
+
});
|
|
244
339
|
};
|