apteva 0.1.5 → 0.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apteva",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
4
4
  "description": "Run AI agents locally. Multi-provider support for Claude, GPT, Gemini, Llama, and more.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/binary.ts CHANGED
@@ -3,6 +3,18 @@ import { existsSync, mkdirSync, chmodSync } from "fs";
3
3
 
4
4
  // Binary configuration
5
5
  const BINARY_BASE_URL = "https://github.com/apteva/agent/releases/latest/download";
6
+ const DOWNLOAD_TIMEOUT = 60000; // 60 seconds
7
+ const MAX_RETRIES = 3;
8
+ const RETRY_DELAY = 2000; // 2 seconds
9
+
10
+ // ANSI colors for console output
11
+ const c = {
12
+ reset: "\x1b[0m",
13
+ orange: "\x1b[38;5;208m",
14
+ gray: "\x1b[38;5;245m",
15
+ green: "\x1b[38;5;82m",
16
+ red: "\x1b[38;5;196m",
17
+ };
6
18
 
7
19
  // Determine platform and architecture
8
20
  function getPlatformInfo(): { platform: string; arch: string; ext: string } {
@@ -40,8 +52,26 @@ export function binaryExists(binDir: string): boolean {
40
52
  return existsSync(getBinaryPath(binDir));
41
53
  }
42
54
 
55
+ // Helper to delay
56
+ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
57
+
58
+ // Download with timeout
59
+ async function fetchWithTimeout(url: string, timeout: number): Promise<Response> {
60
+ const controller = new AbortController();
61
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
62
+
63
+ try {
64
+ const response = await fetch(url, { signal: controller.signal });
65
+ clearTimeout(timeoutId);
66
+ return response;
67
+ } catch (err) {
68
+ clearTimeout(timeoutId);
69
+ throw err;
70
+ }
71
+ }
72
+
43
73
  // Download binary if missing
44
- export async function ensureBinary(binDir: string): Promise<{ success: boolean; path: string; error?: string }> {
74
+ export async function ensureBinary(binDir: string, silent = false): Promise<{ success: boolean; path: string; error?: string; downloaded?: boolean }> {
45
75
  const binaryPath = getBinaryPath(binDir);
46
76
 
47
77
  // Ensure bin directory exists
@@ -51,46 +81,81 @@ export async function ensureBinary(binDir: string): Promise<{ success: boolean;
51
81
 
52
82
  // Check if already exists
53
83
  if (existsSync(binaryPath)) {
54
- return { success: true, path: binaryPath };
84
+ return { success: true, path: binaryPath, downloaded: false };
55
85
  }
56
86
 
57
87
  const url = getDownloadUrl();
88
+ const filename = getBinaryFilename();
58
89
 
59
- try {
60
- const response = await fetch(url);
61
-
62
- if (!response.ok) {
63
- // For now, since binaries don't exist yet, create a placeholder message
64
- if (response.status === 404) {
65
- return {
66
- success: false,
67
- path: binaryPath,
68
- error: `Agent binary not available yet. Binary URL: ${url}\n\nThe agent binary will be available in a future release. For now, you can:\n1. Build your own agent binary\n2. Set AGENT_BINARY_PATH environment variable to point to your binary`,
69
- };
70
- }
71
- return {
72
- success: false,
73
- path: binaryPath,
74
- error: `Failed to download binary: HTTP ${response.status}`,
75
- };
76
- }
90
+ // Show downloading message (server.ts already printed "Binary ")
91
+ if (!silent) {
92
+ process.stdout.write(`${c.orange}downloading...${c.reset}`);
93
+ }
94
+
95
+ let lastError: string = "";
77
96
 
78
- const arrayBuffer = await response.arrayBuffer();
79
- await Bun.write(binaryPath, arrayBuffer);
97
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
98
+ try {
99
+ if (!silent && attempt > 1) {
100
+ // Clear line and show retry status
101
+ process.stdout.write(`\r Agent ${c.orange}retry ${attempt}/${MAX_RETRIES}...${c.reset} `);
102
+ }
80
103
 
81
- // Make executable on Unix systems
82
- if (process.platform !== "win32") {
83
- chmodSync(binaryPath, 0o755);
104
+ const response = await fetchWithTimeout(url, DOWNLOAD_TIMEOUT);
105
+
106
+ if (!response.ok) {
107
+ if (response.status === 404) {
108
+ if (!silent) {
109
+ console.log(`\r${c.red}not found${c.reset} `);
110
+ }
111
+ return {
112
+ success: false,
113
+ path: binaryPath,
114
+ error: `Binary not found at ${url}`,
115
+ };
116
+ }
117
+ lastError = `HTTP ${response.status}`;
118
+ if (attempt < MAX_RETRIES) {
119
+ await sleep(RETRY_DELAY);
120
+ continue;
121
+ }
122
+ } else {
123
+ // Download the binary
124
+ const arrayBuffer = await response.arrayBuffer();
125
+ await Bun.write(binaryPath, arrayBuffer);
126
+
127
+ // Make executable on Unix systems
128
+ if (process.platform !== "win32") {
129
+ chmodSync(binaryPath, 0o755);
130
+ }
131
+
132
+ // Show success
133
+ if (!silent) {
134
+ const sizeMB = (arrayBuffer.byteLength / 1024 / 1024).toFixed(1);
135
+ console.log(`\r${c.green}binary ready${c.reset} ${c.gray}(${sizeMB}MB downloaded)${c.reset} `);
136
+ }
137
+
138
+ return { success: true, path: binaryPath, downloaded: true };
139
+ }
140
+ } catch (err: any) {
141
+ lastError = err.name === "AbortError" ? "timeout" : String(err.message || err);
142
+ if (attempt < MAX_RETRIES) {
143
+ await sleep(RETRY_DELAY);
144
+ continue;
145
+ }
84
146
  }
147
+ }
85
148
 
86
- return { success: true, path: binaryPath };
87
- } catch (err) {
88
- return {
89
- success: false,
90
- path: binaryPath,
91
- error: `Failed to download binary: ${err}`,
92
- };
149
+ // All retries failed
150
+ if (!silent) {
151
+ console.log(`\r${c.red}failed${c.reset} ${c.gray}(${lastError})${c.reset} `);
93
152
  }
153
+
154
+ return {
155
+ success: false,
156
+ path: binaryPath,
157
+ error: `Failed after ${MAX_RETRIES} attempts: ${lastError}`,
158
+ };
94
159
  }
95
160
 
96
161
  // Get binary status info
@@ -1,5 +1,5 @@
1
1
  import { join } from "path";
2
- import { existsSync } from "fs";
2
+ import { existsSync, statSync } from "fs";
3
3
 
4
4
  // Find dist directory - handle both development and npx contexts
5
5
  function findDistDir(): string {
@@ -10,8 +10,15 @@ function findDistDir(): string {
10
10
  ];
11
11
 
12
12
  for (const dir of candidates) {
13
- if (existsSync(join(dir, "index.html"))) {
14
- return dir;
13
+ try {
14
+ if (existsSync(dir) && statSync(dir).isDirectory()) {
15
+ const indexPath = join(dir, "index.html");
16
+ if (existsSync(indexPath)) {
17
+ return dir;
18
+ }
19
+ }
20
+ } catch {
21
+ continue;
15
22
  }
16
23
  }
17
24
 
@@ -20,33 +27,71 @@ function findDistDir(): string {
20
27
 
21
28
  const DIST_DIR = findDistDir();
22
29
 
23
- export async function serveStatic(req: Request, path: string): Promise<Response> {
24
- // Default to index.html for root and SPA routes
25
- let filePath = path === "/" ? "/index.html" : path;
30
+ // MIME types for common file extensions
31
+ const MIME_TYPES: Record<string, string> = {
32
+ ".html": "text/html; charset=utf-8",
33
+ ".js": "application/javascript; charset=utf-8",
34
+ ".css": "text/css; charset=utf-8",
35
+ ".json": "application/json; charset=utf-8",
36
+ ".png": "image/png",
37
+ ".jpg": "image/jpeg",
38
+ ".jpeg": "image/jpeg",
39
+ ".gif": "image/gif",
40
+ ".svg": "image/svg+xml",
41
+ ".ico": "image/x-icon",
42
+ ".woff": "font/woff",
43
+ ".woff2": "font/woff2",
44
+ ".ttf": "font/ttf",
45
+ ".eot": "application/vnd.ms-fontobject",
46
+ };
47
+
48
+ function getMimeType(path: string): string {
49
+ const ext = path.substring(path.lastIndexOf(".")).toLowerCase();
50
+ return MIME_TYPES[ext] || "application/octet-stream";
51
+ }
26
52
 
53
+ export async function serveStatic(req: Request, path: string): Promise<Response> {
27
54
  try {
28
- // Try to serve the file
55
+ // Default to index.html for root
56
+ let filePath = path === "/" ? "/index.html" : path;
57
+
58
+ // Prevent directory traversal attacks
59
+ if (filePath.includes("..")) {
60
+ return new Response("Forbidden", { status: 403 });
61
+ }
62
+
29
63
  const fullPath = join(DIST_DIR, filePath);
30
- const file = Bun.file(fullPath);
31
64
 
32
- if (await file.exists()) {
33
- return new Response(file);
65
+ // Check if file exists using sync API (more reliable)
66
+ if (existsSync(fullPath)) {
67
+ try {
68
+ const stat = statSync(fullPath);
69
+ if (stat.isFile()) {
70
+ const file = Bun.file(fullPath);
71
+ const mimeType = getMimeType(filePath);
72
+ return new Response(file, {
73
+ headers: { "Content-Type": mimeType },
74
+ });
75
+ }
76
+ } catch {
77
+ // Fall through to SPA handling
78
+ }
34
79
  }
35
80
 
36
81
  // For SPA: if file doesn't exist and it's not a static asset, serve index.html
37
82
  if (!path.includes(".")) {
38
- const indexFile = Bun.file(join(DIST_DIR, "index.html"));
39
- if (await indexFile.exists()) {
40
- return new Response(indexFile);
83
+ const indexPath = join(DIST_DIR, "index.html");
84
+ if (existsSync(indexPath)) {
85
+ const indexFile = Bun.file(indexPath);
86
+ return new Response(indexFile, {
87
+ headers: { "Content-Type": "text/html; charset=utf-8" },
88
+ });
41
89
  }
42
90
  }
43
- } catch (e) {
44
- // Fall through to fallback
45
- }
46
91
 
47
- // If dist doesn't exist, serve a development message
48
- return new Response(
49
- `<!DOCTYPE html>
92
+ // If dist doesn't exist, serve a development message
93
+ return new Response(
94
+ `<!DOCTYPE html>
50
95
  <html>
51
96
  <head>
52
97
  <title>Apteva</title>
@@ -64,6 +109,13 @@ export async function serveStatic(req: Request, path: string): Promise<Response>
64
109
  </div>
65
110
  </body>
66
111
  </html>`,
67
- { headers: { "Content-Type": "text/html" } }
68
- );
112
+ {
113
+ status: 200,
114
+ headers: { "Content-Type": "text/html; charset=utf-8" }
115
+ }
116
+ );
117
+ } catch (error) {
118
+ console.error("Static file error:", error);
119
+ return new Response("Internal Server Error", { status: 500 });
120
+ }
69
121
  }
package/src/server.ts CHANGED
@@ -73,13 +73,13 @@ console.log(`
73
73
  ${c.gray}Run AI agents locally${c.reset}
74
74
  `);
75
75
 
76
- // Check binary
77
- process.stdout.write(` ${c.darkGray}Binary${c.reset} `);
76
+ // Check binary - ensureBinary handles progress output when downloading
77
+ process.stdout.write(` ${c.darkGray}Agent${c.reset} `);
78
78
  const binaryResult = await ensureBinary(BIN_DIR);
79
- if (!binaryResult.success) {
80
- console.log(`${c.orange}not available${c.reset}`);
81
- } else {
82
- console.log(`${c.gray}ready${c.reset}`);
79
+ // ensureBinary prints its own status when downloading/failing
80
+ // We only need to print "ready" if binary already existed
81
+ if (binaryResult.success && !binaryResult.downloaded) {
82
+ console.log(`${c.gray}binary ready${c.reset}`);
83
83
  }
84
84
 
85
85
  // Check database
@@ -93,6 +93,8 @@ console.log(`${c.gray}${configuredProviders.length} configured${c.reset}`);
93
93
 
94
94
  const server = Bun.serve({
95
95
  port: PORT,
96
+ hostname: "0.0.0.0", // Listen on all interfaces
97
+ development: false, // Suppress "Started server" message
96
98
 
97
99
  async fetch(req: Request): Promise<Response> {
98
100
  const url = new URL(req.url);