@utopia-ai/cli 0.1.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/.claude/settings.json +1 -0
- package/.claude/settings.local.json +38 -0
- package/bin/utopia.js +20 -0
- package/package.json +46 -0
- package/python/README.md +34 -0
- package/python/instrumenter/instrument.py +1148 -0
- package/python/pyproject.toml +32 -0
- package/python/setup.py +27 -0
- package/python/utopia_runtime/__init__.py +30 -0
- package/python/utopia_runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/utopia_runtime/__pycache__/client.cpython-313.pyc +0 -0
- package/python/utopia_runtime/__pycache__/probe.cpython-313.pyc +0 -0
- package/python/utopia_runtime/client.py +31 -0
- package/python/utopia_runtime/probe.py +446 -0
- package/python/utopia_runtime.egg-info/PKG-INFO +59 -0
- package/python/utopia_runtime.egg-info/SOURCES.txt +10 -0
- package/python/utopia_runtime.egg-info/dependency_links.txt +1 -0
- package/python/utopia_runtime.egg-info/top_level.txt +1 -0
- package/scripts/publish-npm.sh +14 -0
- package/scripts/publish-pypi.sh +17 -0
- package/src/cli/commands/codex.ts +193 -0
- package/src/cli/commands/context.ts +188 -0
- package/src/cli/commands/destruct.ts +237 -0
- package/src/cli/commands/easter-eggs.ts +203 -0
- package/src/cli/commands/init.ts +505 -0
- package/src/cli/commands/instrument.ts +962 -0
- package/src/cli/commands/mcp.ts +16 -0
- package/src/cli/commands/serve.ts +194 -0
- package/src/cli/commands/status.ts +304 -0
- package/src/cli/commands/validate.ts +328 -0
- package/src/cli/index.ts +37 -0
- package/src/cli/utils/config.ts +54 -0
- package/src/graph/index.ts +687 -0
- package/src/instrumenter/javascript.ts +1798 -0
- package/src/mcp/index.ts +886 -0
- package/src/runtime/js/index.ts +518 -0
- package/src/runtime/js/package-lock.json +30 -0
- package/src/runtime/js/package.json +30 -0
- package/src/runtime/js/tsconfig.json +16 -0
- package/src/server/db/index.ts +26 -0
- package/src/server/db/schema.ts +45 -0
- package/src/server/index.ts +79 -0
- package/src/server/middleware/auth.ts +74 -0
- package/src/server/routes/admin.ts +36 -0
- package/src/server/routes/graph.ts +358 -0
- package/src/server/routes/probes.ts +286 -0
- package/src/types.ts +147 -0
- package/src/utopia-mode/index.ts +206 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,962 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { resolve, dirname } from 'node:path';
|
|
4
|
+
import { spawn, execSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, statSync, unlinkSync, rmSync } from 'node:fs';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { loadConfig, configExists } from '../utils/config.js';
|
|
8
|
+
import type { UtopiaConfig } from '../utils/config.js';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Shared: runtime API docs (used by both instrument and reinstrument)
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const RUNTIME_API_DOCS = `
|
|
15
|
+
## Utopia Runtime API
|
|
16
|
+
|
|
17
|
+
Import: \`import { __utopia } from 'utopia-runtime';\`
|
|
18
|
+
You MUST add this import to any file you add probes to.
|
|
19
|
+
|
|
20
|
+
All methods are async, non-blocking, and NEVER throw.
|
|
21
|
+
|
|
22
|
+
CRITICAL: Do NOT call \`__utopia()\` as a function. It is an OBJECT with methods. Always call \`__utopia.reportFunction()\`, \`__utopia.reportApi()\`, etc.
|
|
23
|
+
|
|
24
|
+
### Available methods with EXACT TypeScript signatures:
|
|
25
|
+
|
|
26
|
+
\`\`\`typescript
|
|
27
|
+
__utopia.reportFunction(data: {
|
|
28
|
+
file: string; // REQUIRED — the source file path
|
|
29
|
+
line: number; // REQUIRED — line number
|
|
30
|
+
functionName: string; // REQUIRED — name of the function
|
|
31
|
+
args: unknown[]; // REQUIRED — MUST be an array, e.g. [{ key: "value" }]
|
|
32
|
+
returnValue?: unknown; // optional — what the function returned
|
|
33
|
+
duration: number; // REQUIRED — milliseconds
|
|
34
|
+
callStack: string[]; // REQUIRED — can be empty array []
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
__utopia.reportApi(data: {
|
|
38
|
+
file: string; // REQUIRED
|
|
39
|
+
line: number; // REQUIRED
|
|
40
|
+
functionName: string; // REQUIRED
|
|
41
|
+
method: string; // REQUIRED — "GET", "POST", etc.
|
|
42
|
+
url: string; // REQUIRED — the URL called
|
|
43
|
+
statusCode?: number; // optional — HTTP status
|
|
44
|
+
duration: number; // REQUIRED — milliseconds
|
|
45
|
+
requestHeaders?: Record<string, string>;
|
|
46
|
+
responseHeaders?: Record<string, string>;
|
|
47
|
+
requestBody?: unknown;
|
|
48
|
+
responseBody?: unknown;
|
|
49
|
+
error?: string;
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
__utopia.reportError(data: {
|
|
53
|
+
file: string; // REQUIRED
|
|
54
|
+
line: number; // REQUIRED
|
|
55
|
+
functionName: string; // REQUIRED
|
|
56
|
+
errorType: string; // REQUIRED — e.g. "TypeError"
|
|
57
|
+
message: string; // REQUIRED — error message
|
|
58
|
+
stack: string; // REQUIRED — stack trace, use "" if unavailable
|
|
59
|
+
inputData: Record<string, unknown>; // REQUIRED — what input caused the error
|
|
60
|
+
codeLine: string; // REQUIRED — the code that failed, use "" if unavailable
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
__utopia.reportInfra(data: {
|
|
64
|
+
file: string; // REQUIRED
|
|
65
|
+
line: number; // REQUIRED
|
|
66
|
+
provider: string; // REQUIRED — "vercel", "aws", "gcp", etc.
|
|
67
|
+
region?: string;
|
|
68
|
+
serviceType?: string;
|
|
69
|
+
instanceId?: string;
|
|
70
|
+
envVars: Record<string, string>; // REQUIRED — filtered env vars (no secrets!)
|
|
71
|
+
memoryUsage: number; // REQUIRED — bytes, use 0 if unavailable
|
|
72
|
+
})
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
75
|
+
### EXACT code examples — copy these patterns:
|
|
76
|
+
|
|
77
|
+
**reportFunction (most common — use for data shapes, decisions, config):**
|
|
78
|
+
\`\`\`typescript
|
|
79
|
+
// utopia:probe
|
|
80
|
+
try {
|
|
81
|
+
__utopia.reportFunction({
|
|
82
|
+
file: 'lib/flags.ts', line: 10, functionName: 'isFeatureEnabled',
|
|
83
|
+
args: [{ flagName, userId }],
|
|
84
|
+
returnValue: { enabled: result, reason: error ? 'exception' : 'evaluated' },
|
|
85
|
+
duration: Date.now() - __utopia_start,
|
|
86
|
+
callStack: [],
|
|
87
|
+
});
|
|
88
|
+
} catch { /* probe error — swallow silently */ }
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
**reportApi (for HTTP calls):**
|
|
92
|
+
\`\`\`typescript
|
|
93
|
+
// utopia:probe
|
|
94
|
+
try {
|
|
95
|
+
__utopia.reportApi({
|
|
96
|
+
file: 'lib/api-client.ts', line: 25, functionName: 'customFetch',
|
|
97
|
+
method: method || 'GET',
|
|
98
|
+
url: urlString,
|
|
99
|
+
statusCode: response.status,
|
|
100
|
+
duration: Date.now() - __utopia_start,
|
|
101
|
+
});
|
|
102
|
+
} catch { /* probe error — swallow silently */ }
|
|
103
|
+
\`\`\`
|
|
104
|
+
|
|
105
|
+
**reportError (for catch blocks):**
|
|
106
|
+
\`\`\`typescript
|
|
107
|
+
// utopia:probe
|
|
108
|
+
try {
|
|
109
|
+
__utopia.reportError({
|
|
110
|
+
file: 'app/layout.tsx', line: 50, functionName: 'AuthLayout',
|
|
111
|
+
errorType: err instanceof Error ? err.constructor.name : 'UnknownError',
|
|
112
|
+
message: err instanceof Error ? err.message : String(err),
|
|
113
|
+
stack: err instanceof Error ? err.stack || '' : '',
|
|
114
|
+
inputData: { userId, path },
|
|
115
|
+
codeLine: '',
|
|
116
|
+
});
|
|
117
|
+
} catch { /* probe error — swallow silently */ }
|
|
118
|
+
\`\`\`
|
|
119
|
+
|
|
120
|
+
**reportInfra (for entry points, once on startup):**
|
|
121
|
+
\`\`\`typescript
|
|
122
|
+
// utopia:probe
|
|
123
|
+
try {
|
|
124
|
+
__utopia.reportInfra({
|
|
125
|
+
file: 'app/layout.tsx', line: 5, provider: 'vercel',
|
|
126
|
+
region: process.env.VERCEL_REGION,
|
|
127
|
+
envVars: Object.fromEntries(
|
|
128
|
+
Object.entries(process.env).filter(([k]) =>
|
|
129
|
+
!k.includes('KEY') && !k.includes('SECRET') && !k.includes('TOKEN')
|
|
130
|
+
).map(([k, v]) => [k, String(v)])
|
|
131
|
+
),
|
|
132
|
+
memoryUsage: typeof process !== 'undefined' && process.memoryUsage ? process.memoryUsage().heapUsed : 0,
|
|
133
|
+
});
|
|
134
|
+
} catch { /* probe error — swallow silently */ }
|
|
135
|
+
\`\`\`
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
function buildProbeRules(config: UtopiaConfig): string {
|
|
139
|
+
const base = `
|
|
140
|
+
## CRITICAL Rules — Follow Exactly
|
|
141
|
+
|
|
142
|
+
1. ALWAYS call \`__utopia.reportFunction()\`, \`__utopia.reportApi()\`, etc. — NEVER \`__utopia()\`
|
|
143
|
+
2. EVERY probe call MUST be wrapped in \`try { ... } catch { /* probe error */ }\`
|
|
144
|
+
3. Add \`// utopia:probe\` comment before each probe's try block
|
|
145
|
+
4. \`args\` MUST be an array: \`args: [{ key: value }]\` NOT \`args: { key: value }\`
|
|
146
|
+
5. \`callStack\` MUST be an array: \`callStack: []\`
|
|
147
|
+
6. \`duration\` MUST be a number (milliseconds). Use \`const __utopia_start = Date.now();\` before the operation
|
|
148
|
+
7. Never log passwords, tokens, API keys, or secrets
|
|
149
|
+
8. Never await a probe call — fire and forget
|
|
150
|
+
9. Get it right the first time — do NOT use incorrect API patterns and then fix them
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
const dataRules = config.dataMode === 'full' ? `
|
|
154
|
+
## Data Collection: FULL CONTEXT MODE
|
|
155
|
+
|
|
156
|
+
Capture REAL data in probes — actual inputs, outputs, DB results, request/response bodies. This gives maximum visibility.
|
|
157
|
+
|
|
158
|
+
- Capture actual function arguments and return values
|
|
159
|
+
- Capture real DB query results (row data, not just counts)
|
|
160
|
+
- Capture request/response bodies for API calls
|
|
161
|
+
- Capture real user inputs and form data
|
|
162
|
+
- Still NEVER log passwords, tokens, API keys, or secrets
|
|
163
|
+
- Truncate very large payloads (>1KB) to avoid bloating probe data
|
|
164
|
+
` : `
|
|
165
|
+
## Data Collection: SCHEMAS & SHAPES ONLY
|
|
166
|
+
|
|
167
|
+
Probes MUST anonymize all user/customer data. Capture structure, not content.
|
|
168
|
+
|
|
169
|
+
**NEVER capture:** actual names, emails, phones, addresses, PII, user-generated content, passwords, tokens, IPs, session IDs
|
|
170
|
+
**ALWAYS capture:** counts, field names/shapes, distributions as numbers, types, lengths, booleans, system IDs, enum values
|
|
171
|
+
|
|
172
|
+
Example — WRONG: \`args: [{ name: "Alice", email: "alice@example.com" }]\`
|
|
173
|
+
Example — RIGHT: \`args: [{ fields_present: ["name", "email"], has_notes: true }]\`, \`returnValue: { count: 8 }\`
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
const securityRules = (config.probeGoal === 'security' || config.probeGoal === 'both') ? `
|
|
177
|
+
## Security Probes
|
|
178
|
+
|
|
179
|
+
Add probes that detect insecure patterns at runtime. An AI agent reading this data should spot vulnerabilities immediately.
|
|
180
|
+
|
|
181
|
+
### What to probe for:
|
|
182
|
+
|
|
183
|
+
**SQL Injection:**
|
|
184
|
+
- Capture raw SQL queries being built — look for string concatenation/f-strings instead of parameterized queries
|
|
185
|
+
- Report when user input flows directly into a query without sanitization
|
|
186
|
+
- Capture the query pattern AND whether params were used: \`{ query: "SELECT...", parameterized: false, raw_input_in_query: true }\`
|
|
187
|
+
|
|
188
|
+
**Authentication & Authorization:**
|
|
189
|
+
- Capture auth check results: who was checked, what was the decision, was the token valid
|
|
190
|
+
- Report when endpoints are accessed WITHOUT auth checks
|
|
191
|
+
- Capture token validation: \`{ token_present: true, token_valid: false, expired: true, user_role: "admin" }\`
|
|
192
|
+
- Report permission escalation attempts: user trying to access admin routes
|
|
193
|
+
|
|
194
|
+
**Input Validation:**
|
|
195
|
+
- Capture when user input is used without validation/sanitization
|
|
196
|
+
- Report missing CSRF tokens on state-changing endpoints
|
|
197
|
+
- Capture Content-Type mismatches (expecting JSON, got something else)
|
|
198
|
+
|
|
199
|
+
**Insecure Patterns:**
|
|
200
|
+
- Report HTTP (not HTTPS) calls to external services
|
|
201
|
+
- Capture when sensitive data appears in URL query params instead of body/headers
|
|
202
|
+
- Report missing rate limiting on auth endpoints
|
|
203
|
+
- Capture CORS configuration: what origins are allowed
|
|
204
|
+
- Report when error messages expose internal details (stack traces, DB schemas) to users
|
|
205
|
+
|
|
206
|
+
**Dependency & Config:**
|
|
207
|
+
- Capture debug mode status in production
|
|
208
|
+
- Report when secrets are loaded from env vars that might be logged
|
|
209
|
+
- Capture TLS/SSL configuration for outbound connections
|
|
210
|
+
|
|
211
|
+
### Security probe example:
|
|
212
|
+
\`\`\`
|
|
213
|
+
// utopia:probe
|
|
214
|
+
try {
|
|
215
|
+
__utopia.reportFunction({
|
|
216
|
+
file: 'routes/users.ts', line: 45, functionName: 'getUserById',
|
|
217
|
+
args: [{
|
|
218
|
+
input_source: 'url_param',
|
|
219
|
+
sanitized: false,
|
|
220
|
+
used_in_query: true,
|
|
221
|
+
query_parameterized: true
|
|
222
|
+
}],
|
|
223
|
+
returnValue: { auth_checked: true, role_verified: false },
|
|
224
|
+
duration: Date.now() - __utopia_start,
|
|
225
|
+
callStack: [],
|
|
226
|
+
});
|
|
227
|
+
} catch { /* probe error */ }
|
|
228
|
+
\`\`\`
|
|
229
|
+
` : '';
|
|
230
|
+
|
|
231
|
+
return base + dataRules + securityRules;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Initial instrumentation prompt
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
function frameworkRuntimeDocs(config: UtopiaConfig): string {
|
|
240
|
+
if (config.framework === 'python') {
|
|
241
|
+
return `
|
|
242
|
+
## Utopia Runtime API (Python)
|
|
243
|
+
|
|
244
|
+
Import: \`import utopia_runtime\`
|
|
245
|
+
You MUST add this import to any file you add probes to.
|
|
246
|
+
|
|
247
|
+
All functions are non-blocking and NEVER raise. Every probe call MUST be wrapped in try/except.
|
|
248
|
+
|
|
249
|
+
CRITICAL: Call \`utopia_runtime.report_function(...)\`, NOT \`utopia_runtime(...)\`.
|
|
250
|
+
|
|
251
|
+
### Available functions:
|
|
252
|
+
|
|
253
|
+
\`\`\`python
|
|
254
|
+
utopia_runtime.report_function(
|
|
255
|
+
file="path/to/file.py", # REQUIRED
|
|
256
|
+
line=10, # REQUIRED
|
|
257
|
+
function_name="my_func", # REQUIRED
|
|
258
|
+
args=[{"key": "value"}], # REQUIRED — must be a list
|
|
259
|
+
return_value={"result": True}, # optional
|
|
260
|
+
duration=150, # REQUIRED — milliseconds
|
|
261
|
+
call_stack=[], # REQUIRED — can be empty list
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
utopia_runtime.report_api(
|
|
265
|
+
file="path/to/file.py", # REQUIRED
|
|
266
|
+
line=10, # REQUIRED
|
|
267
|
+
function_name="fetch_users", # REQUIRED
|
|
268
|
+
method="GET", # REQUIRED
|
|
269
|
+
url="https://api.example.com", # REQUIRED
|
|
270
|
+
status_code=200, # optional
|
|
271
|
+
duration=150, # REQUIRED
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
utopia_runtime.report_error(
|
|
275
|
+
file="path/to/file.py", # REQUIRED
|
|
276
|
+
line=10, # REQUIRED
|
|
277
|
+
function_name="my_func", # REQUIRED
|
|
278
|
+
error_type="ValueError", # REQUIRED
|
|
279
|
+
message="invalid input", # REQUIRED
|
|
280
|
+
stack=traceback.format_exc(), # REQUIRED — use "" if unavailable
|
|
281
|
+
input_data={"arg1": repr(x)}, # REQUIRED
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
utopia_runtime.report_db(
|
|
285
|
+
file="path/to/file.py", # REQUIRED
|
|
286
|
+
line=10, # REQUIRED
|
|
287
|
+
function_name="get_users", # REQUIRED
|
|
288
|
+
operation="SELECT", # REQUIRED
|
|
289
|
+
query="SELECT * FROM users", # optional
|
|
290
|
+
table="users", # optional
|
|
291
|
+
duration=50, # REQUIRED
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
utopia_runtime.report_infra(
|
|
295
|
+
file="path/to/file.py", # REQUIRED
|
|
296
|
+
line=1, # REQUIRED
|
|
297
|
+
provider="aws", # REQUIRED
|
|
298
|
+
env_vars={k: v for k, v in os.environ.items() if "KEY" not in k and "SECRET" not in k}, # REQUIRED
|
|
299
|
+
)
|
|
300
|
+
\`\`\`
|
|
301
|
+
|
|
302
|
+
### EXACT code example — copy this pattern:
|
|
303
|
+
|
|
304
|
+
\`\`\`python
|
|
305
|
+
# utopia:probe
|
|
306
|
+
try:
|
|
307
|
+
utopia_runtime.report_function(
|
|
308
|
+
file="app/routes.py", line=25, function_name="get_user",
|
|
309
|
+
args=[{"user_id": user_id}],
|
|
310
|
+
return_value={"found": user is not None, "role": getattr(user, "role", None)},
|
|
311
|
+
duration=int((time.time() - _utopia_start) * 1000),
|
|
312
|
+
call_stack=[],
|
|
313
|
+
)
|
|
314
|
+
except Exception:
|
|
315
|
+
pass # probe error — swallow silently
|
|
316
|
+
\`\`\`
|
|
317
|
+
`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// JS/TS (nextjs, react)
|
|
321
|
+
return RUNTIME_API_DOCS;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function frameworkProbeRules(config: UtopiaConfig): string {
|
|
325
|
+
if (config.framework === 'python') {
|
|
326
|
+
// Python-specific syntax rules + shared data/security rules
|
|
327
|
+
const pyBase = `
|
|
328
|
+
## CRITICAL Rules — Follow Exactly
|
|
329
|
+
|
|
330
|
+
1. ALWAYS call \`utopia_runtime.report_function()\`, \`utopia_runtime.report_api()\`, etc. — NEVER \`utopia_runtime()\`
|
|
331
|
+
2. EVERY probe call MUST be wrapped in \`try: ... except Exception: pass\`
|
|
332
|
+
3. Add \`# utopia:probe\` comment before each probe's try block
|
|
333
|
+
4. \`args\` MUST be a list: \`args=[{"key": value}]\` NOT \`args={"key": value}\`
|
|
334
|
+
5. \`call_stack\` MUST be a list: \`call_stack=[]\`
|
|
335
|
+
6. \`duration\` MUST be an int (milliseconds). Use \`_utopia_start = time.time()\` before the operation
|
|
336
|
+
7. Never log passwords, tokens, API keys, or secrets
|
|
337
|
+
8. Never await a probe call — fire and forget
|
|
338
|
+
9. Get it right the first time
|
|
339
|
+
`;
|
|
340
|
+
// Get the shared data mode + security rules (these are language-agnostic)
|
|
341
|
+
const sharedRules = buildProbeRules(config);
|
|
342
|
+
// Extract just the data and security sections (skip the JS-specific base rules)
|
|
343
|
+
const dataAndSecurity = sharedRules.substring(sharedRules.indexOf('## Data Collection'));
|
|
344
|
+
return pyBase + (dataAndSecurity || '');
|
|
345
|
+
}
|
|
346
|
+
return buildProbeRules(config);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildInstrumentationPrompt(config: UtopiaConfig): string {
|
|
350
|
+
return `You are giving a codebase the ability to speak. You're adding Utopia probes — these aren't logs. They're the code's voice, telling AI agents what's actually happening at runtime so those agents can write better code.
|
|
351
|
+
|
|
352
|
+
## Project Context
|
|
353
|
+
- Provider: ${config.cloudProvider} / ${config.service}
|
|
354
|
+
- Deployment: ${config.deploymentMethod}
|
|
355
|
+
- Languages: ${config.language.join(', ')}
|
|
356
|
+
- Framework: ${config.framework}
|
|
357
|
+
- Standalone: ${config.isStandalone}
|
|
358
|
+
- Data mode: ${config.dataMode === 'full' ? 'FULL DATA — capture real inputs/outputs/data' : 'SCHEMAS ONLY — capture shapes, counts, types, never real user data'}
|
|
359
|
+
- Probe goal: ${config.probeGoal === 'both' ? 'DEBUGGING + SECURITY' : config.probeGoal === 'security' ? 'SECURITY FOCUS' : 'DEBUGGING FOCUS'}
|
|
360
|
+
|
|
361
|
+
${frameworkRuntimeDocs(config)}
|
|
362
|
+
|
|
363
|
+
${frameworkProbeRules(config)}
|
|
364
|
+
|
|
365
|
+
## How to Think About Probes
|
|
366
|
+
|
|
367
|
+
You are NOT a logger. You are building a bridge between production and the AI agent that will work on this code next.
|
|
368
|
+
|
|
369
|
+
Ask yourself for every function: "If an AI agent needed to modify this code, what would it need to know about how it actually behaves in production?"
|
|
370
|
+
|
|
371
|
+
### Deep probes — capture CONTEXT, not just events:
|
|
372
|
+
|
|
373
|
+
**Instead of:** "API call to /users returned 200 in 150ms"
|
|
374
|
+
**Do this:** Capture the response shape, how many items came back, what query params were used, whether pagination was involved, what the auth context was.
|
|
375
|
+
|
|
376
|
+
**Instead of:** "Error in processOrder"
|
|
377
|
+
**Do this:** Capture what the order data looked like, what validation failed, what the user state was, what upstream call triggered this.
|
|
378
|
+
|
|
379
|
+
**Instead of:** "Database query took 50ms"
|
|
380
|
+
**Do this:** Capture the query pattern, number of rows, whether it was cached, what triggered the query, the data shape returned.
|
|
381
|
+
|
|
382
|
+
### What makes a great probe:
|
|
383
|
+
|
|
384
|
+
1. **Data shape capture** — When a function receives or returns data, capture the SHAPE (keys, array lengths, types) not raw data. Use: \`{ shape: Object.keys(data), count: Array.isArray(data) ? data.length : 1 }\`
|
|
385
|
+
|
|
386
|
+
2. **Decision point capture** — At every if/else or switch that matters, report which path was taken and why: \`{ branch: 'premium_user', reason: 'subscription.tier === premium', userId: user.id }\`
|
|
387
|
+
|
|
388
|
+
3. **Integration context** — For every external call, capture not just timing but the full context: what triggered it, what was the input shape, what came back, how does the response get used downstream.
|
|
389
|
+
|
|
390
|
+
4. **Error context that enables fixing** — Don't just capture the error. Capture the full state that led to it: function inputs, relevant config, upstream data, the exact data that violated the expectation.
|
|
391
|
+
|
|
392
|
+
5. **Runtime configuration** — Capture feature flags, environment variables, SDK versions, tenant/user context that affects behavior. An agent needs to know the runtime environment to write correct code.
|
|
393
|
+
|
|
394
|
+
6. **Relationship mapping** — When function A calls function B, capture that chain. Use reportFunction to show how data flows: what goes in, what comes out, what transforms happen.
|
|
395
|
+
|
|
396
|
+
## Your Task
|
|
397
|
+
|
|
398
|
+
1. **Explore the codebase deeply.** Understand the architecture, data flow, entry points, integrations, business logic. Read key files thoroughly.
|
|
399
|
+
|
|
400
|
+
2. **Instrument comprehensively.** Add probes to:
|
|
401
|
+
- Every API route / server action / endpoint handler — capture request shape, response shape, auth context, timing
|
|
402
|
+
- Every external API call — capture full integration context (what triggers it, input/output shapes, error patterns)
|
|
403
|
+
- Every database interaction — capture query patterns, data shapes, performance
|
|
404
|
+
- Authentication/authorization — capture auth state, token validation, permission checks
|
|
405
|
+
- Business logic — capture decision points, data transformations, validation results
|
|
406
|
+
- Error boundaries — capture the FULL state that led to the error
|
|
407
|
+
- Entry points — capture infrastructure context (provider, region, memory, config)
|
|
408
|
+
- Feature flags / config — capture what features are active and their values
|
|
409
|
+
- Data transformations — capture input shape → output shape for key transforms
|
|
410
|
+
|
|
411
|
+
3. **For each probe, think:** What would an AI agent building a new feature here need to know? What would an AI agent debugging a production issue need to see? Add THAT data.
|
|
412
|
+
|
|
413
|
+
${config.probeGoal === 'security' || config.probeGoal === 'both' ? `4. **Add security probes.** Beyond debugging, actively look for:
|
|
414
|
+
- SQL queries built with string concatenation — capture whether parameterized
|
|
415
|
+
- Auth checks — capture every auth decision point, who was checked, what passed/failed
|
|
416
|
+
- Input validation — capture where user input enters the system and whether it's sanitized
|
|
417
|
+
- Insecure HTTP calls, exposed error details, missing rate limiting, CORS config
|
|
418
|
+
- Report these with clear flags: \`{ parameterized: false, raw_input_in_query: true }\`
|
|
419
|
+
` : ''}
|
|
420
|
+
${config.probeGoal === 'security' || config.probeGoal === 'both' ? '5' : '4'}. **Add the import to every file you add probes to.** ${config.framework === 'python' ? '`import utopia_runtime`' : '`import { __utopia } from \'utopia-runtime\';`'}
|
|
421
|
+
|
|
422
|
+
${config.probeGoal === 'security' || config.probeGoal === 'both' ? '6' : '5'}. Give a summary of what you instrumented and why.
|
|
423
|
+
|
|
424
|
+
Remember: These probes are how the code talks back to the agent. Make them rich, contextual, and useful.`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// Reinstrument prompt (targeted, context-driven)
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
function buildReinstrumentPrompt(config: UtopiaConfig, purpose: string): string {
|
|
432
|
+
return `You are adding targeted Utopia probes to this codebase for a specific purpose. The codebase already has some Utopia probes from initial instrumentation. You are adding MORE probes in areas relevant to the task at hand.
|
|
433
|
+
|
|
434
|
+
## Your Purpose
|
|
435
|
+
${purpose}
|
|
436
|
+
|
|
437
|
+
## Project Context
|
|
438
|
+
- Provider: ${config.cloudProvider} / ${config.service}
|
|
439
|
+
- Framework: ${config.framework}
|
|
440
|
+
- Languages: ${config.language.join(', ')}
|
|
441
|
+
|
|
442
|
+
${frameworkRuntimeDocs(config)}
|
|
443
|
+
|
|
444
|
+
${frameworkProbeRules(config)}
|
|
445
|
+
|
|
446
|
+
## Important Rules for Reinstrumentation
|
|
447
|
+
|
|
448
|
+
1. **DO NOT remove or modify existing probes** (look for \`// utopia:probe\` markers)
|
|
449
|
+
2. **Add probes specifically relevant to the purpose above** — don't re-instrument the whole codebase
|
|
450
|
+
3. **Go deep** — since this is targeted, add very detailed probes that capture everything relevant to the stated purpose
|
|
451
|
+
4. **Think about what the AI agent working on "${purpose}" would need to see from production**
|
|
452
|
+
|
|
453
|
+
## What to do
|
|
454
|
+
|
|
455
|
+
1. Understand the purpose above. What part of the codebase is involved?
|
|
456
|
+
2. Find the relevant files and functions
|
|
457
|
+
3. Add rich, contextual probes that capture everything an AI agent would need for this task
|
|
458
|
+
4. Add the import to any new files (check if it's already imported first): ${config.framework === 'python' ? '`import utopia_runtime`' : '`import { __utopia } from \'utopia-runtime\';`'}
|
|
459
|
+
5. Summarize what you added and why it's relevant to the purpose
|
|
460
|
+
|
|
461
|
+
Be thorough in the targeted area. These probes should give deep insight into the specific area of interest.`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// Shared utilities
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
|
|
468
|
+
function findUtopiaRoot(): string | null {
|
|
469
|
+
// Walk up from this file to find the utopia project root (has src/runtime/js/index.ts)
|
|
470
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
471
|
+
let dir = dirname(__filename);
|
|
472
|
+
for (let i = 0; i < 10; i++) {
|
|
473
|
+
if (existsSync(resolve(dir, 'src', 'runtime', 'js', 'index.ts'))) return dir;
|
|
474
|
+
const parent = dirname(dir);
|
|
475
|
+
if (parent === dir) break;
|
|
476
|
+
dir = parent;
|
|
477
|
+
}
|
|
478
|
+
// Also check the npm global link target
|
|
479
|
+
try {
|
|
480
|
+
const binPath = execSync('which utopia', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
481
|
+
// Follow symlinks: bin/utopia.js -> utopia project root
|
|
482
|
+
const realBin = readFileSync(binPath, 'utf-8'); // Read to check, but we need the dir
|
|
483
|
+
let linkDir = dirname(binPath);
|
|
484
|
+
for (let i = 0; i < 10; i++) {
|
|
485
|
+
if (existsSync(resolve(linkDir, 'src', 'runtime', 'js', 'index.ts'))) return linkDir;
|
|
486
|
+
const parent = dirname(linkDir);
|
|
487
|
+
if (parent === linkDir) break;
|
|
488
|
+
linkDir = parent;
|
|
489
|
+
}
|
|
490
|
+
} catch { /* ignore */ }
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function installJsRuntime(cwd: string): { ok: boolean; error?: string } {
|
|
495
|
+
let pm = 'npm';
|
|
496
|
+
if (existsSync(resolve(cwd, 'pnpm-lock.yaml'))) pm = 'pnpm';
|
|
497
|
+
else if (existsSync(resolve(cwd, 'yarn.lock'))) pm = 'yarn';
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
execSync(`${pm} add utopia-runtime 2>&1`, { cwd, stdio: 'pipe' });
|
|
501
|
+
return { ok: true };
|
|
502
|
+
} catch (err) {
|
|
503
|
+
return { ok: false, error: `${pm} add utopia-runtime failed: ${(err as Error).message}` };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function installPythonRuntime(cwd: string): { ok: boolean; error?: string } {
|
|
508
|
+
const venvDirs = ['.venv', 'venv', 'env', '.env'];
|
|
509
|
+
let pip = '';
|
|
510
|
+
for (const vdir of venvDirs) {
|
|
511
|
+
const pipPath = resolve(cwd, vdir, 'bin', 'pip');
|
|
512
|
+
if (existsSync(pipPath)) { pip = pipPath; break; }
|
|
513
|
+
}
|
|
514
|
+
if (!pip && process.env.VIRTUAL_ENV) {
|
|
515
|
+
const venvPip = resolve(process.env.VIRTUAL_ENV, 'bin', 'pip');
|
|
516
|
+
if (existsSync(venvPip)) pip = venvPip;
|
|
517
|
+
}
|
|
518
|
+
if (!pip) pip = 'pip3';
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
execSync(`${pip} install utopia-runtime --quiet 2>&1`, { cwd, stdio: 'pipe' });
|
|
522
|
+
return { ok: true };
|
|
523
|
+
} catch (err) {
|
|
524
|
+
return { ok: false, error: `pip install utopia-runtime failed: ${(err as Error).message}` };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function installRuntime(cwd: string, framework: string): { ok: boolean; error?: string } {
|
|
529
|
+
if (framework === 'python') {
|
|
530
|
+
return installPythonRuntime(cwd);
|
|
531
|
+
}
|
|
532
|
+
return installJsRuntime(cwd);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function ensureEnvVars(cwd: string, config: UtopiaConfig): void {
|
|
536
|
+
// Python projects read from .utopia/config.json directly — no env vars needed
|
|
537
|
+
// (avoids conflicts with Pydantic Settings and other strict env parsers)
|
|
538
|
+
if (config.framework === 'python') {
|
|
539
|
+
console.log(chalk.dim(' Python project — config read from .utopia/config.json (no env vars needed)'));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const isNextJs = config.framework === 'nextjs';
|
|
544
|
+
const envFileName = isNextJs ? '.env.local' : '.env';
|
|
545
|
+
const envFilePath = resolve(cwd, envFileName);
|
|
546
|
+
|
|
547
|
+
const envVars: string[] = [
|
|
548
|
+
`UTOPIA_ENDPOINT=${config.dataEndpoint}`,
|
|
549
|
+
`UTOPIA_PROJECT_ID=${config.projectId}`,
|
|
550
|
+
];
|
|
551
|
+
if (isNextJs) {
|
|
552
|
+
envVars.push(
|
|
553
|
+
`NEXT_PUBLIC_UTOPIA_ENDPOINT=${config.dataEndpoint}`,
|
|
554
|
+
`NEXT_PUBLIC_UTOPIA_PROJECT_ID=${config.projectId}`,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let existingEnv = '';
|
|
559
|
+
try { existingEnv = readFileSync(envFilePath, 'utf-8'); } catch { /* doesn't exist */ }
|
|
560
|
+
|
|
561
|
+
let updatedEnv = existingEnv;
|
|
562
|
+
for (const envVar of envVars) {
|
|
563
|
+
const key = envVar.split('=')[0];
|
|
564
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
565
|
+
if (regex.test(updatedEnv)) {
|
|
566
|
+
updatedEnv = updatedEnv.replace(regex, envVar);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const missing = envVars.filter(v => !updatedEnv.includes(v.split('=')[0] + '='));
|
|
571
|
+
if (missing.length > 0) {
|
|
572
|
+
updatedEnv += '\n# Utopia probe configuration\n' + missing.join('\n') + '\n';
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (updatedEnv !== existingEnv) {
|
|
576
|
+
writeFileSync(envFilePath, updatedEnv || missing.join('\n') + '\n');
|
|
577
|
+
console.log(chalk.green(` Environment variables updated in ${envFileName}`));
|
|
578
|
+
} else {
|
|
579
|
+
console.log(chalk.dim(` Environment variables up to date in ${envFileName}`));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const SNAPSHOT_DIR = '.utopia/snapshots';
|
|
584
|
+
const SKIP_DIRS = new Set(['node_modules', '.next', 'dist', 'build', '.utopia', '.git', '__pycache__', 'venv', '.venv', 'coverage']);
|
|
585
|
+
const SOURCE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py']);
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Snapshot all source files before instrumentation.
|
|
589
|
+
* Only snapshots files that don't already have a snapshot (so reinstrument
|
|
590
|
+
* preserves the original pre-instrument state).
|
|
591
|
+
*/
|
|
592
|
+
function snapshotFiles(cwd: string): number {
|
|
593
|
+
const snapshotBase = resolve(cwd, SNAPSHOT_DIR);
|
|
594
|
+
let count = 0;
|
|
595
|
+
|
|
596
|
+
function walk(dir: string): void {
|
|
597
|
+
let entries: string[];
|
|
598
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
599
|
+
for (const entry of entries) {
|
|
600
|
+
if (entry.startsWith('.') || SKIP_DIRS.has(entry)) continue;
|
|
601
|
+
const full = resolve(dir, entry);
|
|
602
|
+
try {
|
|
603
|
+
const st = statSync(full);
|
|
604
|
+
if (st.isDirectory()) {
|
|
605
|
+
walk(full);
|
|
606
|
+
} else if (st.isFile()) {
|
|
607
|
+
const ext = full.substring(full.lastIndexOf('.'));
|
|
608
|
+
if (!SOURCE_EXTS.has(ext)) continue;
|
|
609
|
+
const rel = full.substring(cwd.length + 1);
|
|
610
|
+
const snapPath = resolve(snapshotBase, rel);
|
|
611
|
+
// Only snapshot if we don't already have one (preserve original)
|
|
612
|
+
if (!existsSync(snapPath)) {
|
|
613
|
+
mkdirSync(dirname(snapPath), { recursive: true });
|
|
614
|
+
writeFileSync(snapPath, readFileSync(full));
|
|
615
|
+
count++;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} catch { /* skip unreadable */ }
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
walk(cwd);
|
|
623
|
+
return count;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* After instrumentation, remove snapshots for files that weren't actually modified.
|
|
628
|
+
* This keeps the snapshot dir lean.
|
|
629
|
+
*/
|
|
630
|
+
function pruneUnchangedSnapshots(cwd: string): void {
|
|
631
|
+
const snapshotBase = resolve(cwd, SNAPSHOT_DIR);
|
|
632
|
+
if (!existsSync(snapshotBase)) return;
|
|
633
|
+
|
|
634
|
+
function walk(dir: string): void {
|
|
635
|
+
let entries: string[];
|
|
636
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
637
|
+
for (const entry of entries) {
|
|
638
|
+
const full = resolve(dir, entry);
|
|
639
|
+
try {
|
|
640
|
+
const st = statSync(full);
|
|
641
|
+
if (st.isDirectory()) {
|
|
642
|
+
walk(full);
|
|
643
|
+
// Remove empty dirs
|
|
644
|
+
try {
|
|
645
|
+
const remaining = readdirSync(full);
|
|
646
|
+
if (remaining.length === 0) rmSync(full, { recursive: true });
|
|
647
|
+
} catch { /* ignore */ }
|
|
648
|
+
} else if (st.isFile()) {
|
|
649
|
+
const rel = full.substring(snapshotBase.length + 1);
|
|
650
|
+
const sourcePath = resolve(cwd, rel);
|
|
651
|
+
if (!existsSync(sourcePath)) {
|
|
652
|
+
// Source file was deleted — remove snapshot
|
|
653
|
+
unlinkSync(full);
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
// Compare snapshot to current file
|
|
657
|
+
const snapshot = readFileSync(full);
|
|
658
|
+
const current = readFileSync(sourcePath);
|
|
659
|
+
if (snapshot.equals(current)) {
|
|
660
|
+
// File wasn't modified — remove snapshot
|
|
661
|
+
unlinkSync(full);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
} catch { /* skip */ }
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
walk(snapshotBase);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function isAlreadyInstrumented(cwd: string): boolean {
|
|
672
|
+
try {
|
|
673
|
+
const result = execSync(
|
|
674
|
+
`grep -rl "utopia:probe" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.py" . 2>/dev/null | head -1`,
|
|
675
|
+
{ cwd, stdio: 'pipe', encoding: 'utf-8' }
|
|
676
|
+
);
|
|
677
|
+
return result.trim().length > 0;
|
|
678
|
+
} catch {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function spawnAgentSession(cwd: string, prompt: string, agent: string): Promise<number> {
|
|
684
|
+
return new Promise<number>((resolvePromise) => {
|
|
685
|
+
let child: ReturnType<typeof spawn>;
|
|
686
|
+
|
|
687
|
+
// Write prompt to temp file to avoid shell argument length limits
|
|
688
|
+
const tmpPromptFile = resolve(cwd, '.utopia', '.prompt.tmp');
|
|
689
|
+
mkdirSync(dirname(tmpPromptFile), { recursive: true });
|
|
690
|
+
writeFileSync(tmpPromptFile, prompt);
|
|
691
|
+
|
|
692
|
+
if (agent === 'codex') {
|
|
693
|
+
child = spawn('codex', [
|
|
694
|
+
'exec', readFileSync(tmpPromptFile, 'utf-8'),
|
|
695
|
+
'--full-auto',
|
|
696
|
+
], {
|
|
697
|
+
cwd,
|
|
698
|
+
stdio: ['ignore', 'inherit', 'pipe'],
|
|
699
|
+
env: { ...process.env },
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// Codex doesn't stream structured output, so show a spinner
|
|
703
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
704
|
+
let frame = 0;
|
|
705
|
+
const startTime = Date.now();
|
|
706
|
+
const spinner = setInterval(() => {
|
|
707
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
708
|
+
const min = Math.floor(elapsed / 60);
|
|
709
|
+
const sec = elapsed % 60;
|
|
710
|
+
const timeStr = min > 0 ? `${min}m ${sec}s` : `${sec}s`;
|
|
711
|
+
process.stdout.write(`\r ${frames[frame % frames.length]} Codex is instrumenting... (${timeStr}) `);
|
|
712
|
+
frame++;
|
|
713
|
+
}, 100);
|
|
714
|
+
child.on('close', () => { clearInterval(spinner); process.stdout.write('\r' + ' '.repeat(60) + '\r'); });
|
|
715
|
+
} else {
|
|
716
|
+
child = spawn('claude', [
|
|
717
|
+
'-p', prompt,
|
|
718
|
+
'--allowedTools', 'Edit,Read,Grep,Glob,Bash,Write',
|
|
719
|
+
'--permission-mode', 'acceptEdits',
|
|
720
|
+
'--output-format', 'stream-json',
|
|
721
|
+
'--verbose',
|
|
722
|
+
], {
|
|
723
|
+
cwd,
|
|
724
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
725
|
+
env: { ...process.env },
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Clean up temp file
|
|
730
|
+
try { unlinkSync(tmpPromptFile); } catch { /* ignore */ }
|
|
731
|
+
|
|
732
|
+
let errorOutput = '';
|
|
733
|
+
let filesEdited = 0;
|
|
734
|
+
let filesRead = 0;
|
|
735
|
+
|
|
736
|
+
child.stdout?.on('data', (data: Buffer) => {
|
|
737
|
+
for (const line of data.toString().split('\n')) {
|
|
738
|
+
if (!line.trim()) continue;
|
|
739
|
+
try {
|
|
740
|
+
const msg = JSON.parse(line);
|
|
741
|
+
|
|
742
|
+
// Claude Code streaming format
|
|
743
|
+
if (msg.type === 'assistant' && msg.message?.content) {
|
|
744
|
+
for (const block of msg.message.content) {
|
|
745
|
+
if (block.type === 'text' && block.text) {
|
|
746
|
+
process.stdout.write(chalk.dim(block.text));
|
|
747
|
+
}
|
|
748
|
+
if (block.type === 'tool_use') {
|
|
749
|
+
if (block.name === 'Edit' || block.name === 'Write') {
|
|
750
|
+
filesEdited++;
|
|
751
|
+
const fp = (block.input?.file_path || '').split('/').slice(-2).join('/');
|
|
752
|
+
console.log(chalk.green(` [${filesEdited}] Edited: ${fp}`));
|
|
753
|
+
} else if (block.name === 'Read') {
|
|
754
|
+
filesRead++;
|
|
755
|
+
const fp = (block.input?.file_path || '').split('/').slice(-2).join('/');
|
|
756
|
+
if (filesRead <= 20 || filesRead % 10 === 0) {
|
|
757
|
+
console.log(chalk.dim(` Reading: ${fp}`));
|
|
758
|
+
}
|
|
759
|
+
} else if (block.name === 'Grep' || block.name === 'Glob') {
|
|
760
|
+
console.log(chalk.dim(` Searching: ${block.input?.pattern || '...'}`));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (msg.type === 'result' && msg.result) {
|
|
766
|
+
console.log('\n' + chalk.white(msg.result));
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Codex streaming format (JSONL events)
|
|
770
|
+
if (msg.type === 'message' && msg.content) {
|
|
771
|
+
process.stdout.write(chalk.dim(typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)));
|
|
772
|
+
}
|
|
773
|
+
if (msg.type === 'tool_call' || msg.type === 'function_call') {
|
|
774
|
+
const name = msg.name || msg.function?.name || '';
|
|
775
|
+
if (name.includes('edit') || name.includes('write') || name.includes('patch')) {
|
|
776
|
+
filesEdited++;
|
|
777
|
+
console.log(chalk.green(` [${filesEdited}] Edited: ${msg.arguments?.path || msg.arguments?.file || '...'}`));
|
|
778
|
+
} else if (name.includes('read')) {
|
|
779
|
+
filesRead++;
|
|
780
|
+
if (filesRead <= 20 || filesRead % 10 === 0) {
|
|
781
|
+
console.log(chalk.dim(` Reading: ${msg.arguments?.path || msg.arguments?.file || '...'}`));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
} catch { /* partial JSON line */ }
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
child.stderr?.on('data', (data: Buffer) => { errorOutput += data.toString(); });
|
|
790
|
+
|
|
791
|
+
child.on('close', (code) => {
|
|
792
|
+
if (code !== 0 && errorOutput) {
|
|
793
|
+
console.log(chalk.dim(` ${errorOutput.trim()}`));
|
|
794
|
+
}
|
|
795
|
+
resolvePromise(code ?? 1);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
child.on('error', (err) => {
|
|
799
|
+
const agentName = agent === 'codex' ? 'Codex' : 'Claude Code';
|
|
800
|
+
console.log(chalk.red(`\n Error spawning ${agentName}: ${err.message}`));
|
|
801
|
+
resolvePromise(1);
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function checkAgentAvailable(agent: string): boolean {
|
|
807
|
+
const cmd = agent === 'codex' ? 'codex' : 'claude';
|
|
808
|
+
try {
|
|
809
|
+
execSync(`which ${cmd}`, { stdio: 'pipe' });
|
|
810
|
+
return true;
|
|
811
|
+
} catch {
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ---------------------------------------------------------------------------
|
|
817
|
+
// instrument command — initial, full-codebase instrumentation
|
|
818
|
+
// ---------------------------------------------------------------------------
|
|
819
|
+
|
|
820
|
+
export const instrumentCommand = new Command('instrument')
|
|
821
|
+
.description('Add production probes to your codebase via Claude Code (initial instrumentation)')
|
|
822
|
+
.action(async () => {
|
|
823
|
+
const cwd = process.cwd();
|
|
824
|
+
|
|
825
|
+
if (!configExists(cwd)) {
|
|
826
|
+
console.log(chalk.red('\n Error: No .utopia/config.json found.'));
|
|
827
|
+
console.log(chalk.dim(' Run "utopia init" first.\n'));
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Check if already instrumented
|
|
832
|
+
if (isAlreadyInstrumented(cwd)) {
|
|
833
|
+
console.log(chalk.yellow('\n This codebase already has Utopia probes.'));
|
|
834
|
+
console.log(chalk.dim(' Use "utopia reinstrument -p <purpose>" to add targeted probes.'));
|
|
835
|
+
console.log(chalk.dim(' Or remove existing probes first (search for "utopia:probe" markers).\n'));
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const config = await loadConfig(cwd);
|
|
840
|
+
console.log(chalk.bold.cyan('\n Utopia Instrumentation\n'));
|
|
841
|
+
|
|
842
|
+
// Install runtime
|
|
843
|
+
console.log(chalk.dim(' Installing utopia-runtime...'));
|
|
844
|
+
const rtResult = installRuntime(cwd, config.framework);
|
|
845
|
+
if (rtResult.ok) {
|
|
846
|
+
console.log(chalk.green(' utopia-runtime installed.'));
|
|
847
|
+
} else {
|
|
848
|
+
console.log(chalk.red(` Error installing utopia-runtime: ${rtResult.error}`));
|
|
849
|
+
console.log(chalk.dim(' Your app will fail to resolve "utopia-runtime" until this is fixed.'));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Env vars
|
|
853
|
+
console.log(chalk.dim(' Verifying environment variables...'));
|
|
854
|
+
try { ensureEnvVars(cwd, config); } catch { /* non-fatal */ }
|
|
855
|
+
console.log('');
|
|
856
|
+
|
|
857
|
+
// Check agent CLI
|
|
858
|
+
const agentName = config.agent === 'codex' ? 'Codex' : 'Claude Code';
|
|
859
|
+
if (!checkAgentAvailable(config.agent)) {
|
|
860
|
+
console.log(chalk.red(` Error: ${agentName} CLI not found.`));
|
|
861
|
+
if (config.agent === 'codex') {
|
|
862
|
+
console.log(chalk.dim(' Install: npm install -g @openai/codex\n'));
|
|
863
|
+
} else {
|
|
864
|
+
console.log(chalk.dim(' Install from: https://docs.anthropic.com/en/docs/claude-code\n'));
|
|
865
|
+
}
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Snapshot all source files before agent modifies them
|
|
870
|
+
console.log(chalk.dim(' Snapshotting source files...'));
|
|
871
|
+
const snapshotCount = snapshotFiles(cwd);
|
|
872
|
+
console.log(chalk.dim(` Snapshotted ${snapshotCount} file(s).\n`));
|
|
873
|
+
|
|
874
|
+
console.log(chalk.dim(` Launching ${agentName} for initial instrumentation...`));
|
|
875
|
+
console.log(chalk.dim(` ${agentName} will analyze your codebase and add deep, contextual probes.\n`));
|
|
876
|
+
console.log(chalk.bold.white(` --- ${agentName} Session ---\n`));
|
|
877
|
+
|
|
878
|
+
const code = await spawnAgentSession(cwd, buildInstrumentationPrompt(config), config.agent);
|
|
879
|
+
|
|
880
|
+
console.log(chalk.bold.white(`\n --- End ${agentName} Session ---\n`));
|
|
881
|
+
|
|
882
|
+
// Prune snapshots for files that weren't modified
|
|
883
|
+
pruneUnchangedSnapshots(cwd);
|
|
884
|
+
|
|
885
|
+
if (code === 0) {
|
|
886
|
+
console.log(chalk.bold.green(' Instrumentation complete!\n'));
|
|
887
|
+
} else {
|
|
888
|
+
console.log(chalk.yellow(` ${agentName} exited with code ${code}.\n`));
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
console.log(chalk.dim(' Next steps:'));
|
|
892
|
+
console.log(chalk.dim(' 1. utopia validate — Verify probe syntax'));
|
|
893
|
+
console.log(chalk.dim(' 2. utopia serve — Start the data service'));
|
|
894
|
+
console.log(chalk.dim(' 3. Run your app and watch probe data flow in\n'));
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// ---------------------------------------------------------------------------
|
|
898
|
+
// reinstrument command — targeted, purpose-driven probe addition
|
|
899
|
+
// ---------------------------------------------------------------------------
|
|
900
|
+
|
|
901
|
+
export const reinstrumentCommand = new Command('reinstrument')
|
|
902
|
+
.description('Add targeted probes for a specific task or area')
|
|
903
|
+
.requiredOption('-p, --purpose <purpose>', 'What the probes are for (e.g. "debugging auth flow", "preparing to refactor billing")')
|
|
904
|
+
.action(async (options) => {
|
|
905
|
+
const cwd = process.cwd();
|
|
906
|
+
|
|
907
|
+
if (!configExists(cwd)) {
|
|
908
|
+
console.log(chalk.red('\n Error: No .utopia/config.json found.'));
|
|
909
|
+
console.log(chalk.dim(' Run "utopia init" first.\n'));
|
|
910
|
+
process.exit(1);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const config = await loadConfig(cwd);
|
|
914
|
+
const purpose = options.purpose as string;
|
|
915
|
+
|
|
916
|
+
console.log(chalk.bold.cyan('\n Utopia Reinstrumentation\n'));
|
|
917
|
+
console.log(chalk.white(` Purpose: ${purpose}\n`));
|
|
918
|
+
|
|
919
|
+
// Ensure runtime is present
|
|
920
|
+
const runtimeExists = config.framework === 'python'
|
|
921
|
+
? existsSync(resolve(cwd, 'utopia_runtime', '__init__.py')) || existsSync(resolve(cwd, '.venv', 'lib'))
|
|
922
|
+
: existsSync(resolve(cwd, 'node_modules', 'utopia-runtime', 'index.js'));
|
|
923
|
+
if (!runtimeExists) {
|
|
924
|
+
console.log(chalk.dim(' Installing utopia-runtime...'));
|
|
925
|
+
const rtResult = installRuntime(cwd, config.framework);
|
|
926
|
+
if (rtResult.ok) {
|
|
927
|
+
console.log(chalk.green(' utopia-runtime installed.'));
|
|
928
|
+
} else {
|
|
929
|
+
console.log(chalk.red(` Error: ${rtResult.error}`));
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Env vars
|
|
934
|
+
try { ensureEnvVars(cwd, config); } catch { /* non-fatal */ }
|
|
935
|
+
|
|
936
|
+
const agentName = config.agent === 'codex' ? 'Codex' : 'Claude Code';
|
|
937
|
+
if (!checkAgentAvailable(config.agent)) {
|
|
938
|
+
console.log(chalk.red(` Error: ${agentName} CLI not found.\n`));
|
|
939
|
+
process.exit(1);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Snapshot files before agent modifies them (preserves original pre-instrument state)
|
|
943
|
+
console.log(chalk.dim(' Snapshotting source files...'));
|
|
944
|
+
snapshotFiles(cwd);
|
|
945
|
+
|
|
946
|
+
console.log(chalk.dim(` Launching ${agentName} for targeted instrumentation...\n`));
|
|
947
|
+
console.log(chalk.bold.white(` --- ${agentName} Session ---\n`));
|
|
948
|
+
|
|
949
|
+
const code = await spawnAgentSession(cwd, buildReinstrumentPrompt(config, purpose), config.agent);
|
|
950
|
+
|
|
951
|
+
console.log(chalk.bold.white(`\n --- End ${agentName} Session ---\n`));
|
|
952
|
+
|
|
953
|
+
pruneUnchangedSnapshots(cwd);
|
|
954
|
+
|
|
955
|
+
if (code === 0) {
|
|
956
|
+
console.log(chalk.bold.green(' Reinstrumentation complete!\n'));
|
|
957
|
+
} else {
|
|
958
|
+
console.log(chalk.yellow(` ${agentName} exited with code ${code}.\n`));
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
console.log(chalk.dim(' Restart your app to activate the new probes.\n'));
|
|
962
|
+
});
|