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.
Files changed (56) hide show
  1. package/AGENTS.md +8 -3
  2. package/README.md +20 -15
  3. package/agents/project/AGENTS.md +16 -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 +10 -9
  7. package/agents/project/optimizations.md +1 -1
  8. package/agents/project/root/AGENTS.md +15 -8
  9. package/agents/project/server/services/AGENTS.md +1 -0
  10. package/agents/project/tests/AGENTS.md +1 -0
  11. package/cli/commands/db.ts +160 -0
  12. package/cli/commands/dev.ts +148 -25
  13. package/cli/commands/diagnose.ts +2 -0
  14. package/cli/commands/explain.ts +38 -9
  15. package/cli/commands/mcp.ts +126 -9
  16. package/cli/commands/orient.ts +44 -17
  17. package/cli/commands/runtime.ts +100 -17
  18. package/cli/mcp/router.ts +1028 -0
  19. package/cli/presentation/commands.ts +56 -25
  20. package/cli/presentation/help.ts +1 -1
  21. package/cli/runtime/commands.ts +163 -21
  22. package/cli/runtime/devSessions.ts +328 -2
  23. package/cli/runtime/mcpDaemon.ts +288 -0
  24. package/cli/runtime/ports.ts +151 -0
  25. package/cli/utils/agents.ts +94 -17
  26. package/cli/utils/appRoots.ts +232 -0
  27. package/common/dev/database.ts +226 -0
  28. package/common/dev/diagnostics.ts +1 -1
  29. package/common/dev/inspection.ts +8 -1
  30. package/common/dev/mcpPayloads.ts +456 -17
  31. package/common/dev/mcpServer.ts +51 -0
  32. package/docs/agent-routing.md +32 -21
  33. package/docs/dev-commands.md +1 -1
  34. package/docs/dev-sessions.md +3 -1
  35. package/docs/diagnostics.md +21 -20
  36. package/docs/mcp.md +114 -50
  37. package/docs/migrate-from-2.1.3.md +3 -5
  38. package/docs/request-tracing.md +3 -3
  39. package/package.json +10 -3
  40. package/server/app/devDiagnostics.ts +92 -0
  41. package/server/app/devMcp.ts +55 -0
  42. package/server/services/prisma/mariadb.ts +7 -3
  43. package/server/services/router/http/index.ts +25 -0
  44. package/server/services/router/request/ip.test.cjs +0 -1
  45. package/tests/agents-utils.test.cjs +58 -3
  46. package/tests/cli-mcp-command.test.cjs +327 -0
  47. package/tests/codex-mcp-usage.test.cjs +307 -0
  48. package/tests/dev-sessions.test.cjs +113 -0
  49. package/tests/dev-transpile-watch.test.cjs +0 -1
  50. package/tests/eslint-rules.test.cjs +0 -1
  51. package/tests/inspection.test.cjs +0 -1
  52. package/tests/mcp.test.cjs +769 -2
  53. package/tests/router-cache-config.test.cjs +0 -1
  54. package/vitest.config.mjs +9 -0
  55. package/cli/mcp/provider.ts +0 -365
  56. package/cli/mcp/stdio.ts +0 -16
@@ -0,0 +1,226 @@
1
+ export type TDatabaseReadQueryKind = 'explain' | 'select' | 'show';
2
+
3
+ export type TDatabaseReadQueryInput = {
4
+ limit?: number;
5
+ sql: string;
6
+ timeoutMs?: number;
7
+ };
8
+
9
+ export type TDatabaseReadQueryColumn = {
10
+ name: string;
11
+ table?: string;
12
+ type?: number | string;
13
+ };
14
+
15
+ export type TDatabaseReadQueryValue = boolean | number | string | null;
16
+
17
+ export type TDatabaseReadQueryRow = Record<string, TDatabaseReadQueryValue>;
18
+
19
+ export type TDatabaseReadQueryResponse = {
20
+ columns: TDatabaseReadQueryColumn[];
21
+ elapsedMs: number;
22
+ kind: TDatabaseReadQueryKind;
23
+ limit: number;
24
+ limited: boolean;
25
+ rowCount: number;
26
+ rows: TDatabaseReadQueryRow[];
27
+ sql: string;
28
+ };
29
+
30
+ export type TValidatedDatabaseReadQuery = {
31
+ kind: TDatabaseReadQueryKind;
32
+ sql: string;
33
+ };
34
+
35
+ export const defaultDatabaseReadLimit = 50;
36
+ export const maxDatabaseReadLimit = 500;
37
+ export const defaultDatabaseReadTimeoutMs = 5_000;
38
+ export const maxDatabaseReadTimeoutMs = 30_000;
39
+
40
+ const allowedQueryKinds = new Set<TDatabaseReadQueryKind>(['explain', 'select', 'show']);
41
+ const sqlKeywordPattern = /^[A-Za-z]+/;
42
+ const sqlCommentPattern = /\/\*[\s\S]*?\*\//g;
43
+ const sqlLineCommentPattern = /(?:^|\n)\s*(?:--|#).*?(?=\n|$)/g;
44
+
45
+ const clampInteger = ({ fallback, max, min, value }: { fallback: number; max: number; min: number; value?: number }) => {
46
+ if (value === undefined || !Number.isInteger(value) || value < min) return fallback;
47
+
48
+ return Math.min(value, max);
49
+ };
50
+
51
+ export const normalizeDatabaseReadLimit = (limit?: number) =>
52
+ clampInteger({
53
+ fallback: defaultDatabaseReadLimit,
54
+ max: maxDatabaseReadLimit,
55
+ min: 1,
56
+ value: limit,
57
+ });
58
+
59
+ export const normalizeDatabaseReadTimeoutMs = (timeoutMs?: number) =>
60
+ clampInteger({
61
+ fallback: defaultDatabaseReadTimeoutMs,
62
+ max: maxDatabaseReadTimeoutMs,
63
+ min: 100,
64
+ value: timeoutMs,
65
+ });
66
+
67
+ const skipLeadingTrivia = (sql: string) => {
68
+ let index = 0;
69
+
70
+ while (index < sql.length) {
71
+ const char = sql[index];
72
+ const next = sql[index + 1];
73
+
74
+ if (/\s/.test(char)) {
75
+ index += 1;
76
+ continue;
77
+ }
78
+
79
+ if (char === '-' && next === '-') {
80
+ index = sql.indexOf('\n', index + 2);
81
+ if (index === -1) return sql.length;
82
+ continue;
83
+ }
84
+
85
+ if (char === '#') {
86
+ index = sql.indexOf('\n', index + 1);
87
+ if (index === -1) return sql.length;
88
+ continue;
89
+ }
90
+
91
+ if (char === '/' && next === '*') {
92
+ const end = sql.indexOf('*/', index + 2);
93
+ if (end === -1) throw new Error('SQL contains an unterminated block comment.');
94
+ index = end + 2;
95
+ continue;
96
+ }
97
+
98
+ return index;
99
+ }
100
+
101
+ return index;
102
+ };
103
+
104
+ const stripSqlComments = (sql: string) =>
105
+ sql.replace(sqlCommentPattern, ' ').replace(sqlLineCommentPattern, '\n');
106
+
107
+ const findFirstStatementEnd = (sql: string) => {
108
+ let quote: "'" | '"' | '`' | undefined;
109
+ let lineComment = false;
110
+ let blockComment = false;
111
+
112
+ for (let index = 0; index < sql.length; index += 1) {
113
+ const char = sql[index];
114
+ const next = sql[index + 1];
115
+
116
+ if (lineComment) {
117
+ if (char === '\n') lineComment = false;
118
+ continue;
119
+ }
120
+
121
+ if (blockComment) {
122
+ if (char === '*' && next === '/') {
123
+ blockComment = false;
124
+ index += 1;
125
+ }
126
+ continue;
127
+ }
128
+
129
+ if (quote) {
130
+ if (char === '\\') {
131
+ index += 1;
132
+ continue;
133
+ }
134
+ if (char === quote) quote = undefined;
135
+ continue;
136
+ }
137
+
138
+ if (char === '-' && next === '-') {
139
+ lineComment = true;
140
+ index += 1;
141
+ continue;
142
+ }
143
+
144
+ if (char === '#') {
145
+ lineComment = true;
146
+ continue;
147
+ }
148
+
149
+ if (char === '/' && next === '*') {
150
+ blockComment = true;
151
+ index += 1;
152
+ continue;
153
+ }
154
+
155
+ if (char === '\'' || char === '"' || char === '`') {
156
+ quote = char;
157
+ continue;
158
+ }
159
+
160
+ if (char === ';') return index;
161
+ }
162
+
163
+ if (quote) throw new Error('SQL contains an unterminated quoted string.');
164
+ if (blockComment) throw new Error('SQL contains an unterminated block comment.');
165
+
166
+ return -1;
167
+ };
168
+
169
+ const assertSingleStatement = (sql: string) => {
170
+ const end = findFirstStatementEnd(sql);
171
+ if (end === -1) return sql.trim();
172
+
173
+ const first = sql.slice(0, end).trim();
174
+ const rest = stripSqlComments(sql.slice(end + 1)).trim();
175
+ if (rest) throw new Error('Only one read-only SQL statement may be executed.');
176
+
177
+ return first;
178
+ };
179
+
180
+ const getReadQueryKind = (sql: string): TDatabaseReadQueryKind => {
181
+ const start = skipLeadingTrivia(sql);
182
+ const keyword = sql.slice(start).match(sqlKeywordPattern)?.[0]?.toLowerCase();
183
+
184
+ if (!keyword || !allowedQueryKinds.has(keyword as TDatabaseReadQueryKind)) {
185
+ throw new Error('Only SELECT, SHOW, and EXPLAIN SQL statements are allowed.');
186
+ }
187
+
188
+ return keyword as TDatabaseReadQueryKind;
189
+ };
190
+
191
+ const assertAllowedReadQueryShape = (sql: string) => {
192
+ const normalized = stripSqlComments(sql).replace(/\s+/g, ' ').trim().toLowerCase();
193
+
194
+ if (/\bexplain\s+analyze\b/.test(normalized)) {
195
+ throw new Error('EXPLAIN ANALYZE is not allowed because it executes the target query.');
196
+ }
197
+
198
+ if (/\binto\s+(?:out|dump)file\b/.test(normalized)) {
199
+ throw new Error('SELECT INTO OUTFILE and SELECT INTO DUMPFILE are not allowed.');
200
+ }
201
+
202
+ if (/\bload_file\s*\(/.test(normalized)) {
203
+ throw new Error('LOAD_FILE is not allowed in database diagnostics.');
204
+ }
205
+
206
+ if (/\bfor\s+update\b/.test(normalized) || /\block\s+in\s+share\s+mode\b/.test(normalized)) {
207
+ throw new Error('Locking read statements are not allowed in database diagnostics.');
208
+ }
209
+
210
+ if (/\b(?:sleep|benchmark)\s*\(/.test(normalized)) {
211
+ throw new Error('Sleep and benchmark functions are not allowed in database diagnostics.');
212
+ }
213
+ };
214
+
215
+ export const validateDatabaseReadQuery = (rawSql: string): TValidatedDatabaseReadQuery => {
216
+ const normalizedSql = rawSql.replace(/^\uFEFF/, '').trim();
217
+ if (!normalizedSql) throw new Error('SQL query is required.');
218
+ if (normalizedSql.length > 20_000) throw new Error('SQL query is too long for database diagnostics.');
219
+
220
+ const sql = assertSingleStatement(normalizedSql);
221
+ const kind = getReadQueryKind(sql);
222
+
223
+ assertAllowedReadQueryShape(sql);
224
+
225
+ return { kind, sql };
226
+ };
@@ -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,