proteum 2.2.9 → 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.
Files changed (59) hide show
  1. package/AGENTS.md +10 -4
  2. package/README.md +58 -15
  3. package/agents/project/AGENTS.md +53 -10
  4. package/agents/project/DOCUMENTATION.md +1326 -0
  5. package/agents/project/app-root/AGENTS.md +2 -2
  6. package/agents/project/diagnostics.md +12 -7
  7. package/agents/project/optimizations.md +1 -0
  8. package/agents/project/root/AGENTS.md +24 -9
  9. package/agents/project/tests/AGENTS.md +7 -0
  10. package/agents/project/tests/e2e/AGENTS.md +13 -0
  11. package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
  12. package/cli/commands/connect.ts +40 -4
  13. package/cli/commands/dev.ts +148 -25
  14. package/cli/commands/diagnose.ts +138 -5
  15. package/cli/commands/doctor.ts +24 -4
  16. package/cli/commands/explain.ts +134 -6
  17. package/cli/commands/mcp.ts +133 -0
  18. package/cli/commands/orient.ts +93 -3
  19. package/cli/commands/perf.ts +118 -13
  20. package/cli/commands/runtime.ts +234 -0
  21. package/cli/commands/trace.ts +116 -21
  22. package/cli/mcp/router.ts +1010 -0
  23. package/cli/presentation/commands.ts +93 -26
  24. package/cli/presentation/devSession.ts +2 -0
  25. package/cli/presentation/help.ts +1 -1
  26. package/cli/runtime/commands.ts +215 -24
  27. package/cli/runtime/devSessions.ts +328 -2
  28. package/cli/runtime/mcpDaemon.ts +288 -0
  29. package/cli/runtime/ports.ts +151 -0
  30. package/cli/utils/agentOutput.ts +46 -0
  31. package/cli/utils/agents.ts +194 -51
  32. package/cli/utils/appRoots.ts +232 -0
  33. package/common/dev/diagnostics.ts +1 -1
  34. package/common/dev/inspection.ts +22 -7
  35. package/common/dev/mcpPayloads.ts +1150 -0
  36. package/common/dev/mcpServer.ts +287 -0
  37. package/docs/agent-routing.md +137 -0
  38. package/docs/dev-commands.md +2 -0
  39. package/docs/dev-sessions.md +4 -1
  40. package/docs/diagnostics.md +70 -24
  41. package/docs/mcp.md +206 -0
  42. package/docs/migrate-from-2.1.3.md +14 -6
  43. package/docs/request-tracing.md +12 -6
  44. package/package.json +11 -3
  45. package/server/app/devMcp.ts +204 -0
  46. package/server/services/router/http/cache.ts +116 -0
  47. package/server/services/router/http/index.ts +94 -35
  48. package/server/services/router/index.ts +8 -11
  49. package/server/services/router/request/ip.test.cjs +0 -1
  50. package/tests/agents-utils.test.cjs +92 -14
  51. package/tests/cli-mcp-command.test.cjs +262 -0
  52. package/tests/codex-mcp-usage.test.cjs +307 -0
  53. package/tests/dev-sessions.test.cjs +113 -0
  54. package/tests/dev-transpile-watch.test.cjs +117 -9
  55. package/tests/eslint-rules.test.cjs +0 -1
  56. package/tests/inspection.test.cjs +66 -0
  57. package/tests/mcp.test.cjs +873 -0
  58. package/tests/router-cache-config.test.cjs +73 -0
  59. package/vitest.config.mjs +9 -0
@@ -0,0 +1,232 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import type { Dirent } from 'fs';
4
+
5
+ import { readProteumManifest, type TProteumManifest } from '../compiler/common/proteumManifest';
6
+
7
+ export type TProteumAppRootSummary = {
8
+ appRoot: string;
9
+ hasManifest: boolean;
10
+ manifest?: {
11
+ counts: {
12
+ connectedProjects: number;
13
+ controllers: number;
14
+ routes: number;
15
+ };
16
+ diagnostics: {
17
+ errors: number;
18
+ warnings: number;
19
+ };
20
+ identifier: string;
21
+ name: string;
22
+ routerPort: number;
23
+ };
24
+ manifestError?: string;
25
+ packageManager: 'npm' | 'pnpm' | 'yarn' | 'unknown';
26
+ relativeAppRoot?: string;
27
+ };
28
+
29
+ const proteumAppRootRequiredEntries = ['package.json', 'identity.config.ts', 'proteum.config.ts', 'client', 'server'];
30
+ const ignoredSearchDirectories = new Set([
31
+ '.cache',
32
+ '.git',
33
+ '.proteum',
34
+ 'bin',
35
+ 'coverage',
36
+ 'dev',
37
+ 'node_modules',
38
+ 'playwright-report',
39
+ 'test-results',
40
+ 'var',
41
+ ]);
42
+
43
+ const resolveExistingPath = (value: string) => {
44
+ const resolved = path.resolve(value);
45
+
46
+ try {
47
+ return fs.realpathSync(resolved);
48
+ } catch {
49
+ return resolved;
50
+ }
51
+ };
52
+
53
+ const pathEntryExists = (filepath: string) => {
54
+ try {
55
+ fs.lstatSync(filepath);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ };
61
+
62
+ const isDirectory = (filepath: string) => {
63
+ try {
64
+ return fs.statSync(filepath).isDirectory();
65
+ } catch {
66
+ return false;
67
+ }
68
+ };
69
+
70
+ const resolveSearchRoot = (value: string) => {
71
+ const resolved = resolveExistingPath(value);
72
+ if (isDirectory(resolved)) return resolved;
73
+ return path.dirname(resolved);
74
+ };
75
+
76
+ export const isProteumAppRoot = (workdir: string) =>
77
+ proteumAppRootRequiredEntries.every((entry) => pathEntryExists(path.join(workdir, entry)));
78
+
79
+ export const findNearestProteumAppRoot = (startPath: string) => {
80
+ let currentPath = resolveSearchRoot(startPath);
81
+
82
+ while (true) {
83
+ if (isProteumAppRoot(currentPath)) return currentPath;
84
+
85
+ const parentPath = path.dirname(currentPath);
86
+ if (parentPath === currentPath) return undefined;
87
+ currentPath = parentPath;
88
+ }
89
+ };
90
+
91
+ export const findProteumAppRootsUnder = (root: string, { maxDepth = 5 }: { maxDepth?: number } = {}) => {
92
+ const searchRoot = resolveSearchRoot(root);
93
+ const appRoots: string[] = [];
94
+ const seen = new Set<string>();
95
+
96
+ const visit = (directory: string, depth: number) => {
97
+ const canonicalDirectory = resolveExistingPath(directory);
98
+ if (seen.has(canonicalDirectory)) return;
99
+ seen.add(canonicalDirectory);
100
+
101
+ if (isProteumAppRoot(canonicalDirectory)) {
102
+ appRoots.push(canonicalDirectory);
103
+ return;
104
+ }
105
+
106
+ if (depth >= maxDepth) return;
107
+
108
+ let entries: Dirent[];
109
+ try {
110
+ entries = fs.readdirSync(canonicalDirectory, { withFileTypes: true });
111
+ } catch {
112
+ return;
113
+ }
114
+
115
+ for (const entry of entries) {
116
+ if (!entry.isDirectory()) continue;
117
+ if (ignoredSearchDirectories.has(entry.name)) continue;
118
+ visit(path.join(canonicalDirectory, entry.name), depth + 1);
119
+ }
120
+ };
121
+
122
+ visit(searchRoot, 0);
123
+
124
+ return appRoots.sort((left, right) => left.localeCompare(right));
125
+ };
126
+
127
+ const findPackageManager = (appRoot: string): TProteumAppRootSummary['packageManager'] => {
128
+ let currentPath = path.resolve(appRoot);
129
+
130
+ while (true) {
131
+ if (pathEntryExists(path.join(currentPath, 'package-lock.json'))) return 'npm';
132
+ if (pathEntryExists(path.join(currentPath, 'pnpm-lock.yaml'))) return 'pnpm';
133
+ if (pathEntryExists(path.join(currentPath, 'yarn.lock'))) return 'yarn';
134
+
135
+ if (pathEntryExists(path.join(currentPath, '.git'))) return 'unknown';
136
+
137
+ const parentPath = path.dirname(currentPath);
138
+ if (parentPath === currentPath) return 'unknown';
139
+ currentPath = parentPath;
140
+ }
141
+ };
142
+
143
+ const summarizeManifest = (manifest: TProteumManifest): NonNullable<TProteumAppRootSummary['manifest']> => {
144
+ const errors = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length;
145
+ const warnings = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length;
146
+
147
+ return {
148
+ counts: {
149
+ connectedProjects: manifest.connectedProjects.length,
150
+ controllers: manifest.controllers.length,
151
+ routes: manifest.routes.client.length + manifest.routes.server.length,
152
+ },
153
+ diagnostics: { errors, warnings },
154
+ identifier: manifest.app.identity.identifier,
155
+ name: manifest.app.identity.name,
156
+ routerPort: manifest.env.resolved.routerPort,
157
+ };
158
+ };
159
+
160
+ export const readProteumAppRootSummary = (appRoot: string, baseRoot?: string): TProteumAppRootSummary => {
161
+ const normalizedAppRoot = resolveExistingPath(appRoot);
162
+ const relativeAppRoot = baseRoot ? path.relative(resolveExistingPath(baseRoot), normalizedAppRoot) || '.' : undefined;
163
+ const summary: TProteumAppRootSummary = {
164
+ appRoot: normalizedAppRoot,
165
+ hasManifest: false,
166
+ packageManager: findPackageManager(normalizedAppRoot),
167
+ relativeAppRoot,
168
+ };
169
+
170
+ try {
171
+ const manifest = readProteumManifest(normalizedAppRoot);
172
+ summary.hasManifest = true;
173
+ summary.manifest = summarizeManifest(manifest);
174
+ } catch (error) {
175
+ summary.manifestError = error instanceof Error ? error.message : String(error);
176
+ }
177
+
178
+ return summary;
179
+ };
180
+
181
+ export const resolveProteumAppRootContext = (cwd: string) => {
182
+ const normalizedCwd = resolveSearchRoot(cwd);
183
+ const nearestAppRoot = findNearestProteumAppRoot(normalizedCwd);
184
+ const appRoots = nearestAppRoot ? [nearestAppRoot] : findProteumAppRootsUnder(normalizedCwd);
185
+
186
+ return {
187
+ cwd: normalizedCwd,
188
+ isAppRoot: nearestAppRoot === normalizedCwd,
189
+ isWrapper: !nearestAppRoot && appRoots.length > 0,
190
+ nearestAppRoot,
191
+ appRoots,
192
+ appCandidates: appRoots.map((appRoot) => readProteumAppRootSummary(appRoot, normalizedCwd)),
193
+ };
194
+ };
195
+
196
+ export const quoteShellPath = (value: string) => JSON.stringify(value);
197
+
198
+ const createAppScopedCommand = ({
199
+ appRoot,
200
+ baseRoot,
201
+ command,
202
+ }: {
203
+ appRoot: string;
204
+ baseRoot?: string;
205
+ command: string;
206
+ }) => {
207
+ const relativeAppRoot = baseRoot ? path.relative(resolveExistingPath(baseRoot), resolveExistingPath(appRoot)) || '.' : '';
208
+
209
+ if (!relativeAppRoot || relativeAppRoot === '.') return command;
210
+ return `cd ${quoteShellPath(relativeAppRoot)} && ${command}`;
211
+ };
212
+
213
+ export const createStartDevCommand = ({
214
+ appRoot,
215
+ baseRoot,
216
+ port,
217
+ }: {
218
+ appRoot: string;
219
+ baseRoot?: string;
220
+ port?: number;
221
+ }) => {
222
+ const command = `npx proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port ${port || '<free-port>'}`;
223
+
224
+ return createAppScopedCommand({ appRoot, baseRoot, command });
225
+ };
226
+
227
+ export const createRuntimeStatusCommand = ({ appRoot, baseRoot }: { appRoot: string; baseRoot?: string }) =>
228
+ createAppScopedCommand({
229
+ appRoot,
230
+ baseRoot,
231
+ command: 'npx proteum runtime status',
232
+ });
@@ -125,7 +125,7 @@ export const buildExplainSummaryItems = (manifest: TProteumManifest) => {
125
125
  `Routes: ${manifest.routes.client.length} client, ${manifest.routes.server.length} server`,
126
126
  `Layouts: ${manifest.layouts.length}`,
127
127
  `Diagnostics: ${errorsCount} errors, ${warningsCount} warnings`,
128
- 'Use `proteum explain --json` for the full machine-readable manifest or pass section flags like `routes` and `services`.',
128
+ 'Use `proteum explain --manifest` for the full manifest or pass section flags with `--full` when raw arrays are required.',
129
129
  ];
130
130
  };
131
131
 
@@ -61,6 +61,7 @@ export type TTraceAttributionResponse = {
61
61
 
62
62
  export type TOrientGuidance = {
63
63
  agents: string;
64
+ documentation: string;
64
65
  diagnostics: string;
65
66
  optimizations: string;
66
67
  codingStyle: string;
@@ -791,6 +792,11 @@ const resolveGuidance = ({
791
792
  fallbackFilepath: joinPath(fallbackRoot, 'diagnostics.md'),
792
793
  relativePath: 'diagnostics.md',
793
794
  });
795
+ const documentation = resolveGuidanceFile({
796
+ appRoot: manifest.app.root,
797
+ fallbackFilepath: joinPath(fallbackRoot, 'DOCUMENTATION.md'),
798
+ relativePath: 'DOCUMENTATION.md',
799
+ });
794
800
  const optimizations = resolveGuidanceFile({
795
801
  appRoot: manifest.app.root,
796
802
  fallbackFilepath: joinPath(fallbackRoot, 'optimizations.md'),
@@ -802,13 +808,14 @@ const resolveGuidance = ({
802
808
  relativePath: 'CODING_STYLE.md',
803
809
  });
804
810
 
805
- for (const warning of [agents.warning, diagnostics.warning, optimizations.warning, codingStyle.warning]) {
811
+ for (const warning of [agents.warning, documentation.warning, diagnostics.warning, optimizations.warning, codingStyle.warning]) {
806
812
  if (warning) warnings.push(warning);
807
813
  }
808
814
 
809
815
  return {
810
816
  guidance: {
811
817
  agents: agents.filepath,
818
+ documentation: documentation.filepath,
812
819
  diagnostics: diagnostics.filepath,
813
820
  optimizations: optimizations.filepath,
814
821
  codingStyle: codingStyle.filepath,
@@ -1156,12 +1163,20 @@ export const explainOwner = (manifest: TProteumManifest, query: string): TExplai
1156
1163
  const normalizedQuery = normalizeText(query);
1157
1164
  if (!normalizedQuery) return { matches: [], normalizedQuery, query };
1158
1165
 
1159
- const matches = buildManifestEntries(manifest)
1160
- .map((entry) => {
1161
- const { score, matchedOn } = scoreOwnerMatch(query, entry);
1162
- return score > 0 ? toOwnerMatch(entry, score, matchedOn) : undefined;
1163
- })
1164
- .filter((match): match is TExplainOwnerMatch => match !== undefined)
1166
+ const entries = buildManifestEntries(manifest);
1167
+ const scoredMatches =
1168
+ normalizedQuery === '/'
1169
+ ? entries
1170
+ .filter((entry) => (entry.kind === 'route' || entry.kind === 'controller') && normalizeText(entry.label) === '/')
1171
+ .map((entry) => toOwnerMatch(entry, 200, ['/']))
1172
+ : entries
1173
+ .map((entry) => {
1174
+ const { score, matchedOn } = scoreOwnerMatch(query, entry);
1175
+ return score > 0 ? toOwnerMatch(entry, score, matchedOn) : undefined;
1176
+ })
1177
+ .filter((match): match is TExplainOwnerMatch => match !== undefined);
1178
+
1179
+ const matches = scoredMatches
1165
1180
  .sort((left, right) => right.score - left.score || left.kind.localeCompare(right.kind) || left.label.localeCompare(right.label))
1166
1181
  .slice(0, 12);
1167
1182