@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.
Files changed (49) hide show
  1. package/.claude/settings.json +1 -0
  2. package/.claude/settings.local.json +38 -0
  3. package/bin/utopia.js +20 -0
  4. package/package.json +46 -0
  5. package/python/README.md +34 -0
  6. package/python/instrumenter/instrument.py +1148 -0
  7. package/python/pyproject.toml +32 -0
  8. package/python/setup.py +27 -0
  9. package/python/utopia_runtime/__init__.py +30 -0
  10. package/python/utopia_runtime/__pycache__/__init__.cpython-313.pyc +0 -0
  11. package/python/utopia_runtime/__pycache__/client.cpython-313.pyc +0 -0
  12. package/python/utopia_runtime/__pycache__/probe.cpython-313.pyc +0 -0
  13. package/python/utopia_runtime/client.py +31 -0
  14. package/python/utopia_runtime/probe.py +446 -0
  15. package/python/utopia_runtime.egg-info/PKG-INFO +59 -0
  16. package/python/utopia_runtime.egg-info/SOURCES.txt +10 -0
  17. package/python/utopia_runtime.egg-info/dependency_links.txt +1 -0
  18. package/python/utopia_runtime.egg-info/top_level.txt +1 -0
  19. package/scripts/publish-npm.sh +14 -0
  20. package/scripts/publish-pypi.sh +17 -0
  21. package/src/cli/commands/codex.ts +193 -0
  22. package/src/cli/commands/context.ts +188 -0
  23. package/src/cli/commands/destruct.ts +237 -0
  24. package/src/cli/commands/easter-eggs.ts +203 -0
  25. package/src/cli/commands/init.ts +505 -0
  26. package/src/cli/commands/instrument.ts +962 -0
  27. package/src/cli/commands/mcp.ts +16 -0
  28. package/src/cli/commands/serve.ts +194 -0
  29. package/src/cli/commands/status.ts +304 -0
  30. package/src/cli/commands/validate.ts +328 -0
  31. package/src/cli/index.ts +37 -0
  32. package/src/cli/utils/config.ts +54 -0
  33. package/src/graph/index.ts +687 -0
  34. package/src/instrumenter/javascript.ts +1798 -0
  35. package/src/mcp/index.ts +886 -0
  36. package/src/runtime/js/index.ts +518 -0
  37. package/src/runtime/js/package-lock.json +30 -0
  38. package/src/runtime/js/package.json +30 -0
  39. package/src/runtime/js/tsconfig.json +16 -0
  40. package/src/server/db/index.ts +26 -0
  41. package/src/server/db/schema.ts +45 -0
  42. package/src/server/index.ts +79 -0
  43. package/src/server/middleware/auth.ts +74 -0
  44. package/src/server/routes/admin.ts +36 -0
  45. package/src/server/routes/graph.ts +358 -0
  46. package/src/server/routes/probes.ts +286 -0
  47. package/src/types.ts +147 -0
  48. package/src/utopia-mode/index.ts +206 -0
  49. 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
+ });