@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,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
+ }