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 +1 -1
- package/src/binary.ts +97 -32
- package/src/routes/static.ts +73 -21
- package/src/server.ts +8 -6
package/package.json
CHANGED
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
package/src/routes/static.ts
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
//
|
|
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
|
|
33
|
-
|
|
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
|
|
39
|
-
if (
|
|
40
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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);
|