proteum 2.3.0 → 2.4.1
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 +15 -10
- package/agents/project/DOCUMENTATION.md +1326 -0
- package/agents/project/app-root/AGENTS.md +2 -2
- package/agents/project/diagnostics.md +9 -8
- package/agents/project/root/AGENTS.md +14 -8
- package/agents/project/tests/AGENTS.md +1 -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 +1010 -0
- package/cli/presentation/commands.ts +34 -24
- package/cli/presentation/help.ts +1 -1
- package/cli/runtime/commands.ts +129 -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 +93 -17
- package/cli/utils/appRoots.ts +232 -0
- package/common/dev/diagnostics.ts +1 -1
- package/common/dev/inspection.ts +8 -1
- package/common/dev/mcpPayloads.ts +431 -17
- package/common/dev/mcpServer.ts +33 -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 +109 -52
- 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/devMcp.ts +45 -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 +262 -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 +748 -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
package/cli/commands/mcp.ts
CHANGED
|
@@ -1,16 +1,133 @@
|
|
|
1
1
|
import cli from '..';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { startProteumMachineMcpRouter, startProteumMachineMcpRouterHttp } from '../mcp/router';
|
|
3
|
+
import {
|
|
4
|
+
ensureMachineMcpDaemonProcess,
|
|
5
|
+
inspectMachineMcpDaemonRecord,
|
|
6
|
+
resolveMachineMcpDaemonPort,
|
|
7
|
+
stopMachineMcpDaemonProcess,
|
|
8
|
+
} from '../runtime/mcpDaemon';
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
const printJson = (payload: unknown) => {
|
|
11
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const printStatus = async () => {
|
|
15
|
+
const inspection = await inspectMachineMcpDaemonRecord({ cleanStale: true });
|
|
16
|
+
|
|
17
|
+
if (cli.args.json === true) {
|
|
18
|
+
printJson({
|
|
19
|
+
daemon: inspection
|
|
20
|
+
? {
|
|
21
|
+
live: inspection.live,
|
|
22
|
+
stale: inspection.stale,
|
|
23
|
+
invalid: inspection.invalid,
|
|
24
|
+
parseError: inspection.parseError,
|
|
25
|
+
record: inspection.record,
|
|
26
|
+
}
|
|
27
|
+
: null,
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!inspection?.record || !inspection.live) {
|
|
33
|
+
console.info('No live Proteum machine MCP daemon found.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.info(
|
|
38
|
+
[
|
|
39
|
+
`Proteum machine MCP daemon is running.`,
|
|
40
|
+
`pid ${inspection.record.pid}`,
|
|
41
|
+
`mcp ${inspection.record.mcpUrl}`,
|
|
42
|
+
`health ${inspection.record.healthUrl}`,
|
|
43
|
+
].join('\n'),
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const runDaemon = async () => {
|
|
48
|
+
const existing = await inspectMachineMcpDaemonRecord({ cleanStale: true });
|
|
49
|
+
|
|
50
|
+
if (existing?.record && existing.live && existing.record.pid !== process.pid) {
|
|
51
|
+
if (cli.args.json === true) {
|
|
52
|
+
printJson({ started: false, daemon: existing.record });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.info(`Proteum machine MCP daemon is already running at ${existing.record.mcpUrl} (pid ${existing.record.pid}).`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const port = resolveMachineMcpDaemonPort(typeof cli.args.port === 'string' ? cli.args.port : undefined);
|
|
61
|
+
|
|
62
|
+
await startProteumMachineMcpRouterHttp({
|
|
63
|
+
port,
|
|
64
|
+
version: String(cli.packageJson.version || ''),
|
|
10
65
|
});
|
|
11
66
|
|
|
12
|
-
|
|
13
|
-
|
|
67
|
+
if (cli.args.json === true) {
|
|
68
|
+
printJson({
|
|
69
|
+
started: true,
|
|
70
|
+
daemon: {
|
|
71
|
+
pid: process.pid,
|
|
72
|
+
mcpUrl: `http://127.0.0.1:${port}/mcp`,
|
|
73
|
+
healthUrl: `http://127.0.0.1:${port}/health`,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
} else {
|
|
77
|
+
console.info(`Proteum machine MCP daemon started at http://127.0.0.1:${port}/mcp.`);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const ensureDaemon = async () => {
|
|
82
|
+
const result = await ensureMachineMcpDaemonProcess({
|
|
83
|
+
coreRoot: cli.paths.core.root,
|
|
84
|
+
port: typeof cli.args.port === 'string' ? cli.args.port : undefined,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (cli.args.json === true) {
|
|
88
|
+
printJson({ started: result.started, daemon: result.inspection.record });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (result.inspection.record) {
|
|
93
|
+
console.info(
|
|
94
|
+
result.started
|
|
95
|
+
? `Proteum machine MCP daemon started at ${result.inspection.record.mcpUrl}.`
|
|
96
|
+
: `Proteum machine MCP daemon is already running at ${result.inspection.record.mcpUrl}.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const run = async () => {
|
|
102
|
+
if (cli.args.action === 'status') {
|
|
103
|
+
await printStatus();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (cli.args.action === 'stop') {
|
|
108
|
+
const result = await stopMachineMcpDaemonProcess();
|
|
109
|
+
if (cli.args.json === true) {
|
|
110
|
+
printJson({ stopped: result.stopped, daemon: result.inspection?.record || null });
|
|
111
|
+
} else if (result.stopped) {
|
|
112
|
+
console.info('Proteum machine MCP daemon stopped.');
|
|
113
|
+
} else if (result.inspection?.record) {
|
|
114
|
+
console.info(`Could not stop Proteum machine MCP daemon pid ${result.inspection.record.pid}.`);
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (cli.args.daemon === true) {
|
|
121
|
+
await runDaemon();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (cli.args.stdio !== true && (process.stdout.isTTY || cli.args.json === true)) {
|
|
126
|
+
await ensureDaemon();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await startProteumMachineMcpRouter({
|
|
14
131
|
version: String(cli.packageJson.version || ''),
|
|
15
132
|
});
|
|
16
133
|
};
|
package/cli/commands/orient.ts
CHANGED
|
@@ -7,6 +7,7 @@ import cli from '..';
|
|
|
7
7
|
import Compiler from '../compiler';
|
|
8
8
|
import { readProteumManifest } from '../compiler/common/proteumManifest';
|
|
9
9
|
import { buildOrientationResponse, type TOrientResponse } from '@common/dev/inspection';
|
|
10
|
+
import { resolveTriggeredInstructionReads } from '@common/dev/mcpPayloads';
|
|
10
11
|
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
11
12
|
import { compactList, printAgentResponse, printJson, quoteCommandArgument } from '../utils/agentOutput';
|
|
12
13
|
|
|
@@ -125,6 +126,7 @@ const renderHuman = (response: TOrientResponse) =>
|
|
|
125
126
|
...(response.app.routerPort ? [`- routerPort=${response.app.routerPort}`] : []),
|
|
126
127
|
'Guidance',
|
|
127
128
|
`- agents=${response.guidance.agents}`,
|
|
129
|
+
`- documentation=${response.guidance.documentation}`,
|
|
128
130
|
`- diagnostics=${response.guidance.diagnostics}`,
|
|
129
131
|
`- optimizations=${response.guidance.optimizations}`,
|
|
130
132
|
`- codingStyle=${response.guidance.codingStyle}`,
|
|
@@ -163,23 +165,48 @@ const compactOwnerMatch = (match: TOrientResponse['owner']['matches'][number]) =
|
|
|
163
165
|
source: match.source,
|
|
164
166
|
});
|
|
165
167
|
|
|
166
|
-
const buildInstructionPlan = (response: TOrientResponse) =>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
168
|
+
const buildInstructionPlan = (response: TOrientResponse) => {
|
|
169
|
+
const triggered = resolveTriggeredInstructionReads({
|
|
170
|
+
codingStyle: response.guidance.codingStyle,
|
|
171
|
+
diagnostics: response.guidance.diagnostics,
|
|
172
|
+
documentation: response.guidance.documentation,
|
|
173
|
+
optimizations: response.guidance.optimizations,
|
|
174
|
+
query: response.normalizedQuery || response.query,
|
|
175
|
+
rootAgentsFile:
|
|
176
|
+
response.app.repoRoot !== response.app.appRoot
|
|
177
|
+
? path.join(response.app.repoRoot, 'AGENTS.md')
|
|
178
|
+
: response.guidance.agents,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
mustRead: [
|
|
183
|
+
...new Set([
|
|
184
|
+
response.guidance.agents,
|
|
185
|
+
...response.guidance.areaAgents,
|
|
186
|
+
...triggered.map((entry) => entry.file),
|
|
187
|
+
]),
|
|
188
|
+
],
|
|
189
|
+
triggered,
|
|
190
|
+
readWhen: [
|
|
191
|
+
{
|
|
192
|
+
file: response.guidance.documentation,
|
|
193
|
+
when: 'Read before non-trivial coding tasks to choose the smallest `/docs` pack and update docs after changes.',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
file: response.guidance.diagnostics,
|
|
197
|
+
when: 'Read only for raw errors, failing requests, traces, perf regressions, or reproduction work.',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
file: response.guidance.codingStyle,
|
|
201
|
+
when: 'Read before editing implementation files.',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
file: response.guidance.optimizations,
|
|
205
|
+
when: 'Read after client-side implementation or when the task explicitly concerns packages, build, runtime, or performance.',
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
};
|
|
183
210
|
|
|
184
211
|
const printCompactOrient = (response: TOrientResponse) => {
|
|
185
212
|
const topOwner = response.owner.matches[0];
|
package/cli/commands/runtime.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { UsageError } from 'clipanion';
|
|
|
5
5
|
|
|
6
6
|
import cli from '..';
|
|
7
7
|
import { readProteumManifest } from '../compiler/common/proteumManifest';
|
|
8
|
-
import { listDevSessionInspections, type TDevSessionInspection } from '../runtime/devSessions';
|
|
8
|
+
import { listDevSessionInspections, writeMachineDevSessionRecord, type TDevSessionInspection } from '../runtime/devSessions';
|
|
9
|
+
import { inspectDevPort, type TDevPortInspection } from '../runtime/ports';
|
|
9
10
|
import { printAgentResponse, printJson, quoteCommandArgument } from '../utils/agentOutput';
|
|
10
11
|
import type { TDoctorResponse } from '@common/dev/diagnostics';
|
|
11
12
|
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
@@ -40,6 +41,11 @@ const getSessionUrl = (inspection: TDevSessionInspection) => {
|
|
|
40
41
|
return `http://localhost:${inspection.record.routerPort}`;
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
const getSessionMcpUrl = (inspection: TDevSessionInspection) => {
|
|
45
|
+
const sessionUrl = getSessionUrl(inspection);
|
|
46
|
+
return sessionUrl ? `${sessionUrl}/__proteum/mcp` : '';
|
|
47
|
+
};
|
|
48
|
+
|
|
43
49
|
const probeDoctor = async (baseUrl: string) => {
|
|
44
50
|
if (!baseUrl) return { reachable: false, error: 'No dev URL is registered.' };
|
|
45
51
|
|
|
@@ -78,11 +84,90 @@ const compactSession = (inspection: TDevSessionInspection) => ({
|
|
|
78
84
|
pid: inspection.record?.pid,
|
|
79
85
|
routerPort: inspection.record?.routerPort,
|
|
80
86
|
publicUrl: inspection.record?.publicUrl,
|
|
87
|
+
mcpUrl: inspection.record ? getSessionMcpUrl(inspection) : undefined,
|
|
81
88
|
state: inspection.record?.state,
|
|
82
89
|
startedAt: inspection.record?.startedAt,
|
|
83
90
|
updatedAt: inspection.record?.updatedAt,
|
|
84
91
|
});
|
|
85
92
|
|
|
93
|
+
const createStartDevCommand = (port?: number) =>
|
|
94
|
+
`proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port ${port || '<free-port>'}`;
|
|
95
|
+
|
|
96
|
+
const describePortOwner = (portInspection?: TDevPortInspection) => {
|
|
97
|
+
if (!portInspection || portInspection.router.available) return '';
|
|
98
|
+
if (portInspection.router.proteum) {
|
|
99
|
+
const appLabel =
|
|
100
|
+
portInspection.router.app?.identifier ||
|
|
101
|
+
portInspection.router.app?.name ||
|
|
102
|
+
portInspection.router.app?.appRoot ||
|
|
103
|
+
'another Proteum app';
|
|
104
|
+
return `Configured router port ${portInspection.router.port} is already occupied by ${appLabel}.`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return `Configured router port ${portInspection.router.port} is already occupied by a non-Proteum or unrecognized process.`;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const getNextActions = ({
|
|
111
|
+
health,
|
|
112
|
+
portInspection,
|
|
113
|
+
selectedSession,
|
|
114
|
+
}: {
|
|
115
|
+
health: { reachable: boolean };
|
|
116
|
+
portInspection?: TDevPortInspection;
|
|
117
|
+
selectedSession: TDevSessionInspection | undefined;
|
|
118
|
+
}) => {
|
|
119
|
+
if (!selectedSession?.record || !selectedSession.live) {
|
|
120
|
+
const portOwner = describePortOwner(portInspection);
|
|
121
|
+
|
|
122
|
+
if (portInspection?.router.proteum && portInspection.router.matchesApp) {
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
label: 'Use Existing Runtime',
|
|
126
|
+
command: `proteum diagnose ${quoteCommandArgument('/')} --port ${portInspection.router.port}`,
|
|
127
|
+
reason:
|
|
128
|
+
'A Proteum runtime for this app already responds on the configured router port, but no tracked session file is live. Do not start a second dev server; use this port for CLI evidence or stop the owning process before starting a tracked session.',
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const startPort =
|
|
134
|
+
portInspection && !portInspection.canStartOnConfiguredPort ? portInspection.recommendedPort : portInspection?.router.port;
|
|
135
|
+
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
label: 'Start Dev',
|
|
139
|
+
command: createStartDevCommand(startPort),
|
|
140
|
+
reason: portOwner
|
|
141
|
+
? `${portOwner} Use an alternate free router/HMR port pair; do not probe page bodies to identify port owners.`
|
|
142
|
+
: 'Create a tracked dev session before request-time diagnostics.',
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!health.reachable) {
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
label: 'Stop Unreachable Dev',
|
|
151
|
+
command: `proteum dev stop --session-file ${quoteCommandArgument(selectedSession.sessionFilePath)}`,
|
|
152
|
+
reason: 'A tracked session exists but the runtime and MCP endpoint are unreachable.',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
label: 'Start Dev',
|
|
156
|
+
command: createStartDevCommand(portInspection?.recommendedPort),
|
|
157
|
+
reason: 'Start a fresh tracked session after stopping the unreachable one.',
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
label: 'Diagnose Root',
|
|
165
|
+
command: `proteum diagnose ${quoteCommandArgument('/')} --port ${selectedSession.record.routerPort}`,
|
|
166
|
+
reason: 'Use the selected runtime for the smallest request-level diagnostic pass.',
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
};
|
|
170
|
+
|
|
86
171
|
export const run = async () => {
|
|
87
172
|
const action = getAction();
|
|
88
173
|
if (action !== 'status') return;
|
|
@@ -93,10 +178,21 @@ export const run = async () => {
|
|
|
93
178
|
sessionFilePath: typeof cli.args.sessionFile === 'string' && cli.args.sessionFile ? cli.args.sessionFile : undefined,
|
|
94
179
|
});
|
|
95
180
|
const liveSessions = sessions.filter((inspection) => inspection.live && inspection.record);
|
|
181
|
+
await Promise.allSettled(
|
|
182
|
+
liveSessions.map((inspection) =>
|
|
183
|
+
inspection.record ? writeMachineDevSessionRecord(inspection.record) : Promise.resolve(undefined),
|
|
184
|
+
),
|
|
185
|
+
);
|
|
96
186
|
const selectedSession =
|
|
97
187
|
liveSessions.find((inspection) => inspection.record?.state === 'ready') || liveSessions[0] || sessions.find((inspection) => inspection.record);
|
|
98
188
|
const selectedBaseUrl = selectedSession ? getSessionUrl(selectedSession) : '';
|
|
99
189
|
const health = selectedSession && selectedSession.live ? await probeDoctor(selectedBaseUrl) : { reachable: false, error: 'No live tracked dev session.' };
|
|
190
|
+
const configuredDevPort = manifest
|
|
191
|
+
? await inspectDevPort({
|
|
192
|
+
appRoot: cli.paths.appRoot,
|
|
193
|
+
port: manifest.env.resolved.routerPort,
|
|
194
|
+
})
|
|
195
|
+
: undefined;
|
|
100
196
|
|
|
101
197
|
const payload = {
|
|
102
198
|
appRoot: cli.paths.appRoot,
|
|
@@ -119,6 +215,7 @@ export const run = async () => {
|
|
|
119
215
|
selected: selectedSession ? compactSession(selectedSession) : undefined,
|
|
120
216
|
sessions: sessions.map(compactSession),
|
|
121
217
|
health,
|
|
218
|
+
configuredDevPort,
|
|
122
219
|
};
|
|
123
220
|
|
|
124
221
|
if (cli.args.full === true) {
|
|
@@ -129,23 +226,9 @@ export const run = async () => {
|
|
|
129
226
|
printAgentResponse({
|
|
130
227
|
summary: selectedSession
|
|
131
228
|
? `${selectedSession.live ? 'live' : 'stale'} dev session on ${selectedSession.record?.routerPort || 'unknown port'}; health=${health.reachable ? 'reachable' : 'unreachable'}`
|
|
132
|
-
: 'No tracked Proteum dev session found.',
|
|
229
|
+
: describePortOwner(configuredDevPort) || 'No tracked Proteum dev session found.',
|
|
133
230
|
data: payload,
|
|
134
|
-
nextActions: selectedSession
|
|
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
|
-
],
|
|
231
|
+
nextActions: getNextActions({ health, portInspection: configuredDevPort, selectedSession }),
|
|
149
232
|
fullDetailCommand: 'proteum runtime status --full',
|
|
150
233
|
});
|
|
151
234
|
};
|