@lakitu/sdk 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 (111) hide show
  1. package/README.md +166 -0
  2. package/convex/_generated/api.d.ts +45 -0
  3. package/convex/_generated/api.js +23 -0
  4. package/convex/_generated/dataModel.d.ts +58 -0
  5. package/convex/_generated/server.d.ts +143 -0
  6. package/convex/_generated/server.js +93 -0
  7. package/convex/cloud/CLAUDE.md +238 -0
  8. package/convex/cloud/_generated/api.ts +84 -0
  9. package/convex/cloud/_generated/component.ts +861 -0
  10. package/convex/cloud/_generated/dataModel.ts +60 -0
  11. package/convex/cloud/_generated/server.ts +156 -0
  12. package/convex/cloud/convex.config.ts +16 -0
  13. package/convex/cloud/index.ts +29 -0
  14. package/convex/cloud/intentSchema/generate.ts +447 -0
  15. package/convex/cloud/intentSchema/index.ts +16 -0
  16. package/convex/cloud/intentSchema/types.ts +418 -0
  17. package/convex/cloud/ksaPolicy.ts +554 -0
  18. package/convex/cloud/mail.ts +92 -0
  19. package/convex/cloud/schema.ts +322 -0
  20. package/convex/cloud/utils/kanbanContext.ts +229 -0
  21. package/convex/cloud/workflows/agentBoard.ts +451 -0
  22. package/convex/cloud/workflows/agentPrompt.ts +272 -0
  23. package/convex/cloud/workflows/agentThread.ts +374 -0
  24. package/convex/cloud/workflows/compileSandbox.ts +146 -0
  25. package/convex/cloud/workflows/crudBoard.ts +217 -0
  26. package/convex/cloud/workflows/crudKSAs.ts +262 -0
  27. package/convex/cloud/workflows/crudLorobeads.ts +371 -0
  28. package/convex/cloud/workflows/crudSkills.ts +205 -0
  29. package/convex/cloud/workflows/crudThreads.ts +708 -0
  30. package/convex/cloud/workflows/lifecycleSandbox.ts +1396 -0
  31. package/convex/cloud/workflows/sandboxConvex.ts +1046 -0
  32. package/convex/sandbox/README.md +90 -0
  33. package/convex/sandbox/_generated/api.d.ts +2934 -0
  34. package/convex/sandbox/_generated/api.js +23 -0
  35. package/convex/sandbox/_generated/dataModel.d.ts +60 -0
  36. package/convex/sandbox/_generated/server.d.ts +143 -0
  37. package/convex/sandbox/_generated/server.js +93 -0
  38. package/convex/sandbox/actions/bash.ts +130 -0
  39. package/convex/sandbox/actions/browser.ts +282 -0
  40. package/convex/sandbox/actions/file.ts +336 -0
  41. package/convex/sandbox/actions/lsp.ts +325 -0
  42. package/convex/sandbox/actions/pdf.ts +119 -0
  43. package/convex/sandbox/agent/codeExecLoop.ts +535 -0
  44. package/convex/sandbox/agent/decisions.ts +284 -0
  45. package/convex/sandbox/agent/index.ts +515 -0
  46. package/convex/sandbox/agent/subagents.ts +651 -0
  47. package/convex/sandbox/brandResearch/index.ts +417 -0
  48. package/convex/sandbox/context/index.ts +7 -0
  49. package/convex/sandbox/context/session.ts +402 -0
  50. package/convex/sandbox/convex.config.ts +17 -0
  51. package/convex/sandbox/index.ts +51 -0
  52. package/convex/sandbox/nodeActions/codeExec.ts +130 -0
  53. package/convex/sandbox/planning/beads.ts +187 -0
  54. package/convex/sandbox/planning/index.ts +8 -0
  55. package/convex/sandbox/planning/sync.ts +194 -0
  56. package/convex/sandbox/prompts/codeExec.ts +852 -0
  57. package/convex/sandbox/prompts/modes.ts +231 -0
  58. package/convex/sandbox/prompts/system.ts +142 -0
  59. package/convex/sandbox/schema.ts +510 -0
  60. package/convex/sandbox/state/artifacts.ts +99 -0
  61. package/convex/sandbox/state/checkpoints.ts +341 -0
  62. package/convex/sandbox/state/files.ts +383 -0
  63. package/convex/sandbox/state/index.ts +10 -0
  64. package/convex/sandbox/state/verification.actions.ts +268 -0
  65. package/convex/sandbox/state/verification.ts +101 -0
  66. package/convex/sandbox/tsconfig.json +25 -0
  67. package/convex/sandbox/utils/codeExecHelpers.ts +52 -0
  68. package/dist/cli/commands/build.d.ts +19 -0
  69. package/dist/cli/commands/build.d.ts.map +1 -0
  70. package/dist/cli/commands/build.js +223 -0
  71. package/dist/cli/commands/init.d.ts +16 -0
  72. package/dist/cli/commands/init.d.ts.map +1 -0
  73. package/dist/cli/commands/init.js +148 -0
  74. package/dist/cli/commands/publish.d.ts +12 -0
  75. package/dist/cli/commands/publish.d.ts.map +1 -0
  76. package/dist/cli/commands/publish.js +33 -0
  77. package/dist/cli/index.d.ts +14 -0
  78. package/dist/cli/index.d.ts.map +1 -0
  79. package/dist/cli/index.js +40 -0
  80. package/dist/sdk/builders.d.ts +104 -0
  81. package/dist/sdk/builders.d.ts.map +1 -0
  82. package/dist/sdk/builders.js +214 -0
  83. package/dist/sdk/index.d.ts +29 -0
  84. package/dist/sdk/index.d.ts.map +1 -0
  85. package/dist/sdk/index.js +38 -0
  86. package/dist/sdk/types.d.ts +107 -0
  87. package/dist/sdk/types.d.ts.map +1 -0
  88. package/dist/sdk/types.js +6 -0
  89. package/ksa/README.md +263 -0
  90. package/ksa/_generated/REFERENCE.md +2954 -0
  91. package/ksa/_generated/registry.ts +257 -0
  92. package/ksa/_shared/configReader.ts +302 -0
  93. package/ksa/_shared/configSchemas.ts +649 -0
  94. package/ksa/_shared/gateway.ts +175 -0
  95. package/ksa/_shared/ksaBehaviors.ts +411 -0
  96. package/ksa/_shared/ksaProxy.ts +248 -0
  97. package/ksa/_shared/localDb.ts +302 -0
  98. package/ksa/index.ts +134 -0
  99. package/package.json +93 -0
  100. package/runtime/browser/agent-browser.ts +330 -0
  101. package/runtime/entrypoint.ts +194 -0
  102. package/runtime/lsp/manager.ts +366 -0
  103. package/runtime/pdf/pdf-generator.ts +50 -0
  104. package/runtime/pdf/renderer.ts +357 -0
  105. package/runtime/pdf/schema.ts +97 -0
  106. package/runtime/services/file-watcher.ts +191 -0
  107. package/template/build.ts +307 -0
  108. package/template/e2b/Dockerfile +69 -0
  109. package/template/e2b/e2b.toml +13 -0
  110. package/template/e2b/prebuild.sh +68 -0
  111. package/template/e2b/start.sh +14 -0
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "@lakitu/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Self-hosted AI agent framework for Convex + E2B with code execution",
5
+ "type": "module",
6
+ "main": "./dist/sdk/index.js",
7
+ "types": "./dist/sdk/index.d.ts",
8
+ "bin": {
9
+ "lakitu": "./dist/cli/index.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/sdk/index.d.ts",
14
+ "import": "./dist/sdk/index.js"
15
+ },
16
+ "./types": {
17
+ "types": "./dist/sdk/types.d.ts",
18
+ "import": "./dist/sdk/types.js"
19
+ },
20
+ "./builders": {
21
+ "types": "./dist/sdk/builders.d.ts",
22
+ "import": "./dist/sdk/builders.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "convex",
28
+ "ksa",
29
+ "runtime",
30
+ "template",
31
+ "README.md"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "prepublishOnly": "bun run build",
36
+ "dev": "convex dev",
37
+ "deploy": "convex deploy",
38
+ "generate": "convex codegen",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "test:ui": "vitest --ui",
42
+ "test:convex": "convex run tests/runner:runAll",
43
+ "test:integration": "bun run tests/integration/run.ts",
44
+ "typecheck": "tsc --noEmit",
45
+ "template:build": "bun template/build.ts",
46
+ "template:push": "bun template/build.ts --push",
47
+ "template:base": "bun template/build.ts --base --push",
48
+ "template:custom": "bun template/build.ts --custom --push"
49
+ },
50
+ "keywords": [
51
+ "ai",
52
+ "agent",
53
+ "ksa",
54
+ "convex",
55
+ "e2b",
56
+ "sandbox",
57
+ "code-execution",
58
+ "self-hosted"
59
+ ],
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "https://github.com/project-social/lakitu"
63
+ },
64
+ "license": "MIT",
65
+ "dependencies": {
66
+ "commander": "^12.1.0",
67
+ "e2b": "^2.10.1",
68
+ "zod": "^3.23.0"
69
+ },
70
+ "devDependencies": {
71
+ "@anthropic-ai/sdk": "^0.27.0",
72
+ "@ai-sdk/openai": "^3.0.10",
73
+ "@openrouter/ai-sdk-provider": "^1.5.4",
74
+ "@convex-dev/agent": "^0.2.0",
75
+ "@types/node": "^20.0.0",
76
+ "@types/pdfkit": "^0.13.0",
77
+ "ai": "^6.0.34",
78
+ "convex": "^1.17.0",
79
+ "convex-test": "^0.0.34",
80
+ "loro-crdt": "^1.0.0",
81
+ "openapi-fetch": "^0.10.0",
82
+ "pdfkit": "^0.15.0",
83
+ "playwright": "^1.48.0",
84
+ "typescript": "^5.6.0",
85
+ "vitest": "^2.0.0",
86
+ "yaml": "^2.4.5",
87
+ "zod-to-json-schema": "^3.23.0"
88
+ },
89
+ "peerDependencies": {
90
+ "convex": "^1.0.0",
91
+ "zod": "^3.0.0"
92
+ }
93
+ }
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Agent Browser - Vercel agent-browser Wrapper
3
+ *
4
+ * Provides browser automation capabilities using the agent-browser CLI.
5
+ */
6
+
7
+ import { spawn, execSync } from "child_process";
8
+
9
+ export interface BrowserSession {
10
+ sessionId: string;
11
+ status: "idle" | "navigating" | "ready" | "error";
12
+ currentUrl?: string;
13
+ error?: string;
14
+ }
15
+
16
+ export interface ElementRef {
17
+ ref: string; // e.g., "@e1"
18
+ tag: string;
19
+ text?: string;
20
+ role?: string;
21
+ attributes: Record<string, string>;
22
+ }
23
+
24
+ export interface PageSnapshot {
25
+ url: string;
26
+ title: string;
27
+ elements: ElementRef[];
28
+ screenshot?: string; // Base64 encoded
29
+ }
30
+
31
+ class AgentBrowser {
32
+ private session: BrowserSession | null = null;
33
+
34
+ /**
35
+ * Start a new browser session
36
+ */
37
+ async start(): Promise<string> {
38
+ const sessionId = `browser_${Date.now()}`;
39
+
40
+ this.session = {
41
+ sessionId,
42
+ status: "idle",
43
+ };
44
+
45
+ return sessionId;
46
+ }
47
+
48
+ /**
49
+ * Navigate to a URL
50
+ */
51
+ async navigate(url: string): Promise<{ success: boolean; error?: string }> {
52
+ if (!this.session) {
53
+ return { success: false, error: "No active session" };
54
+ }
55
+
56
+ try {
57
+ this.session.status = "navigating";
58
+
59
+ const result = execSync(`agent-browser open "${url}"`, {
60
+ encoding: "utf8",
61
+ timeout: 30000,
62
+ env: {
63
+ ...process.env,
64
+ HOME: "/home/user",
65
+ },
66
+ });
67
+
68
+ this.session.status = "ready";
69
+ this.session.currentUrl = url;
70
+
71
+ return { success: true };
72
+ } catch (error: any) {
73
+ this.session.status = "error";
74
+ this.session.error = error.message;
75
+ return { success: false, error: error.message };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Get a snapshot of the current page with interactive elements
81
+ */
82
+ async snapshot(options?: { interactive?: boolean }): Promise<PageSnapshot | null> {
83
+ if (!this.session || this.session.status !== "ready") {
84
+ return null;
85
+ }
86
+
87
+ try {
88
+ const args = ["snapshot"];
89
+ if (options?.interactive !== false) {
90
+ args.push("--interactive");
91
+ }
92
+
93
+ const result = execSync(`agent-browser ${args.join(" ")}`, {
94
+ encoding: "utf8",
95
+ timeout: 15000,
96
+ env: {
97
+ ...process.env,
98
+ HOME: "/home/user",
99
+ },
100
+ });
101
+
102
+ // Parse the snapshot output
103
+ // agent-browser returns a structured format with element refs
104
+ return this.parseSnapshot(result);
105
+ } catch (error: any) {
106
+ console.error("[agent-browser] Snapshot failed:", error.message);
107
+ return null;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Click an element by reference
113
+ */
114
+ async click(ref: string): Promise<{ success: boolean; error?: string }> {
115
+ if (!this.session || this.session.status !== "ready") {
116
+ return { success: false, error: "No active session or page not ready" };
117
+ }
118
+
119
+ try {
120
+ execSync(`agent-browser click "${ref}"`, {
121
+ encoding: "utf8",
122
+ timeout: 10000,
123
+ env: {
124
+ ...process.env,
125
+ HOME: "/home/user",
126
+ },
127
+ });
128
+
129
+ return { success: true };
130
+ } catch (error: any) {
131
+ return { success: false, error: error.message };
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Type text into the focused element
137
+ */
138
+ async type(text: string): Promise<{ success: boolean; error?: string }> {
139
+ if (!this.session || this.session.status !== "ready") {
140
+ return { success: false, error: "No active session or page not ready" };
141
+ }
142
+
143
+ try {
144
+ // Escape the text for shell
145
+ const escaped = text.replace(/"/g, '\\"');
146
+ execSync(`agent-browser type "${escaped}"`, {
147
+ encoding: "utf8",
148
+ timeout: 10000,
149
+ env: {
150
+ ...process.env,
151
+ HOME: "/home/user",
152
+ },
153
+ });
154
+
155
+ return { success: true };
156
+ } catch (error: any) {
157
+ return { success: false, error: error.message };
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Press a key (Enter, Tab, Escape, etc.)
163
+ */
164
+ async press(key: string): Promise<{ success: boolean; error?: string }> {
165
+ if (!this.session || this.session.status !== "ready") {
166
+ return { success: false, error: "No active session or page not ready" };
167
+ }
168
+
169
+ try {
170
+ execSync(`agent-browser press "${key}"`, {
171
+ encoding: "utf8",
172
+ timeout: 5000,
173
+ env: {
174
+ ...process.env,
175
+ HOME: "/home/user",
176
+ },
177
+ });
178
+
179
+ return { success: true };
180
+ } catch (error: any) {
181
+ return { success: false, error: error.message };
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Take a screenshot
187
+ */
188
+ async screenshot(): Promise<string | null> {
189
+ if (!this.session || this.session.status !== "ready") {
190
+ return null;
191
+ }
192
+
193
+ try {
194
+ const result = execSync(`agent-browser screenshot --format base64`, {
195
+ encoding: "utf8",
196
+ timeout: 10000,
197
+ maxBuffer: 50 * 1024 * 1024, // 50MB for large screenshots
198
+ env: {
199
+ ...process.env,
200
+ HOME: "/home/user",
201
+ },
202
+ });
203
+
204
+ return result.trim();
205
+ } catch (error: any) {
206
+ console.error("[agent-browser] Screenshot failed:", error.message);
207
+ return null;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Scroll the page
213
+ */
214
+ async scroll(direction: "up" | "down" | "top" | "bottom"): Promise<{ success: boolean }> {
215
+ if (!this.session || this.session.status !== "ready") {
216
+ return { success: false };
217
+ }
218
+
219
+ try {
220
+ execSync(`agent-browser scroll ${direction}`, {
221
+ encoding: "utf8",
222
+ timeout: 5000,
223
+ env: {
224
+ ...process.env,
225
+ HOME: "/home/user",
226
+ },
227
+ });
228
+
229
+ return { success: true };
230
+ } catch {
231
+ return { success: false };
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Wait for navigation or element
237
+ */
238
+ async wait(options: { timeout?: number; selector?: string }): Promise<{ success: boolean }> {
239
+ const timeout = options.timeout || 5000;
240
+
241
+ try {
242
+ if (options.selector) {
243
+ execSync(`agent-browser wait "${options.selector}" --timeout ${timeout}`, {
244
+ encoding: "utf8",
245
+ timeout: timeout + 1000,
246
+ env: {
247
+ ...process.env,
248
+ HOME: "/home/user",
249
+ },
250
+ });
251
+ } else {
252
+ // Just wait for any pending navigation
253
+ await new Promise((resolve) => setTimeout(resolve, timeout));
254
+ }
255
+
256
+ return { success: true };
257
+ } catch {
258
+ return { success: false };
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Close the browser session
264
+ */
265
+ async close(): Promise<void> {
266
+ if (!this.session) return;
267
+
268
+ try {
269
+ execSync("agent-browser close", {
270
+ encoding: "utf8",
271
+ timeout: 5000,
272
+ env: {
273
+ ...process.env,
274
+ HOME: "/home/user",
275
+ },
276
+ });
277
+ } catch {
278
+ // Ignore errors on close
279
+ }
280
+
281
+ this.session = null;
282
+ }
283
+
284
+ /**
285
+ * Get current session status
286
+ */
287
+ getStatus(): BrowserSession | null {
288
+ return this.session;
289
+ }
290
+
291
+ private parseSnapshot(output: string): PageSnapshot {
292
+ // Parse the agent-browser snapshot output
293
+ // This is a simplified parser - real implementation depends on actual output format
294
+ const lines = output.split("\n");
295
+ const elements: ElementRef[] = [];
296
+
297
+ let url = "";
298
+ let title = "";
299
+
300
+ for (const line of lines) {
301
+ // Parse URL
302
+ if (line.startsWith("URL:")) {
303
+ url = line.slice(4).trim();
304
+ continue;
305
+ }
306
+
307
+ // Parse title
308
+ if (line.startsWith("Title:")) {
309
+ title = line.slice(6).trim();
310
+ continue;
311
+ }
312
+
313
+ // Parse element refs like "@e1 button[Login]"
314
+ const refMatch = line.match(/^(@e\d+)\s+(\w+)(?:\[(.+?)\])?/);
315
+ if (refMatch) {
316
+ elements.push({
317
+ ref: refMatch[1],
318
+ tag: refMatch[2],
319
+ text: refMatch[3],
320
+ attributes: {},
321
+ });
322
+ }
323
+ }
324
+
325
+ return { url, title, elements };
326
+ }
327
+ }
328
+
329
+ // Singleton instance
330
+ export const agentBrowser = new AgentBrowser();
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Sandbox Agent Entrypoint
4
+ *
5
+ * Main startup script for the sandbox environment.
6
+ * Starts all required services and waits for readiness.
7
+ */
8
+
9
+ import { spawn, type ChildProcess } from "child_process";
10
+
11
+ const CONVEX_PORT = 3210;
12
+ const CONVEX_SITE_PORT = 3211;
13
+
14
+ interface ServiceStatus {
15
+ name: string;
16
+ pid?: number;
17
+ status: "starting" | "running" | "failed";
18
+ error?: string;
19
+ }
20
+
21
+ const services: Map<string, ServiceStatus> = new Map();
22
+
23
+ async function startConvexBackend(): Promise<void> {
24
+ console.log("[entrypoint] Starting Convex backend...");
25
+
26
+ const proc = spawn("convex-backend", ["--port", String(CONVEX_PORT), "--site-port", String(CONVEX_SITE_PORT)], {
27
+ env: {
28
+ ...process.env,
29
+ HOME: "/home/user",
30
+ },
31
+ stdio: ["ignore", "pipe", "pipe"],
32
+ });
33
+
34
+ services.set("convex", {
35
+ name: "convex",
36
+ pid: proc.pid,
37
+ status: "starting",
38
+ });
39
+
40
+ proc.stdout?.on("data", (data) => {
41
+ const line = data.toString().trim();
42
+ if (line.includes("Listening")) {
43
+ services.set("convex", { ...services.get("convex")!, status: "running" });
44
+ console.log("[entrypoint] Convex backend ready");
45
+ }
46
+ });
47
+
48
+ proc.stderr?.on("data", (data) => {
49
+ console.error("[convex]", data.toString().trim());
50
+ });
51
+
52
+ proc.on("error", (err) => {
53
+ services.set("convex", { ...services.get("convex")!, status: "failed", error: err.message });
54
+ console.error("[entrypoint] Convex backend failed:", err.message);
55
+ });
56
+
57
+ // Wait for backend to be ready
58
+ await waitForService("convex", 30000);
59
+ }
60
+
61
+ async function startLspServers(): Promise<void> {
62
+ console.log("[entrypoint] Starting LSP servers...");
63
+
64
+ // TypeScript LSP is started on-demand
65
+ services.set("typescript-lsp", {
66
+ name: "typescript-lsp",
67
+ status: "running", // Available but not started until needed
68
+ });
69
+
70
+ // Python LSP is started on-demand
71
+ services.set("python-lsp", {
72
+ name: "python-lsp",
73
+ status: "running",
74
+ });
75
+
76
+ // Rust LSP is started on-demand
77
+ services.set("rust-lsp", {
78
+ name: "rust-lsp",
79
+ status: "running",
80
+ });
81
+
82
+ console.log("[entrypoint] LSP servers available (on-demand)");
83
+ }
84
+
85
+ async function startFileWatcher(): Promise<void> {
86
+ console.log("[entrypoint] Starting file watcher...");
87
+
88
+ const watcherPath = "/home/user/lakitu/runtime/services/file-watcher.ts";
89
+
90
+ try {
91
+ // Check if file exists
92
+ const fs = await import("fs/promises");
93
+ await fs.access(watcherPath);
94
+
95
+ const proc = spawn("bun", ["run", watcherPath], {
96
+ env: process.env,
97
+ stdio: ["ignore", "pipe", "pipe"],
98
+ });
99
+
100
+ services.set("file-watcher", {
101
+ name: "file-watcher",
102
+ pid: proc.pid,
103
+ status: "running",
104
+ });
105
+
106
+ proc.stdout?.on("data", (data) => {
107
+ console.log("[file-watcher]", data.toString().trim());
108
+ });
109
+
110
+ proc.stderr?.on("data", (data) => {
111
+ console.error("[file-watcher]", data.toString().trim());
112
+ });
113
+
114
+ console.log("[entrypoint] File watcher started");
115
+ } catch {
116
+ console.log("[entrypoint] File watcher not found, skipping");
117
+ services.set("file-watcher", {
118
+ name: "file-watcher",
119
+ status: "running", // Mark as running (optional service)
120
+ });
121
+ }
122
+ }
123
+
124
+ async function waitForService(name: string, timeoutMs: number): Promise<void> {
125
+ const start = Date.now();
126
+
127
+ while (Date.now() - start < timeoutMs) {
128
+ const service = services.get(name);
129
+ if (service?.status === "running") {
130
+ return;
131
+ }
132
+ if (service?.status === "failed") {
133
+ throw new Error(`Service ${name} failed: ${service.error}`);
134
+ }
135
+ await new Promise((resolve) => setTimeout(resolve, 100));
136
+ }
137
+
138
+ throw new Error(`Service ${name} timed out after ${timeoutMs}ms`);
139
+ }
140
+
141
+ async function healthCheck(): Promise<boolean> {
142
+ // Check Convex backend
143
+ try {
144
+ const response = await fetch(`http://localhost:${CONVEX_PORT}/health`);
145
+ if (!response.ok) return false;
146
+ } catch {
147
+ return false;
148
+ }
149
+
150
+ return true;
151
+ }
152
+
153
+ async function main(): Promise<void> {
154
+ console.log("[entrypoint] ========================================");
155
+ console.log("[entrypoint] Sandbox Agent Starting");
156
+ console.log("[entrypoint] ========================================");
157
+
158
+ try {
159
+ // Start services in order
160
+ await startConvexBackend();
161
+ await startLspServers();
162
+ await startFileWatcher();
163
+
164
+ // Verify health
165
+ const healthy = await healthCheck();
166
+ if (!healthy) {
167
+ console.warn("[entrypoint] Health check failed, but continuing...");
168
+ }
169
+
170
+ console.log("[entrypoint] ========================================");
171
+ console.log("[entrypoint] Sandbox Ready!");
172
+ console.log("[entrypoint] Convex: http://localhost:" + CONVEX_PORT);
173
+ console.log("[entrypoint] ========================================");
174
+
175
+ // Keep process running
176
+ await new Promise(() => {});
177
+ } catch (error: any) {
178
+ console.error("[entrypoint] Startup failed:", error.message);
179
+ process.exit(1);
180
+ }
181
+ }
182
+
183
+ // Handle shutdown
184
+ process.on("SIGTERM", () => {
185
+ console.log("[entrypoint] Received SIGTERM, shutting down...");
186
+ process.exit(0);
187
+ });
188
+
189
+ process.on("SIGINT", () => {
190
+ console.log("[entrypoint] Received SIGINT, shutting down...");
191
+ process.exit(0);
192
+ });
193
+
194
+ main();