@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,518 @@
|
|
|
1
|
+
// Utopia JS/TS probe runtime
|
|
2
|
+
// Lightweight, non-blocking probe reporter for instrumented applications.
|
|
3
|
+
// Import as: import { __utopia } from 'utopia-runtime'
|
|
4
|
+
//
|
|
5
|
+
// CRITICAL: Every public method is wrapped in try/catch. This runtime
|
|
6
|
+
// MUST NEVER crash the host application under any circumstances.
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Types
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
interface UtopiaConfig {
|
|
13
|
+
endpoint: string;
|
|
14
|
+
projectId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ProbePayload {
|
|
18
|
+
id: string;
|
|
19
|
+
projectId: string;
|
|
20
|
+
probeType: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
file: string;
|
|
23
|
+
line: number;
|
|
24
|
+
functionName: string;
|
|
25
|
+
data: Record<string, unknown>;
|
|
26
|
+
metadata: {
|
|
27
|
+
runtime: 'node';
|
|
28
|
+
environment?: string;
|
|
29
|
+
hostname?: string;
|
|
30
|
+
pid?: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ErrorProbeData {
|
|
35
|
+
file: string;
|
|
36
|
+
line: number;
|
|
37
|
+
functionName: string;
|
|
38
|
+
errorType: string;
|
|
39
|
+
message: string;
|
|
40
|
+
stack: string;
|
|
41
|
+
inputData: Record<string, unknown>;
|
|
42
|
+
codeLine: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface DbProbeData {
|
|
46
|
+
file: string;
|
|
47
|
+
line: number;
|
|
48
|
+
functionName: string;
|
|
49
|
+
operation: string;
|
|
50
|
+
query?: string;
|
|
51
|
+
table?: string;
|
|
52
|
+
duration: number;
|
|
53
|
+
rowCount?: number;
|
|
54
|
+
connectionInfo?: { type?: string; host?: string; database?: string };
|
|
55
|
+
params?: unknown[];
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ApiProbeData {
|
|
60
|
+
file: string;
|
|
61
|
+
line: number;
|
|
62
|
+
functionName: string;
|
|
63
|
+
method: string;
|
|
64
|
+
url: string;
|
|
65
|
+
statusCode?: number;
|
|
66
|
+
duration: number;
|
|
67
|
+
requestHeaders?: Record<string, string>;
|
|
68
|
+
responseHeaders?: Record<string, string>;
|
|
69
|
+
requestBody?: unknown;
|
|
70
|
+
responseBody?: unknown;
|
|
71
|
+
error?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface InfraProbeData {
|
|
75
|
+
file: string;
|
|
76
|
+
line: number;
|
|
77
|
+
provider: string;
|
|
78
|
+
region?: string;
|
|
79
|
+
serviceType?: string;
|
|
80
|
+
instanceId?: string;
|
|
81
|
+
containerInfo?: { containerId?: string; image?: string };
|
|
82
|
+
envVars: Record<string, string>;
|
|
83
|
+
memoryUsage: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface FunctionProbeData {
|
|
87
|
+
file: string;
|
|
88
|
+
line: number;
|
|
89
|
+
functionName: string;
|
|
90
|
+
args: unknown[];
|
|
91
|
+
returnValue?: unknown;
|
|
92
|
+
duration: number;
|
|
93
|
+
callStack: string[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface LlmContextProbeData {
|
|
97
|
+
file: string;
|
|
98
|
+
line: number;
|
|
99
|
+
functionName: string;
|
|
100
|
+
context: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Internal state
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
let _config: UtopiaConfig | null = null;
|
|
108
|
+
let _queue: ProbePayload[] = [];
|
|
109
|
+
let _flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
110
|
+
let _consecutiveFailures = 0;
|
|
111
|
+
let _circuitOpen = false;
|
|
112
|
+
let _circuitOpenTime = 0;
|
|
113
|
+
|
|
114
|
+
const FLUSH_INTERVAL_MS = 5_000;
|
|
115
|
+
const FLUSH_BATCH_SIZE = 50;
|
|
116
|
+
const CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
117
|
+
const CIRCUIT_BREAKER_COOLDOWN_MS = 60_000;
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Helpers
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generate a UUID v4 without external dependencies.
|
|
125
|
+
* Uses crypto.randomUUID() when available, otherwise falls back to a
|
|
126
|
+
* manual implementation using Math.random().
|
|
127
|
+
*/
|
|
128
|
+
function generateId(): string {
|
|
129
|
+
try {
|
|
130
|
+
// Node 19+ / modern runtimes
|
|
131
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
132
|
+
return crypto.randomUUID();
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// Fall through to manual implementation
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Manual UUID v4 fallback
|
|
139
|
+
const hex = '0123456789abcdef';
|
|
140
|
+
let uuid = '';
|
|
141
|
+
for (let i = 0; i < 36; i++) {
|
|
142
|
+
if (i === 8 || i === 13 || i === 18 || i === 23) {
|
|
143
|
+
uuid += '-';
|
|
144
|
+
} else if (i === 14) {
|
|
145
|
+
uuid += '4'; // Version 4
|
|
146
|
+
} else if (i === 19) {
|
|
147
|
+
uuid += hex[(Math.random() * 4) | 8]; // Variant bits
|
|
148
|
+
} else {
|
|
149
|
+
uuid += hex[(Math.random() * 16) | 0];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return uuid;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build metadata object for probe payloads.
|
|
157
|
+
*/
|
|
158
|
+
function buildMetadata(): ProbePayload['metadata'] {
|
|
159
|
+
const meta: ProbePayload['metadata'] = { runtime: 'node' };
|
|
160
|
+
try {
|
|
161
|
+
if (typeof process !== 'undefined') {
|
|
162
|
+
meta.environment = process.env.NODE_ENV || process.env.UTOPIA_ENV || undefined;
|
|
163
|
+
meta.hostname = process.env.HOSTNAME || undefined;
|
|
164
|
+
meta.pid = process.pid || undefined;
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// Swallow — some environments restrict process access
|
|
168
|
+
}
|
|
169
|
+
return meta;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolve configuration from explicit init or environment variables.
|
|
174
|
+
* Returns null if not enough config is available.
|
|
175
|
+
*/
|
|
176
|
+
function resolveConfig(): UtopiaConfig | null {
|
|
177
|
+
if (_config) return _config;
|
|
178
|
+
try {
|
|
179
|
+
if (typeof process === 'undefined' || !process.env) return null;
|
|
180
|
+
// Check both standard and NEXT_PUBLIC_ prefixed env vars (for Next.js client components)
|
|
181
|
+
const endpoint = process.env.UTOPIA_ENDPOINT || process.env.NEXT_PUBLIC_UTOPIA_ENDPOINT;
|
|
182
|
+
const projectId = process.env.UTOPIA_PROJECT_ID || process.env.NEXT_PUBLIC_UTOPIA_PROJECT_ID;
|
|
183
|
+
if (endpoint && projectId) {
|
|
184
|
+
_config = { endpoint, projectId };
|
|
185
|
+
return _config;
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
// Swallow
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Ensure the periodic flush timer is running.
|
|
195
|
+
*/
|
|
196
|
+
function startFlushTimer(): void {
|
|
197
|
+
try {
|
|
198
|
+
if (_flushTimer) return;
|
|
199
|
+
_flushTimer = setInterval(() => {
|
|
200
|
+
flush();
|
|
201
|
+
}, FLUSH_INTERVAL_MS);
|
|
202
|
+
// Unref so the timer does not prevent process exit
|
|
203
|
+
if (_flushTimer && typeof _flushTimer === 'object' && 'unref' in _flushTimer) {
|
|
204
|
+
(_flushTimer as { unref: () => void }).unref();
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// Never throw
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Create a ProbePayload from probe data and enqueue it.
|
|
213
|
+
*/
|
|
214
|
+
function enqueue(
|
|
215
|
+
probeType: string,
|
|
216
|
+
file: string,
|
|
217
|
+
line: number,
|
|
218
|
+
functionName: string,
|
|
219
|
+
data: Record<string, unknown>
|
|
220
|
+
): void {
|
|
221
|
+
try {
|
|
222
|
+
const cfg = resolveConfig();
|
|
223
|
+
const payload: ProbePayload = {
|
|
224
|
+
id: generateId(),
|
|
225
|
+
projectId: cfg?.projectId || '',
|
|
226
|
+
probeType,
|
|
227
|
+
timestamp: new Date().toISOString(),
|
|
228
|
+
file,
|
|
229
|
+
line,
|
|
230
|
+
functionName,
|
|
231
|
+
data,
|
|
232
|
+
metadata: buildMetadata(),
|
|
233
|
+
};
|
|
234
|
+
_queue.push(payload);
|
|
235
|
+
// Flush immediately if batch is full
|
|
236
|
+
if (_queue.length >= FLUSH_BATCH_SIZE) {
|
|
237
|
+
flush();
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// Never throw
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Flush queued probe payloads to the Utopia endpoint.
|
|
246
|
+
* Respects circuit breaker state.
|
|
247
|
+
*/
|
|
248
|
+
async function flush(): Promise<void> {
|
|
249
|
+
try {
|
|
250
|
+
const cfg = resolveConfig();
|
|
251
|
+
if (!cfg) return;
|
|
252
|
+
if (_queue.length === 0) return;
|
|
253
|
+
|
|
254
|
+
// Circuit breaker: if open, check cooldown
|
|
255
|
+
if (_circuitOpen) {
|
|
256
|
+
if (Date.now() < _circuitOpenTime + CIRCUIT_BREAKER_COOLDOWN_MS) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Cooldown has elapsed — allow a single retry
|
|
260
|
+
_circuitOpen = false;
|
|
261
|
+
_consecutiveFailures = 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const batch = _queue.splice(0, FLUSH_BATCH_SIZE);
|
|
265
|
+
|
|
266
|
+
const body = JSON.stringify(batch);
|
|
267
|
+
|
|
268
|
+
const response = await fetch(`${cfg.endpoint}/api/v1/probes`, {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
headers: {
|
|
271
|
+
'Content-Type': 'application/json',
|
|
272
|
+
},
|
|
273
|
+
body,
|
|
274
|
+
signal: AbortSignal.timeout(10_000),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
_consecutiveFailures++;
|
|
279
|
+
if (_consecutiveFailures >= CIRCUIT_BREAKER_THRESHOLD) {
|
|
280
|
+
_circuitOpen = true;
|
|
281
|
+
_circuitOpenTime = Date.now();
|
|
282
|
+
}
|
|
283
|
+
// Put items back at front of queue so they are not lost
|
|
284
|
+
_queue.unshift(...batch);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Success — reset failure tracking
|
|
289
|
+
_consecutiveFailures = 0;
|
|
290
|
+
} catch {
|
|
291
|
+
_consecutiveFailures++;
|
|
292
|
+
if (_consecutiveFailures >= CIRCUIT_BREAKER_THRESHOLD) {
|
|
293
|
+
_circuitOpen = true;
|
|
294
|
+
_circuitOpenTime = Date.now();
|
|
295
|
+
}
|
|
296
|
+
// Never throw
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Public API
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Explicitly initialise the Utopia runtime with connection details.
|
|
306
|
+
* If not called, the runtime will auto-initialise from environment variables
|
|
307
|
+
* on the first probe report.
|
|
308
|
+
*/
|
|
309
|
+
function init(config: { endpoint: string; projectId: string }): void {
|
|
310
|
+
try {
|
|
311
|
+
_config = {
|
|
312
|
+
endpoint: config.endpoint,
|
|
313
|
+
projectId: config.projectId,
|
|
314
|
+
};
|
|
315
|
+
startFlushTimer();
|
|
316
|
+
} catch {
|
|
317
|
+
// Never throw
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Report an error caught by an instrumented function.
|
|
323
|
+
*/
|
|
324
|
+
function reportError(probeData: ErrorProbeData): void {
|
|
325
|
+
try {
|
|
326
|
+
startFlushTimer();
|
|
327
|
+
enqueue(
|
|
328
|
+
'error',
|
|
329
|
+
probeData.file,
|
|
330
|
+
probeData.line,
|
|
331
|
+
probeData.functionName,
|
|
332
|
+
{
|
|
333
|
+
errorType: probeData.errorType,
|
|
334
|
+
message: probeData.message,
|
|
335
|
+
stack: probeData.stack,
|
|
336
|
+
inputData: probeData.inputData,
|
|
337
|
+
codeLine: probeData.codeLine,
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
} catch {
|
|
341
|
+
// Never throw
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Report a database operation observed by an instrumented call site.
|
|
347
|
+
*/
|
|
348
|
+
function reportDb(probeData: DbProbeData): void {
|
|
349
|
+
try {
|
|
350
|
+
startFlushTimer();
|
|
351
|
+
enqueue(
|
|
352
|
+
'database',
|
|
353
|
+
probeData.file,
|
|
354
|
+
probeData.line,
|
|
355
|
+
probeData.functionName,
|
|
356
|
+
{
|
|
357
|
+
operation: probeData.operation,
|
|
358
|
+
query: probeData.query,
|
|
359
|
+
table: probeData.table,
|
|
360
|
+
duration: probeData.duration,
|
|
361
|
+
rowCount: probeData.rowCount,
|
|
362
|
+
connectionInfo: probeData.connectionInfo,
|
|
363
|
+
params: probeData.params,
|
|
364
|
+
error: probeData.error,
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
} catch {
|
|
368
|
+
// Never throw
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Report an HTTP API call observed by an instrumented call site.
|
|
374
|
+
*/
|
|
375
|
+
function reportApi(probeData: ApiProbeData): void {
|
|
376
|
+
try {
|
|
377
|
+
startFlushTimer();
|
|
378
|
+
enqueue(
|
|
379
|
+
'api',
|
|
380
|
+
probeData.file,
|
|
381
|
+
probeData.line,
|
|
382
|
+
probeData.functionName,
|
|
383
|
+
{
|
|
384
|
+
method: probeData.method,
|
|
385
|
+
url: probeData.url,
|
|
386
|
+
statusCode: probeData.statusCode,
|
|
387
|
+
duration: probeData.duration,
|
|
388
|
+
requestHeaders: probeData.requestHeaders,
|
|
389
|
+
responseHeaders: probeData.responseHeaders,
|
|
390
|
+
requestBody: probeData.requestBody,
|
|
391
|
+
responseBody: probeData.responseBody,
|
|
392
|
+
error: probeData.error,
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
} catch {
|
|
396
|
+
// Never throw
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Report infrastructure / deployment context from an entry point file.
|
|
402
|
+
*/
|
|
403
|
+
function reportInfra(probeData: InfraProbeData): void {
|
|
404
|
+
try {
|
|
405
|
+
startFlushTimer();
|
|
406
|
+
enqueue(
|
|
407
|
+
'infra',
|
|
408
|
+
probeData.file,
|
|
409
|
+
probeData.line,
|
|
410
|
+
'<module>',
|
|
411
|
+
{
|
|
412
|
+
provider: probeData.provider,
|
|
413
|
+
region: probeData.region,
|
|
414
|
+
serviceType: probeData.serviceType,
|
|
415
|
+
instanceId: probeData.instanceId,
|
|
416
|
+
containerInfo: probeData.containerInfo,
|
|
417
|
+
envVars: probeData.envVars,
|
|
418
|
+
memoryUsage: probeData.memoryUsage,
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
} catch {
|
|
422
|
+
// Never throw
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Report function-level profiling data.
|
|
428
|
+
*/
|
|
429
|
+
function reportFunction(probeData: FunctionProbeData): void {
|
|
430
|
+
try {
|
|
431
|
+
startFlushTimer();
|
|
432
|
+
enqueue(
|
|
433
|
+
'function',
|
|
434
|
+
probeData.file,
|
|
435
|
+
probeData.line,
|
|
436
|
+
probeData.functionName,
|
|
437
|
+
{
|
|
438
|
+
args: probeData.args,
|
|
439
|
+
returnValue: probeData.returnValue,
|
|
440
|
+
duration: probeData.duration,
|
|
441
|
+
callStack: probeData.callStack,
|
|
442
|
+
}
|
|
443
|
+
);
|
|
444
|
+
} catch {
|
|
445
|
+
// Never throw
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Report LLM-generated context about a function (Utopia mode).
|
|
451
|
+
*/
|
|
452
|
+
function reportLlmContext(probeData: LlmContextProbeData): void {
|
|
453
|
+
try {
|
|
454
|
+
startFlushTimer();
|
|
455
|
+
enqueue(
|
|
456
|
+
'llm_context',
|
|
457
|
+
probeData.file,
|
|
458
|
+
probeData.line,
|
|
459
|
+
probeData.functionName,
|
|
460
|
+
{
|
|
461
|
+
context: probeData.context,
|
|
462
|
+
}
|
|
463
|
+
);
|
|
464
|
+
} catch {
|
|
465
|
+
// Never throw
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Force an immediate flush of all queued probes.
|
|
471
|
+
* Useful before process exit.
|
|
472
|
+
*/
|
|
473
|
+
async function shutdown(): Promise<void> {
|
|
474
|
+
try {
|
|
475
|
+
if (_flushTimer) {
|
|
476
|
+
clearInterval(_flushTimer);
|
|
477
|
+
_flushTimer = null;
|
|
478
|
+
}
|
|
479
|
+
// Flush remaining items in batches
|
|
480
|
+
while (_queue.length > 0) {
|
|
481
|
+
await flush();
|
|
482
|
+
}
|
|
483
|
+
} catch {
|
|
484
|
+
// Never throw
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// Exported object and named exports
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
export const __utopia = {
|
|
493
|
+
init,
|
|
494
|
+
reportError,
|
|
495
|
+
reportDb,
|
|
496
|
+
reportApi,
|
|
497
|
+
reportInfra,
|
|
498
|
+
reportFunction,
|
|
499
|
+
reportLlmContext,
|
|
500
|
+
flush,
|
|
501
|
+
shutdown,
|
|
502
|
+
generateId,
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
export {
|
|
506
|
+
init,
|
|
507
|
+
reportError,
|
|
508
|
+
reportDb,
|
|
509
|
+
reportApi,
|
|
510
|
+
reportInfra,
|
|
511
|
+
reportFunction,
|
|
512
|
+
reportLlmContext,
|
|
513
|
+
flush,
|
|
514
|
+
shutdown,
|
|
515
|
+
generateId,
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
export default __utopia;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "utopia-runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "utopia-runtime",
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"typescript": "^5.7.0"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"node_modules/typescript": {
|
|
16
|
+
"version": "5.9.3",
|
|
17
|
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
18
|
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
19
|
+
"dev": true,
|
|
20
|
+
"license": "Apache-2.0",
|
|
21
|
+
"bin": {
|
|
22
|
+
"tsc": "bin/tsc",
|
|
23
|
+
"tsserver": "bin/tsserver"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=14.17"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "utopia-runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-impact production probe runtime for Utopia — gives AI coding agents real-time visibility into how code runs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": ["utopia", "probes", "observability", "production-context", "ai-agents"],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/paulvann/utopia"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"typescript": "^5.7.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["index.ts"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { initSchema } from './schema.js';
|
|
3
|
+
|
|
4
|
+
let db: Database.Database | null = null;
|
|
5
|
+
|
|
6
|
+
export function initDb(dbPath: string): Database.Database {
|
|
7
|
+
db = new Database(dbPath);
|
|
8
|
+
db.pragma('journal_mode = WAL');
|
|
9
|
+
db.pragma('foreign_keys = ON');
|
|
10
|
+
initSchema(db);
|
|
11
|
+
return db;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getDb(): Database.Database {
|
|
15
|
+
if (!db) {
|
|
16
|
+
throw new Error('Database not initialized. Call initDb() first.');
|
|
17
|
+
}
|
|
18
|
+
return db;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function closeDb(): void {
|
|
22
|
+
if (db) {
|
|
23
|
+
db.close();
|
|
24
|
+
db = null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
|
|
3
|
+
export function initSchema(db: Database.Database): void {
|
|
4
|
+
db.exec(`
|
|
5
|
+
CREATE TABLE IF NOT EXISTS probes (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
project_id TEXT NOT NULL,
|
|
8
|
+
probe_type TEXT NOT NULL CHECK(probe_type IN ('error','database','api','infra','function')),
|
|
9
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10
|
+
file TEXT NOT NULL,
|
|
11
|
+
line INTEGER NOT NULL,
|
|
12
|
+
function_name TEXT NOT NULL DEFAULT '',
|
|
13
|
+
data TEXT NOT NULL DEFAULT '{}',
|
|
14
|
+
metadata TEXT NOT NULL DEFAULT '{}'
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_probes_type ON probes(probe_type);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_probes_file ON probes(file);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_probes_timestamp ON probes(timestamp);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_probes_project ON probes(project_id);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_probes_function ON probes(function_name);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS graph_nodes (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
type TEXT NOT NULL CHECK(type IN ('function','service','database','api','file')),
|
|
26
|
+
name TEXT NOT NULL,
|
|
27
|
+
file TEXT,
|
|
28
|
+
metadata TEXT NOT NULL DEFAULT '{}'
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS graph_edges (
|
|
32
|
+
source TEXT NOT NULL,
|
|
33
|
+
target TEXT NOT NULL,
|
|
34
|
+
type TEXT NOT NULL CHECK(type IN ('calls','queries','serves','depends_on')),
|
|
35
|
+
weight INTEGER NOT NULL DEFAULT 1,
|
|
36
|
+
last_seen TEXT NOT NULL DEFAULT (datetime('now')),
|
|
37
|
+
PRIMARY KEY (source, target, type),
|
|
38
|
+
FOREIGN KEY (source) REFERENCES graph_nodes(id),
|
|
39
|
+
FOREIGN KEY (target) REFERENCES graph_nodes(id)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_edges_source ON graph_edges(source);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_edges_target ON graph_edges(target);
|
|
44
|
+
`);
|
|
45
|
+
}
|