@buda-ai/connector 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/README.md +87 -0
- package/dist/cli.js +532 -0
- package/package.json +38 -0
- package/src/bunny-daemon.ts +52 -0
- package/src/cli.ts +97 -0
- package/src/cloud-client.ts +134 -0
- package/src/config.ts +122 -0
- package/src/logger.ts +36 -0
- package/src/relay.ts +96 -0
- package/src/runtime.ts +59 -0
- package/src/types.ts +37 -0
- package/tsconfig.json +44 -0
- package/tsup.config.ts +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Buda Connector
|
|
2
|
+
|
|
3
|
+
Standalone connector daemon for Buda Agent Connector runtimes.
|
|
4
|
+
|
|
5
|
+
The cloud server defaults to `https://buda.im`. Pass `--server-url` or set
|
|
6
|
+
`BUDA_CONNECTOR_SERVER_URL` only for local development or private deployments.
|
|
7
|
+
|
|
8
|
+
## Role
|
|
9
|
+
|
|
10
|
+
- Registers the local or remote host with `apps/buda`.
|
|
11
|
+
- Starts an embedded `@bunny-agent/daemon` HTTP runtime in the same Node process.
|
|
12
|
+
- Sends heartbeats and local log batches to the cloud registry.
|
|
13
|
+
- Maintains a WebSocket relay so Buda Cloud can actively reach the connector.
|
|
14
|
+
- Stores device identity and local logs under `~/.buda`.
|
|
15
|
+
|
|
16
|
+
## Local Development
|
|
17
|
+
|
|
18
|
+
Start Buda first:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm --filter buda dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Create/register a connector identity:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
BUDA_CONNECTOR_SERVER_URL=http://localhost:3000 pnpm --filter @buda-ai/connector new
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Run the daemon loop:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
BUDA_CONNECTOR_SERVER_URL=http://localhost:3000 pnpm --filter @buda-ai/connector dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The connector starts Bunny Agent daemon on `127.0.0.1` with an ephemeral port
|
|
37
|
+
by default and reports the local daemon URL to Buda in connector metadata.
|
|
38
|
+
|
|
39
|
+
The localhost API path bypasses auth for development. Desktop sidecars should pass
|
|
40
|
+
an OAuth token with `--launch-config-stdin`.
|
|
41
|
+
|
|
42
|
+
## Launch Config
|
|
43
|
+
|
|
44
|
+
Desktop can start the sidecar with:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
printf '{"serverUrl":"https://buda.im","oauthToken":"..."}\n' \
|
|
48
|
+
| buda-connector daemon --launch-config-stdin
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Supported launch config fields:
|
|
52
|
+
|
|
53
|
+
- `serverUrl`
|
|
54
|
+
- `spaceId`
|
|
55
|
+
- `workdirRoot`
|
|
56
|
+
- `oauthToken`
|
|
57
|
+
- `bunnyDaemonHost`
|
|
58
|
+
- `bunnyDaemonPort`
|
|
59
|
+
|
|
60
|
+
## One-line install
|
|
61
|
+
|
|
62
|
+
Buda Space Settings can show a simple Space ID command for the current space.
|
|
63
|
+
This mode identifies the Space, not a specific user:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx -y @buda-ai/connector@latest daemon --space-id "org_..."
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Desktop and other authenticated clients should use OAuth token launch config
|
|
70
|
+
instead:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
printf '%s\n' '{"serverUrl":"https://buda.im","oauthToken":"..."}' \
|
|
74
|
+
| npx -y @buda-ai/connector@latest daemon --launch-config-stdin
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
You can also pass launch options directly:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
buda-connector daemon --space-id "org_..." --workdir "$HOME/.buda/agents"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Local Files
|
|
84
|
+
|
|
85
|
+
- `~/.buda/device.json`
|
|
86
|
+
- `~/.buda/connector.log`
|
|
87
|
+
- `~/.buda/agents/`
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
6
|
+
import { homedir, hostname, platform } from "os";
|
|
7
|
+
import path from "path";
|
|
8
|
+
var CONFIG_DIR = path.join(homedir(), ".buda");
|
|
9
|
+
var CONFIG_PATH = path.join(CONFIG_DIR, "device.json");
|
|
10
|
+
var LOG_PATH = path.join(CONFIG_DIR, "connector.log");
|
|
11
|
+
var DEFAULT_SERVER_URL = "https://buda.im";
|
|
12
|
+
var DEFAULT_WORKDIR_ROOT = path.join(CONFIG_DIR, "agents");
|
|
13
|
+
var DEFAULT_BUNNY_DAEMON_HOST = "127.0.0.1";
|
|
14
|
+
var HEARTBEAT_INTERVAL_MS = 5e3;
|
|
15
|
+
var RELAY_RECONNECT_INTERVAL_MS = 3e3;
|
|
16
|
+
var LAUNCH_CONFIG_STDIN_FLAG = "--launch-config-stdin";
|
|
17
|
+
function getServerUrl(launchConfig) {
|
|
18
|
+
return (launchConfig?.serverUrl || process.env.BUDA_CONNECTOR_SERVER_URL || process.env.BUDA_SERVER_URL || process.env.NEXT_PUBLIC_APP_URL || DEFAULT_SERVER_URL).replace(/\/$/, "");
|
|
19
|
+
}
|
|
20
|
+
function getConnectorRelayUrl(serverUrl, connectorId) {
|
|
21
|
+
const url = new URL("/api/connectors/ws", serverUrl);
|
|
22
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
23
|
+
url.searchParams.set("connectorId", connectorId);
|
|
24
|
+
return url.toString();
|
|
25
|
+
}
|
|
26
|
+
function getWorkdirRoot(launchConfig) {
|
|
27
|
+
return launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || DEFAULT_WORKDIR_ROOT;
|
|
28
|
+
}
|
|
29
|
+
function getBunnyDaemonHost(launchConfig) {
|
|
30
|
+
return launchConfig?.bunnyDaemonHost || process.env.BUDA_CONNECTOR_BUNNY_DAEMON_HOST || DEFAULT_BUNNY_DAEMON_HOST;
|
|
31
|
+
}
|
|
32
|
+
function getBunnyDaemonPort(launchConfig) {
|
|
33
|
+
if (launchConfig?.bunnyDaemonPort !== void 0) return launchConfig.bunnyDaemonPort;
|
|
34
|
+
const raw = process.env.BUDA_CONNECTOR_BUNNY_DAEMON_PORT;
|
|
35
|
+
if (!raw) return 0;
|
|
36
|
+
const parsed = Number(raw);
|
|
37
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
38
|
+
}
|
|
39
|
+
function getSpaceId(launchConfig) {
|
|
40
|
+
return launchConfig?.spaceId || process.env.BUDA_CONNECTOR_SPACE_ID;
|
|
41
|
+
}
|
|
42
|
+
function buildDeviceConfig(launchConfig) {
|
|
43
|
+
const host = hostname();
|
|
44
|
+
return {
|
|
45
|
+
connectorId: `cnr_${randomUUID()}`,
|
|
46
|
+
deviceId: `dev_${randomUUID()}`,
|
|
47
|
+
deviceName: host,
|
|
48
|
+
hostLabel: `${host} (${platform()})`,
|
|
49
|
+
serverUrl: getServerUrl(launchConfig),
|
|
50
|
+
workdirRoot: getWorkdirRoot(launchConfig),
|
|
51
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function readDeviceConfig() {
|
|
55
|
+
try {
|
|
56
|
+
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
57
|
+
return JSON.parse(raw);
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function writeDeviceConfig(config) {
|
|
63
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
64
|
+
await mkdir(config.workdirRoot, { recursive: true });
|
|
65
|
+
await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}
|
|
66
|
+
`, { mode: 384 });
|
|
67
|
+
}
|
|
68
|
+
async function ensureDeviceConfig(launchConfig) {
|
|
69
|
+
const existing = await readDeviceConfig();
|
|
70
|
+
if (existing) {
|
|
71
|
+
return {
|
|
72
|
+
...existing,
|
|
73
|
+
serverUrl: getServerUrl(launchConfig),
|
|
74
|
+
workdirRoot: launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || existing.workdirRoot || getWorkdirRoot(launchConfig)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const config = buildDeviceConfig(launchConfig);
|
|
78
|
+
await writeDeviceConfig(config);
|
|
79
|
+
return config;
|
|
80
|
+
}
|
|
81
|
+
function parseLaunchConfig(raw) {
|
|
82
|
+
if (!raw.trim()) return {};
|
|
83
|
+
const parsed = JSON.parse(raw);
|
|
84
|
+
const bunnyDaemonPort = typeof parsed.bunnyDaemonPort === "number" && Number.isInteger(parsed.bunnyDaemonPort) ? parsed.bunnyDaemonPort : void 0;
|
|
85
|
+
return {
|
|
86
|
+
serverUrl: typeof parsed.serverUrl === "string" ? parsed.serverUrl : void 0,
|
|
87
|
+
workdirRoot: typeof parsed.workdirRoot === "string" ? parsed.workdirRoot : void 0,
|
|
88
|
+
oauthToken: typeof parsed.oauthToken === "string" ? parsed.oauthToken : void 0,
|
|
89
|
+
spaceId: typeof parsed.spaceId === "string" ? parsed.spaceId : void 0,
|
|
90
|
+
bunnyDaemonHost: typeof parsed.bunnyDaemonHost === "string" ? parsed.bunnyDaemonHost : void 0,
|
|
91
|
+
bunnyDaemonPort
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/bunny-daemon.ts
|
|
96
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
97
|
+
import { createDaemon } from "@bunny-agent/daemon";
|
|
98
|
+
|
|
99
|
+
// src/logger.ts
|
|
100
|
+
import { appendFile, mkdir as mkdir2 } from "fs/promises";
|
|
101
|
+
var pendingLogs = [];
|
|
102
|
+
async function writeLog(level, message, metadata) {
|
|
103
|
+
const entry = {
|
|
104
|
+
level,
|
|
105
|
+
message,
|
|
106
|
+
metadata,
|
|
107
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
108
|
+
};
|
|
109
|
+
pendingLogs.push(entry);
|
|
110
|
+
if (pendingLogs.length > 100) pendingLogs.splice(0, pendingLogs.length - 100);
|
|
111
|
+
await mkdir2(CONFIG_DIR, { recursive: true }).catch(() => void 0);
|
|
112
|
+
await appendFile(LOG_PATH, `${JSON.stringify(entry)}
|
|
113
|
+
`).catch(() => void 0);
|
|
114
|
+
const line = `[buda-connector] ${message}`;
|
|
115
|
+
if (level === "error") console.error(line);
|
|
116
|
+
else if (level === "warn") console.warn(line);
|
|
117
|
+
else console.log(line);
|
|
118
|
+
}
|
|
119
|
+
function takePendingLogs() {
|
|
120
|
+
return pendingLogs.splice(0, pendingLogs.length);
|
|
121
|
+
}
|
|
122
|
+
function restorePendingLogs(logs) {
|
|
123
|
+
pendingLogs.unshift(...logs);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/bunny-daemon.ts
|
|
127
|
+
async function startEmbeddedBunnyDaemon(config, launchConfig) {
|
|
128
|
+
const host = getBunnyDaemonHost(launchConfig);
|
|
129
|
+
const port = getBunnyDaemonPort(launchConfig);
|
|
130
|
+
const root = config.workdirRoot;
|
|
131
|
+
await mkdir3(root, { recursive: true });
|
|
132
|
+
const server = createDaemon({ host, port, root });
|
|
133
|
+
await new Promise((resolve, reject) => {
|
|
134
|
+
const onError = (error) => {
|
|
135
|
+
server.off("listening", onListening);
|
|
136
|
+
reject(error);
|
|
137
|
+
};
|
|
138
|
+
const onListening = () => {
|
|
139
|
+
server.off("error", onError);
|
|
140
|
+
resolve();
|
|
141
|
+
};
|
|
142
|
+
server.once("error", onError);
|
|
143
|
+
server.once("listening", onListening);
|
|
144
|
+
server.listen(port, host);
|
|
145
|
+
});
|
|
146
|
+
const address = server.address();
|
|
147
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
148
|
+
const baseUrl = `http://${host}:${actualPort}`;
|
|
149
|
+
await writeLog("info", `bunny daemon listening ${baseUrl}`, {
|
|
150
|
+
root
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
server,
|
|
154
|
+
host,
|
|
155
|
+
port: actualPort,
|
|
156
|
+
baseUrl,
|
|
157
|
+
root
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function stopEmbeddedBunnyDaemon(daemon) {
|
|
161
|
+
await new Promise((resolve) => {
|
|
162
|
+
daemon.server.close(() => resolve());
|
|
163
|
+
});
|
|
164
|
+
await writeLog("info", `bunny daemon stopped ${daemon.baseUrl}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/cloud-client.ts
|
|
168
|
+
import { platform as platform2, release } from "os";
|
|
169
|
+
function buildPayload(config, extra) {
|
|
170
|
+
return {
|
|
171
|
+
connectorId: config.connectorId,
|
|
172
|
+
deviceId: config.deviceId,
|
|
173
|
+
deviceName: config.deviceName,
|
|
174
|
+
hostLabel: config.hostLabel,
|
|
175
|
+
platform: `${platform2()} ${release()}`,
|
|
176
|
+
version: "dev",
|
|
177
|
+
pid: process.pid,
|
|
178
|
+
workdirRoot: config.workdirRoot,
|
|
179
|
+
baseUrl: extra?.bunnyDaemon?.baseUrl,
|
|
180
|
+
bunnyDaemon: extra?.bunnyDaemon ? {
|
|
181
|
+
baseUrl: extra.bunnyDaemon.baseUrl,
|
|
182
|
+
host: extra.bunnyDaemon.host,
|
|
183
|
+
port: extra.bunnyDaemon.port,
|
|
184
|
+
root: extra.bunnyDaemon.root,
|
|
185
|
+
status: "online"
|
|
186
|
+
} : void 0,
|
|
187
|
+
capabilities: ["connector", "bunny-daemon", "coding", "filesystem", "shell", "git"],
|
|
188
|
+
...extra?.logs?.length ? { logs: extra.logs } : {}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function postJson(serverUrl, pathname, body, launchConfig) {
|
|
192
|
+
const token = launchConfig?.oauthToken?.trim();
|
|
193
|
+
const spaceId = getSpaceId(launchConfig)?.trim();
|
|
194
|
+
const response = await fetch(`${serverUrl}${pathname}`, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
...token ? { Authorization: `Bearer ${token}` } : {},
|
|
199
|
+
...!token && spaceId ? { "X-Buda-Space-Id": spaceId } : {}
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify(body)
|
|
202
|
+
});
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
const text = await response.text().catch(() => "");
|
|
205
|
+
throw new Error(`${pathname} failed: ${response.status} ${text}`);
|
|
206
|
+
}
|
|
207
|
+
return await response.json();
|
|
208
|
+
}
|
|
209
|
+
function formatConnectorOwner(connector) {
|
|
210
|
+
return connector.userEmail ?? connector.userId ?? "anonymous";
|
|
211
|
+
}
|
|
212
|
+
async function registerConnector(config, launchConfig, bunnyDaemon) {
|
|
213
|
+
const result = await postJson(
|
|
214
|
+
config.serverUrl,
|
|
215
|
+
"/api/connectors/register",
|
|
216
|
+
buildPayload(config, { bunnyDaemon }),
|
|
217
|
+
launchConfig
|
|
218
|
+
);
|
|
219
|
+
await writeLog(
|
|
220
|
+
"info",
|
|
221
|
+
`registered connector=${result.connector.connectorId} status=${result.connector.status} owner=${formatConnectorOwner(result.connector)}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
async function heartbeatConnector(config, launchConfig, bunnyDaemon) {
|
|
225
|
+
const logs = takePendingLogs();
|
|
226
|
+
const result = await postJson(
|
|
227
|
+
config.serverUrl,
|
|
228
|
+
"/api/connectors/heartbeat",
|
|
229
|
+
buildPayload(config, {
|
|
230
|
+
bunnyDaemon,
|
|
231
|
+
logs
|
|
232
|
+
}),
|
|
233
|
+
launchConfig
|
|
234
|
+
).catch((error) => {
|
|
235
|
+
restorePendingLogs(logs);
|
|
236
|
+
throw error;
|
|
237
|
+
});
|
|
238
|
+
await writeLog(
|
|
239
|
+
"info",
|
|
240
|
+
`heartbeat connector=${result.connector.connectorId} status=${result.connector.status} owner=${formatConnectorOwner(result.connector)}`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
async function unregisterConnector(config, launchConfig) {
|
|
244
|
+
await postJson(
|
|
245
|
+
config.serverUrl,
|
|
246
|
+
"/api/connectors/unregister",
|
|
247
|
+
{
|
|
248
|
+
connectorId: config.connectorId
|
|
249
|
+
},
|
|
250
|
+
launchConfig
|
|
251
|
+
).catch((error) => {
|
|
252
|
+
void writeLog("warn", `unregister failed: ${error instanceof Error ? error.message : error}`);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ../../packages/relaylib/src/protocol.ts
|
|
257
|
+
var utf8Encoder = new TextEncoder();
|
|
258
|
+
function bytesToBase64(bytes) {
|
|
259
|
+
if (typeof Buffer !== "undefined") {
|
|
260
|
+
return Buffer.from(bytes).toString("base64");
|
|
261
|
+
}
|
|
262
|
+
let binary = "";
|
|
263
|
+
for (const byte of bytes) {
|
|
264
|
+
binary += String.fromCharCode(byte);
|
|
265
|
+
}
|
|
266
|
+
return btoa(binary);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ../../packages/relaylib/src/relay-client.ts
|
|
270
|
+
function attachRelayClient(socket, handler, options = {}) {
|
|
271
|
+
const chunkEncoding = options.chunkEncoding ?? "base64";
|
|
272
|
+
socket.on("message", (raw) => {
|
|
273
|
+
let message;
|
|
274
|
+
try {
|
|
275
|
+
message = JSON.parse(raw.toString());
|
|
276
|
+
} catch {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (message.type !== "request") return;
|
|
280
|
+
void handleRequest(socket, message.requestId, message.request, handler, chunkEncoding);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async function handleRequest(socket, requestId, request, handler, chunkEncoding) {
|
|
284
|
+
const send = (message) => {
|
|
285
|
+
if (socket.readyState === 1) socket.send(JSON.stringify(message));
|
|
286
|
+
};
|
|
287
|
+
let response;
|
|
288
|
+
try {
|
|
289
|
+
response = await handler(request);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
send({ type: "error", requestId, error: errorMessage(error) });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
send({
|
|
295
|
+
type: "start",
|
|
296
|
+
requestId,
|
|
297
|
+
status: response.status,
|
|
298
|
+
headers: headersToRecord(response.headers)
|
|
299
|
+
});
|
|
300
|
+
try {
|
|
301
|
+
const body = response.body;
|
|
302
|
+
if (body) {
|
|
303
|
+
const reader = body.getReader();
|
|
304
|
+
if (chunkEncoding === "utf8") {
|
|
305
|
+
const decoder = new TextDecoder();
|
|
306
|
+
while (true) {
|
|
307
|
+
const { done, value } = await reader.read();
|
|
308
|
+
if (done) break;
|
|
309
|
+
const text = decoder.decode(value, { stream: true });
|
|
310
|
+
if (text) send({ type: "chunk", requestId, data: text });
|
|
311
|
+
}
|
|
312
|
+
const tail = decoder.decode();
|
|
313
|
+
if (tail) send({ type: "chunk", requestId, data: tail });
|
|
314
|
+
} else {
|
|
315
|
+
while (true) {
|
|
316
|
+
const { done, value } = await reader.read();
|
|
317
|
+
if (done) break;
|
|
318
|
+
if (value && value.length > 0) {
|
|
319
|
+
send({
|
|
320
|
+
type: "chunk",
|
|
321
|
+
requestId,
|
|
322
|
+
encoding: "base64",
|
|
323
|
+
data: bytesToBase64(value)
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
send({ type: "end", requestId });
|
|
330
|
+
} catch (error) {
|
|
331
|
+
send({ type: "error", requestId, error: errorMessage(error) });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function headersToRecord(headers) {
|
|
335
|
+
const record = {};
|
|
336
|
+
headers.forEach((value, key) => {
|
|
337
|
+
record[key] = value;
|
|
338
|
+
});
|
|
339
|
+
return record;
|
|
340
|
+
}
|
|
341
|
+
function errorMessage(error) {
|
|
342
|
+
return error instanceof Error ? error.message : String(error);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/relay.ts
|
|
346
|
+
import WebSocket from "ws";
|
|
347
|
+
function startConnectorRelay(config, bunnyDaemon, launchConfig) {
|
|
348
|
+
let socket = null;
|
|
349
|
+
let reconnectTimer = null;
|
|
350
|
+
let stopped = false;
|
|
351
|
+
const handler = createRelayHandler(bunnyDaemon);
|
|
352
|
+
const connect = () => {
|
|
353
|
+
if (stopped) return;
|
|
354
|
+
const relayUrl = getConnectorRelayUrl(config.serverUrl, config.connectorId);
|
|
355
|
+
const token = launchConfig?.oauthToken?.trim();
|
|
356
|
+
socket = new WebSocket(relayUrl, {
|
|
357
|
+
headers: token ? { Authorization: `Bearer ${token}` } : void 0
|
|
358
|
+
});
|
|
359
|
+
socket.on("open", () => {
|
|
360
|
+
void writeLog("info", `relay connected ${relayUrl}`);
|
|
361
|
+
});
|
|
362
|
+
attachRelayClient(socket, handler, { chunkEncoding: "utf8" });
|
|
363
|
+
socket.on("close", () => {
|
|
364
|
+
void writeLog("warn", "relay disconnected");
|
|
365
|
+
if (stopped) return;
|
|
366
|
+
reconnectTimer = setTimeout(connect, RELAY_RECONNECT_INTERVAL_MS);
|
|
367
|
+
});
|
|
368
|
+
socket.on("error", (error) => {
|
|
369
|
+
void writeLog("warn", `relay error: ${error instanceof Error ? error.message : error}`);
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
connect();
|
|
373
|
+
return {
|
|
374
|
+
stop() {
|
|
375
|
+
stopped = true;
|
|
376
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
377
|
+
socket?.close();
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function createRelayHandler(bunnyDaemon) {
|
|
382
|
+
const allowedOrigin = new URL(bunnyDaemon.baseUrl).origin;
|
|
383
|
+
return async (request) => {
|
|
384
|
+
const requestUrl = new URL(request.path, bunnyDaemon.baseUrl);
|
|
385
|
+
if (requestUrl.origin !== allowedOrigin) {
|
|
386
|
+
throw new Error("Relay request must target the embedded bunny daemon.");
|
|
387
|
+
}
|
|
388
|
+
await writeLog("info", `relay request ${request.method} ${requestUrl.pathname}`, {
|
|
389
|
+
actorEmail: getHeaderValue(request.headers, "x-buda-relay-actor-email") || null,
|
|
390
|
+
actorId: getHeaderValue(request.headers, "x-buda-relay-actor-id") || null,
|
|
391
|
+
source: getHeaderValue(request.headers, "x-buda-relay-source") || "unknown",
|
|
392
|
+
action: getHeaderValue(request.headers, "x-buda-relay-action") || "unknown",
|
|
393
|
+
target: requestUrl.toString()
|
|
394
|
+
});
|
|
395
|
+
return fetch(requestUrl, {
|
|
396
|
+
method: request.method,
|
|
397
|
+
headers: {
|
|
398
|
+
"Content-Type": "application/json",
|
|
399
|
+
...request.headers ?? {}
|
|
400
|
+
},
|
|
401
|
+
body: request.body === void 0 ? void 0 : JSON.stringify(request.body)
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function getHeaderValue(headers, name) {
|
|
406
|
+
if (!headers) return void 0;
|
|
407
|
+
const normalized = name.toLowerCase();
|
|
408
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
409
|
+
if (key.toLowerCase() === normalized) return value;
|
|
410
|
+
}
|
|
411
|
+
return void 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/runtime.ts
|
|
415
|
+
async function runNewConnector(launchConfig) {
|
|
416
|
+
const config = buildDeviceConfig(launchConfig);
|
|
417
|
+
await writeDeviceConfig(config);
|
|
418
|
+
await registerConnector(config, launchConfig);
|
|
419
|
+
await writeLog("info", `wrote ${CONFIG_PATH}`);
|
|
420
|
+
await writeLog("info", `workdir root ${config.workdirRoot}`);
|
|
421
|
+
await writeLog("info", `log file ${LOG_PATH}`);
|
|
422
|
+
}
|
|
423
|
+
async function runConnectorDaemon(launchConfig) {
|
|
424
|
+
const config = await ensureDeviceConfig(launchConfig);
|
|
425
|
+
const bunnyDaemon = await startEmbeddedBunnyDaemon(config, launchConfig);
|
|
426
|
+
try {
|
|
427
|
+
await registerConnector(config, launchConfig, bunnyDaemon);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
await stopEmbeddedBunnyDaemon(bunnyDaemon);
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
const relay = startConnectorRelay(config, bunnyDaemon, launchConfig);
|
|
433
|
+
await writeLog("info", `server ${config.serverUrl}`);
|
|
434
|
+
await writeLog("info", `config ${CONFIG_PATH}`);
|
|
435
|
+
await writeLog("info", `workdir root ${config.workdirRoot}`);
|
|
436
|
+
await writeLog("info", `log file ${LOG_PATH}`);
|
|
437
|
+
await writeLog("info", `bunny daemon ${bunnyDaemon.baseUrl}`);
|
|
438
|
+
const interval = setInterval(() => {
|
|
439
|
+
heartbeatConnector(config, launchConfig, bunnyDaemon).catch((error) => {
|
|
440
|
+
void writeLog(
|
|
441
|
+
"warn",
|
|
442
|
+
`heartbeat failed: ${error instanceof Error ? error.message : String(error)}`
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
446
|
+
const shutdown = async () => {
|
|
447
|
+
clearInterval(interval);
|
|
448
|
+
relay.stop();
|
|
449
|
+
await unregisterConnector(config, launchConfig);
|
|
450
|
+
await stopEmbeddedBunnyDaemon(bunnyDaemon);
|
|
451
|
+
process.exit(0);
|
|
452
|
+
};
|
|
453
|
+
process.on("SIGINT", shutdown);
|
|
454
|
+
process.on("SIGTERM", shutdown);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/cli.ts
|
|
458
|
+
function printUsage() {
|
|
459
|
+
console.log(
|
|
460
|
+
"Usage: buda-connector new | dev | daemon [--space-id <space>] [--server-url <url>] [--token <token>] [--workdir <path>] [--launch-config-stdin]\n pnpm --filter @buda-ai/connector dev\n\nCommands:\n new Create/register a new connector identity\n dev Run the connector daemon in development\n daemon Run the connector daemon for sidecar/service usage"
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
function readFlagValue(name) {
|
|
464
|
+
const index = process.argv.indexOf(name);
|
|
465
|
+
if (index < 0) return void 0;
|
|
466
|
+
const value = process.argv[index + 1];
|
|
467
|
+
return value && !value.startsWith("--") ? value : void 0;
|
|
468
|
+
}
|
|
469
|
+
function readCliLaunchConfig() {
|
|
470
|
+
const bunnyDaemonPort = readFlagValue("--bunny-daemon-port");
|
|
471
|
+
return {
|
|
472
|
+
serverUrl: readFlagValue("--server-url"),
|
|
473
|
+
workdirRoot: readFlagValue("--workdir"),
|
|
474
|
+
oauthToken: readFlagValue("--token"),
|
|
475
|
+
spaceId: readFlagValue("--space-id"),
|
|
476
|
+
bunnyDaemonHost: readFlagValue("--bunny-daemon-host"),
|
|
477
|
+
bunnyDaemonPort: bunnyDaemonPort && Number.isInteger(Number(bunnyDaemonPort)) ? Number(bunnyDaemonPort) : void 0
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async function readLaunchConfig() {
|
|
481
|
+
const cliConfig = readCliLaunchConfig();
|
|
482
|
+
if (!process.argv.includes(LAUNCH_CONFIG_STDIN_FLAG)) return cliConfig;
|
|
483
|
+
process.stdin.setEncoding("utf8");
|
|
484
|
+
return new Promise((resolve, reject) => {
|
|
485
|
+
let raw = "";
|
|
486
|
+
const cleanup = () => {
|
|
487
|
+
process.stdin.off("data", onData);
|
|
488
|
+
process.stdin.off("error", onError);
|
|
489
|
+
process.stdin.off("end", onEnd);
|
|
490
|
+
};
|
|
491
|
+
const onError = (error) => {
|
|
492
|
+
cleanup();
|
|
493
|
+
reject(error);
|
|
494
|
+
};
|
|
495
|
+
const onData = (chunk) => {
|
|
496
|
+
raw += chunk;
|
|
497
|
+
if (!raw.includes("\n")) return;
|
|
498
|
+
cleanup();
|
|
499
|
+
resolve({ ...cliConfig, ...parseLaunchConfig(raw.split("\n")[0] ?? "") });
|
|
500
|
+
};
|
|
501
|
+
const onEnd = () => {
|
|
502
|
+
cleanup();
|
|
503
|
+
resolve({ ...cliConfig, ...parseLaunchConfig(raw) });
|
|
504
|
+
};
|
|
505
|
+
process.stdin.on("data", onData);
|
|
506
|
+
process.stdin.on("error", onError);
|
|
507
|
+
process.stdin.on("end", onEnd);
|
|
508
|
+
process.stdin.resume();
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
async function main() {
|
|
512
|
+
const command = process.argv[2];
|
|
513
|
+
if (!command || process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
514
|
+
printUsage();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const launchConfig = await readLaunchConfig();
|
|
518
|
+
if (command === "new") {
|
|
519
|
+
await runNewConnector(launchConfig);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (command === "dev" || command === "daemon") {
|
|
523
|
+
await runConnectorDaemon(launchConfig);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
printUsage();
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
main().catch((error) => {
|
|
530
|
+
console.error("[buda-connector] failed:", error);
|
|
531
|
+
process.exit(1);
|
|
532
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@buda-ai/connector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"buda-connector": "./dist/cli.js",
|
|
11
|
+
"connector": "./dist/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "tsx src/cli.ts dev",
|
|
15
|
+
"daemon": "tsx src/cli.ts daemon",
|
|
16
|
+
"new": "tsx src/cli.ts new",
|
|
17
|
+
"start": "node dist/cli.js daemon",
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"lint": "biome format . --write && biome check .",
|
|
21
|
+
"lint:err": "biome format . --write && biome check . --write --diagnostic-level=error"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@bunny-agent/daemon": "0.9.43",
|
|
25
|
+
"relaylib": "workspace:*",
|
|
26
|
+
"ws": "^8.21.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^24.10.0",
|
|
30
|
+
"@types/ws": "^8.18.1",
|
|
31
|
+
"tsup": "^8.5.0",
|
|
32
|
+
"tsx": "^4.20.5",
|
|
33
|
+
"typescript": "^5.9.3"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { createDaemon } from "@bunny-agent/daemon";
|
|
3
|
+
import { getBunnyDaemonHost, getBunnyDaemonPort } from "./config.js";
|
|
4
|
+
import { writeLog } from "./logger.js";
|
|
5
|
+
import type { ConnectorLaunchConfig, DeviceConfig, EmbeddedBunnyDaemon } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export async function startEmbeddedBunnyDaemon(
|
|
8
|
+
config: DeviceConfig,
|
|
9
|
+
launchConfig?: ConnectorLaunchConfig,
|
|
10
|
+
): Promise<EmbeddedBunnyDaemon> {
|
|
11
|
+
const host = getBunnyDaemonHost(launchConfig);
|
|
12
|
+
const port = getBunnyDaemonPort(launchConfig);
|
|
13
|
+
const root = config.workdirRoot;
|
|
14
|
+
await mkdir(root, { recursive: true });
|
|
15
|
+
|
|
16
|
+
const server = createDaemon({ host, port, root });
|
|
17
|
+
await new Promise<void>((resolve, reject) => {
|
|
18
|
+
const onError = (error: Error) => {
|
|
19
|
+
server.off("listening", onListening);
|
|
20
|
+
reject(error);
|
|
21
|
+
};
|
|
22
|
+
const onListening = () => {
|
|
23
|
+
server.off("error", onError);
|
|
24
|
+
resolve();
|
|
25
|
+
};
|
|
26
|
+
server.once("error", onError);
|
|
27
|
+
server.once("listening", onListening);
|
|
28
|
+
server.listen(port, host);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const address = server.address();
|
|
32
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
33
|
+
const baseUrl = `http://${host}:${actualPort}`;
|
|
34
|
+
await writeLog("info", `bunny daemon listening ${baseUrl}`, {
|
|
35
|
+
root,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
server,
|
|
40
|
+
host,
|
|
41
|
+
port: actualPort,
|
|
42
|
+
baseUrl,
|
|
43
|
+
root,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function stopEmbeddedBunnyDaemon(daemon: EmbeddedBunnyDaemon) {
|
|
48
|
+
await new Promise<void>((resolve) => {
|
|
49
|
+
daemon.server.close(() => resolve());
|
|
50
|
+
});
|
|
51
|
+
await writeLog("info", `bunny daemon stopped ${daemon.baseUrl}`);
|
|
52
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { LAUNCH_CONFIG_STDIN_FLAG, parseLaunchConfig } from "./config.js";
|
|
4
|
+
import { runConnectorDaemon, runNewConnector } from "./runtime.js";
|
|
5
|
+
import type { ConnectorCliCommand, ConnectorLaunchConfig } from "./types.js";
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
console.log(
|
|
9
|
+
"Usage: buda-connector new | dev | daemon [--space-id <space>] [--server-url <url>] [--token <token>] [--workdir <path>] [--launch-config-stdin]\n" +
|
|
10
|
+
" pnpm --filter @buda-ai/connector dev\n\n" +
|
|
11
|
+
"Commands:\n" +
|
|
12
|
+
" new Create/register a new connector identity\n" +
|
|
13
|
+
" dev Run the connector daemon in development\n" +
|
|
14
|
+
" daemon Run the connector daemon for sidecar/service usage",
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readFlagValue(name: string): string | undefined {
|
|
19
|
+
const index = process.argv.indexOf(name);
|
|
20
|
+
if (index < 0) return undefined;
|
|
21
|
+
const value = process.argv[index + 1];
|
|
22
|
+
return value && !value.startsWith("--") ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readCliLaunchConfig(): ConnectorLaunchConfig {
|
|
26
|
+
const bunnyDaemonPort = readFlagValue("--bunny-daemon-port");
|
|
27
|
+
return {
|
|
28
|
+
serverUrl: readFlagValue("--server-url"),
|
|
29
|
+
workdirRoot: readFlagValue("--workdir"),
|
|
30
|
+
oauthToken: readFlagValue("--token"),
|
|
31
|
+
spaceId: readFlagValue("--space-id"),
|
|
32
|
+
bunnyDaemonHost: readFlagValue("--bunny-daemon-host"),
|
|
33
|
+
bunnyDaemonPort:
|
|
34
|
+
bunnyDaemonPort && Number.isInteger(Number(bunnyDaemonPort))
|
|
35
|
+
? Number(bunnyDaemonPort)
|
|
36
|
+
: undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function readLaunchConfig(): Promise<ConnectorLaunchConfig> {
|
|
41
|
+
const cliConfig = readCliLaunchConfig();
|
|
42
|
+
if (!process.argv.includes(LAUNCH_CONFIG_STDIN_FLAG)) return cliConfig;
|
|
43
|
+
|
|
44
|
+
process.stdin.setEncoding("utf8");
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
let raw = "";
|
|
47
|
+
const cleanup = () => {
|
|
48
|
+
process.stdin.off("data", onData);
|
|
49
|
+
process.stdin.off("error", onError);
|
|
50
|
+
process.stdin.off("end", onEnd);
|
|
51
|
+
};
|
|
52
|
+
const onError = (error: Error) => {
|
|
53
|
+
cleanup();
|
|
54
|
+
reject(error);
|
|
55
|
+
};
|
|
56
|
+
const onData = (chunk: string) => {
|
|
57
|
+
raw += chunk;
|
|
58
|
+
if (!raw.includes("\n")) return;
|
|
59
|
+
cleanup();
|
|
60
|
+
resolve({ ...cliConfig, ...parseLaunchConfig(raw.split("\n")[0] ?? "") });
|
|
61
|
+
};
|
|
62
|
+
const onEnd = () => {
|
|
63
|
+
cleanup();
|
|
64
|
+
resolve({ ...cliConfig, ...parseLaunchConfig(raw) });
|
|
65
|
+
};
|
|
66
|
+
process.stdin.on("data", onData);
|
|
67
|
+
process.stdin.on("error", onError);
|
|
68
|
+
process.stdin.on("end", onEnd);
|
|
69
|
+
process.stdin.resume();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function main() {
|
|
74
|
+
const command = process.argv[2] as ConnectorCliCommand | undefined;
|
|
75
|
+
if (!command || process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
76
|
+
printUsage();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const launchConfig = await readLaunchConfig();
|
|
81
|
+
if (command === "new") {
|
|
82
|
+
await runNewConnector(launchConfig);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (command === "dev" || command === "daemon") {
|
|
86
|
+
await runConnectorDaemon(launchConfig);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
printUsage();
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main().catch((error) => {
|
|
95
|
+
console.error("[buda-connector] failed:", error);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { platform, release } from "node:os";
|
|
2
|
+
import { getSpaceId } from "./config.js";
|
|
3
|
+
import { restorePendingLogs, takePendingLogs, writeLog } from "./logger.js";
|
|
4
|
+
import type {
|
|
5
|
+
ConnectorLaunchConfig,
|
|
6
|
+
ConnectorLogEntry,
|
|
7
|
+
DeviceConfig,
|
|
8
|
+
EmbeddedBunnyDaemon,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
function buildPayload(
|
|
12
|
+
config: DeviceConfig,
|
|
13
|
+
extra?: {
|
|
14
|
+
bunnyDaemon?: EmbeddedBunnyDaemon;
|
|
15
|
+
logs?: ConnectorLogEntry[];
|
|
16
|
+
},
|
|
17
|
+
) {
|
|
18
|
+
return {
|
|
19
|
+
connectorId: config.connectorId,
|
|
20
|
+
deviceId: config.deviceId,
|
|
21
|
+
deviceName: config.deviceName,
|
|
22
|
+
hostLabel: config.hostLabel,
|
|
23
|
+
platform: `${platform()} ${release()}`,
|
|
24
|
+
version: "dev",
|
|
25
|
+
pid: process.pid,
|
|
26
|
+
workdirRoot: config.workdirRoot,
|
|
27
|
+
baseUrl: extra?.bunnyDaemon?.baseUrl,
|
|
28
|
+
bunnyDaemon: extra?.bunnyDaemon
|
|
29
|
+
? {
|
|
30
|
+
baseUrl: extra.bunnyDaemon.baseUrl,
|
|
31
|
+
host: extra.bunnyDaemon.host,
|
|
32
|
+
port: extra.bunnyDaemon.port,
|
|
33
|
+
root: extra.bunnyDaemon.root,
|
|
34
|
+
status: "online",
|
|
35
|
+
}
|
|
36
|
+
: undefined,
|
|
37
|
+
capabilities: ["connector", "bunny-daemon", "coding", "filesystem", "shell", "git"],
|
|
38
|
+
...(extra?.logs?.length ? { logs: extra.logs } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function postJson<T>(
|
|
43
|
+
serverUrl: string,
|
|
44
|
+
pathname: string,
|
|
45
|
+
body: unknown,
|
|
46
|
+
launchConfig?: ConnectorLaunchConfig,
|
|
47
|
+
): Promise<T> {
|
|
48
|
+
const token = launchConfig?.oauthToken?.trim();
|
|
49
|
+
const spaceId = getSpaceId(launchConfig)?.trim();
|
|
50
|
+
const response = await fetch(`${serverUrl}${pathname}`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
55
|
+
...(!token && spaceId ? { "X-Buda-Space-Id": spaceId } : {}),
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify(body),
|
|
58
|
+
});
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const text = await response.text().catch(() => "");
|
|
61
|
+
throw new Error(`${pathname} failed: ${response.status} ${text}`);
|
|
62
|
+
}
|
|
63
|
+
return (await response.json()) as T;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface RegisteredConnectorResponse {
|
|
67
|
+
connector: {
|
|
68
|
+
connectorId: string;
|
|
69
|
+
status: string;
|
|
70
|
+
userId?: string | null;
|
|
71
|
+
userEmail?: string | null;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatConnectorOwner(connector: RegisteredConnectorResponse["connector"]): string {
|
|
76
|
+
return connector.userEmail ?? connector.userId ?? "anonymous";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function registerConnector(
|
|
80
|
+
config: DeviceConfig,
|
|
81
|
+
launchConfig?: ConnectorLaunchConfig,
|
|
82
|
+
bunnyDaemon?: EmbeddedBunnyDaemon,
|
|
83
|
+
) {
|
|
84
|
+
const result = await postJson<RegisteredConnectorResponse>(
|
|
85
|
+
config.serverUrl,
|
|
86
|
+
"/api/connectors/register",
|
|
87
|
+
buildPayload(config, { bunnyDaemon }),
|
|
88
|
+
launchConfig,
|
|
89
|
+
);
|
|
90
|
+
await writeLog(
|
|
91
|
+
"info",
|
|
92
|
+
`registered connector=${result.connector.connectorId} status=${result.connector.status} owner=${formatConnectorOwner(result.connector)}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function heartbeatConnector(
|
|
97
|
+
config: DeviceConfig,
|
|
98
|
+
launchConfig?: ConnectorLaunchConfig,
|
|
99
|
+
bunnyDaemon?: EmbeddedBunnyDaemon,
|
|
100
|
+
) {
|
|
101
|
+
const logs = takePendingLogs();
|
|
102
|
+
const result = await postJson<RegisteredConnectorResponse>(
|
|
103
|
+
config.serverUrl,
|
|
104
|
+
"/api/connectors/heartbeat",
|
|
105
|
+
buildPayload(config, {
|
|
106
|
+
bunnyDaemon,
|
|
107
|
+
logs,
|
|
108
|
+
}),
|
|
109
|
+
launchConfig,
|
|
110
|
+
).catch((error) => {
|
|
111
|
+
restorePendingLogs(logs);
|
|
112
|
+
throw error;
|
|
113
|
+
});
|
|
114
|
+
await writeLog(
|
|
115
|
+
"info",
|
|
116
|
+
`heartbeat connector=${result.connector.connectorId} status=${result.connector.status} owner=${formatConnectorOwner(result.connector)}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function unregisterConnector(
|
|
121
|
+
config: DeviceConfig,
|
|
122
|
+
launchConfig?: ConnectorLaunchConfig,
|
|
123
|
+
) {
|
|
124
|
+
await postJson(
|
|
125
|
+
config.serverUrl,
|
|
126
|
+
"/api/connectors/unregister",
|
|
127
|
+
{
|
|
128
|
+
connectorId: config.connectorId,
|
|
129
|
+
},
|
|
130
|
+
launchConfig,
|
|
131
|
+
).catch((error) => {
|
|
132
|
+
void writeLog("warn", `unregister failed: ${error instanceof Error ? error.message : error}`);
|
|
133
|
+
});
|
|
134
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir, hostname, platform } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { ConnectorLaunchConfig, DeviceConfig } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export const CONFIG_DIR = path.join(homedir(), ".buda");
|
|
8
|
+
export const CONFIG_PATH = path.join(CONFIG_DIR, "device.json");
|
|
9
|
+
export const LOG_PATH = path.join(CONFIG_DIR, "connector.log");
|
|
10
|
+
export const DEFAULT_SERVER_URL = "https://buda.im";
|
|
11
|
+
export const DEFAULT_WORKDIR_ROOT = path.join(CONFIG_DIR, "agents");
|
|
12
|
+
export const DEFAULT_BUNNY_DAEMON_HOST = "127.0.0.1";
|
|
13
|
+
export const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
14
|
+
export const RELAY_RECONNECT_INTERVAL_MS = 3_000;
|
|
15
|
+
export const LAUNCH_CONFIG_STDIN_FLAG = "--launch-config-stdin";
|
|
16
|
+
|
|
17
|
+
export function getServerUrl(launchConfig?: ConnectorLaunchConfig) {
|
|
18
|
+
return (
|
|
19
|
+
launchConfig?.serverUrl ||
|
|
20
|
+
process.env.BUDA_CONNECTOR_SERVER_URL ||
|
|
21
|
+
process.env.BUDA_SERVER_URL ||
|
|
22
|
+
process.env.NEXT_PUBLIC_APP_URL ||
|
|
23
|
+
DEFAULT_SERVER_URL
|
|
24
|
+
).replace(/\/$/, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getConnectorRelayUrl(serverUrl: string, connectorId: string) {
|
|
28
|
+
const url = new URL("/api/connectors/ws", serverUrl);
|
|
29
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
30
|
+
url.searchParams.set("connectorId", connectorId);
|
|
31
|
+
return url.toString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getWorkdirRoot(launchConfig?: ConnectorLaunchConfig) {
|
|
35
|
+
return launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || DEFAULT_WORKDIR_ROOT;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getBunnyDaemonHost(launchConfig?: ConnectorLaunchConfig) {
|
|
39
|
+
return (
|
|
40
|
+
launchConfig?.bunnyDaemonHost ||
|
|
41
|
+
process.env.BUDA_CONNECTOR_BUNNY_DAEMON_HOST ||
|
|
42
|
+
DEFAULT_BUNNY_DAEMON_HOST
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getBunnyDaemonPort(launchConfig?: ConnectorLaunchConfig) {
|
|
47
|
+
if (launchConfig?.bunnyDaemonPort !== undefined) return launchConfig.bunnyDaemonPort;
|
|
48
|
+
const raw = process.env.BUDA_CONNECTOR_BUNNY_DAEMON_PORT;
|
|
49
|
+
if (!raw) return 0;
|
|
50
|
+
const parsed = Number(raw);
|
|
51
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getSpaceId(launchConfig?: ConnectorLaunchConfig) {
|
|
55
|
+
return launchConfig?.spaceId || process.env.BUDA_CONNECTOR_SPACE_ID;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildDeviceConfig(launchConfig?: ConnectorLaunchConfig): DeviceConfig {
|
|
59
|
+
const host = hostname();
|
|
60
|
+
return {
|
|
61
|
+
connectorId: `cnr_${randomUUID()}`,
|
|
62
|
+
deviceId: `dev_${randomUUID()}`,
|
|
63
|
+
deviceName: host,
|
|
64
|
+
hostLabel: `${host} (${platform()})`,
|
|
65
|
+
serverUrl: getServerUrl(launchConfig),
|
|
66
|
+
workdirRoot: getWorkdirRoot(launchConfig),
|
|
67
|
+
createdAt: new Date().toISOString(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function readDeviceConfig(): Promise<DeviceConfig | null> {
|
|
72
|
+
try {
|
|
73
|
+
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
74
|
+
return JSON.parse(raw) as DeviceConfig;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function writeDeviceConfig(config: DeviceConfig) {
|
|
81
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
82
|
+
await mkdir(config.workdirRoot, { recursive: true });
|
|
83
|
+
await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function ensureDeviceConfig(
|
|
87
|
+
launchConfig?: ConnectorLaunchConfig,
|
|
88
|
+
): Promise<DeviceConfig> {
|
|
89
|
+
const existing = await readDeviceConfig();
|
|
90
|
+
if (existing) {
|
|
91
|
+
return {
|
|
92
|
+
...existing,
|
|
93
|
+
serverUrl: getServerUrl(launchConfig),
|
|
94
|
+
workdirRoot:
|
|
95
|
+
launchConfig?.workdirRoot ||
|
|
96
|
+
process.env.BUDA_CONNECTOR_WORKDIR ||
|
|
97
|
+
existing.workdirRoot ||
|
|
98
|
+
getWorkdirRoot(launchConfig),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const config = buildDeviceConfig(launchConfig);
|
|
102
|
+
await writeDeviceConfig(config);
|
|
103
|
+
return config;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function parseLaunchConfig(raw: string): ConnectorLaunchConfig {
|
|
107
|
+
if (!raw.trim()) return {};
|
|
108
|
+
const parsed = JSON.parse(raw) as Partial<ConnectorLaunchConfig>;
|
|
109
|
+
const bunnyDaemonPort =
|
|
110
|
+
typeof parsed.bunnyDaemonPort === "number" && Number.isInteger(parsed.bunnyDaemonPort)
|
|
111
|
+
? parsed.bunnyDaemonPort
|
|
112
|
+
: undefined;
|
|
113
|
+
return {
|
|
114
|
+
serverUrl: typeof parsed.serverUrl === "string" ? parsed.serverUrl : undefined,
|
|
115
|
+
workdirRoot: typeof parsed.workdirRoot === "string" ? parsed.workdirRoot : undefined,
|
|
116
|
+
oauthToken: typeof parsed.oauthToken === "string" ? parsed.oauthToken : undefined,
|
|
117
|
+
spaceId: typeof parsed.spaceId === "string" ? parsed.spaceId : undefined,
|
|
118
|
+
bunnyDaemonHost:
|
|
119
|
+
typeof parsed.bunnyDaemonHost === "string" ? parsed.bunnyDaemonHost : undefined,
|
|
120
|
+
bunnyDaemonPort,
|
|
121
|
+
};
|
|
122
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { CONFIG_DIR, LOG_PATH } from "./config.js";
|
|
3
|
+
import type { ConnectorLogEntry } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const pendingLogs: ConnectorLogEntry[] = [];
|
|
6
|
+
|
|
7
|
+
export async function writeLog(
|
|
8
|
+
level: ConnectorLogEntry["level"],
|
|
9
|
+
message: string,
|
|
10
|
+
metadata?: Record<string, unknown>,
|
|
11
|
+
) {
|
|
12
|
+
const entry: ConnectorLogEntry = {
|
|
13
|
+
level,
|
|
14
|
+
message,
|
|
15
|
+
metadata,
|
|
16
|
+
createdAt: new Date().toISOString(),
|
|
17
|
+
};
|
|
18
|
+
pendingLogs.push(entry);
|
|
19
|
+
if (pendingLogs.length > 100) pendingLogs.splice(0, pendingLogs.length - 100);
|
|
20
|
+
|
|
21
|
+
await mkdir(CONFIG_DIR, { recursive: true }).catch(() => undefined);
|
|
22
|
+
await appendFile(LOG_PATH, `${JSON.stringify(entry)}\n`).catch(() => undefined);
|
|
23
|
+
|
|
24
|
+
const line = `[buda-connector] ${message}`;
|
|
25
|
+
if (level === "error") console.error(line);
|
|
26
|
+
else if (level === "warn") console.warn(line);
|
|
27
|
+
else console.log(line);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function takePendingLogs(): ConnectorLogEntry[] {
|
|
31
|
+
return pendingLogs.splice(0, pendingLogs.length);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function restorePendingLogs(logs: ConnectorLogEntry[]) {
|
|
35
|
+
pendingLogs.unshift(...logs);
|
|
36
|
+
}
|
package/src/relay.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { attachRelayClient, type RelayRequestHandler } from "relaylib";
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
import { getConnectorRelayUrl, RELAY_RECONNECT_INTERVAL_MS } from "./config.js";
|
|
4
|
+
import { writeLog } from "./logger.js";
|
|
5
|
+
import type { ConnectorLaunchConfig, DeviceConfig, EmbeddedBunnyDaemon } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export function startConnectorRelay(
|
|
8
|
+
config: DeviceConfig,
|
|
9
|
+
bunnyDaemon: EmbeddedBunnyDaemon,
|
|
10
|
+
launchConfig?: ConnectorLaunchConfig,
|
|
11
|
+
) {
|
|
12
|
+
let socket: WebSocket | null = null;
|
|
13
|
+
let reconnectTimer: NodeJS.Timeout | null = null;
|
|
14
|
+
let stopped = false;
|
|
15
|
+
const handler = createRelayHandler(bunnyDaemon);
|
|
16
|
+
|
|
17
|
+
const connect = () => {
|
|
18
|
+
if (stopped) return;
|
|
19
|
+
const relayUrl = getConnectorRelayUrl(config.serverUrl, config.connectorId);
|
|
20
|
+
const token = launchConfig?.oauthToken?.trim();
|
|
21
|
+
socket = new WebSocket(relayUrl, {
|
|
22
|
+
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
socket.on("open", () => {
|
|
26
|
+
void writeLog("info", `relay connected ${relayUrl}`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// The protocol loop (request -> handler -> streamed start/chunk/end) lives in
|
|
30
|
+
// relaylib. `chunkEncoding: "utf8"` keeps the legacy text wire format, since a
|
|
31
|
+
// base64-aware hub is not guaranteed to be deployed yet (the connector ships
|
|
32
|
+
// independently of the cloud). Switch to base64 once that hub is rolled out.
|
|
33
|
+
attachRelayClient(socket, handler, { chunkEncoding: "utf8" });
|
|
34
|
+
|
|
35
|
+
socket.on("close", () => {
|
|
36
|
+
void writeLog("warn", "relay disconnected");
|
|
37
|
+
if (stopped) return;
|
|
38
|
+
reconnectTimer = setTimeout(connect, RELAY_RECONNECT_INTERVAL_MS);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
socket.on("error", (error) => {
|
|
42
|
+
void writeLog("warn", `relay error: ${error instanceof Error ? error.message : error}`);
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
connect();
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
stop() {
|
|
50
|
+
stopped = true;
|
|
51
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
52
|
+
socket?.close();
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* App-specific relay handler: forwards each request to the embedded bunny daemon
|
|
59
|
+
* only (origin guard), records the relay audit headers, then proxies via fetch.
|
|
60
|
+
*/
|
|
61
|
+
function createRelayHandler(bunnyDaemon: EmbeddedBunnyDaemon): RelayRequestHandler {
|
|
62
|
+
const allowedOrigin = new URL(bunnyDaemon.baseUrl).origin;
|
|
63
|
+
|
|
64
|
+
return async (request) => {
|
|
65
|
+
const requestUrl = new URL(request.path, bunnyDaemon.baseUrl);
|
|
66
|
+
if (requestUrl.origin !== allowedOrigin) {
|
|
67
|
+
throw new Error("Relay request must target the embedded bunny daemon.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await writeLog("info", `relay request ${request.method} ${requestUrl.pathname}`, {
|
|
71
|
+
actorEmail: getHeaderValue(request.headers, "x-buda-relay-actor-email") || null,
|
|
72
|
+
actorId: getHeaderValue(request.headers, "x-buda-relay-actor-id") || null,
|
|
73
|
+
source: getHeaderValue(request.headers, "x-buda-relay-source") || "unknown",
|
|
74
|
+
action: getHeaderValue(request.headers, "x-buda-relay-action") || "unknown",
|
|
75
|
+
target: requestUrl.toString(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return fetch(requestUrl, {
|
|
79
|
+
method: request.method,
|
|
80
|
+
headers: {
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
...(request.headers ?? {}),
|
|
83
|
+
},
|
|
84
|
+
body: request.body === undefined ? undefined : JSON.stringify(request.body),
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getHeaderValue(headers: Record<string, string> | undefined, name: string) {
|
|
90
|
+
if (!headers) return undefined;
|
|
91
|
+
const normalized = name.toLowerCase();
|
|
92
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
93
|
+
if (key.toLowerCase() === normalized) return value;
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { startEmbeddedBunnyDaemon, stopEmbeddedBunnyDaemon } from "./bunny-daemon.js";
|
|
2
|
+
import { heartbeatConnector, registerConnector, unregisterConnector } from "./cloud-client.js";
|
|
3
|
+
import {
|
|
4
|
+
buildDeviceConfig,
|
|
5
|
+
CONFIG_PATH,
|
|
6
|
+
ensureDeviceConfig,
|
|
7
|
+
HEARTBEAT_INTERVAL_MS,
|
|
8
|
+
LOG_PATH,
|
|
9
|
+
writeDeviceConfig,
|
|
10
|
+
} from "./config.js";
|
|
11
|
+
import { writeLog } from "./logger.js";
|
|
12
|
+
import { startConnectorRelay } from "./relay.js";
|
|
13
|
+
import type { ConnectorLaunchConfig } from "./types.js";
|
|
14
|
+
|
|
15
|
+
export async function runNewConnector(launchConfig?: ConnectorLaunchConfig) {
|
|
16
|
+
const config = buildDeviceConfig(launchConfig);
|
|
17
|
+
await writeDeviceConfig(config);
|
|
18
|
+
await registerConnector(config, launchConfig);
|
|
19
|
+
await writeLog("info", `wrote ${CONFIG_PATH}`);
|
|
20
|
+
await writeLog("info", `workdir root ${config.workdirRoot}`);
|
|
21
|
+
await writeLog("info", `log file ${LOG_PATH}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function runConnectorDaemon(launchConfig?: ConnectorLaunchConfig) {
|
|
25
|
+
const config = await ensureDeviceConfig(launchConfig);
|
|
26
|
+
const bunnyDaemon = await startEmbeddedBunnyDaemon(config, launchConfig);
|
|
27
|
+
try {
|
|
28
|
+
await registerConnector(config, launchConfig, bunnyDaemon);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
await stopEmbeddedBunnyDaemon(bunnyDaemon);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
const relay = startConnectorRelay(config, bunnyDaemon, launchConfig);
|
|
34
|
+
await writeLog("info", `server ${config.serverUrl}`);
|
|
35
|
+
await writeLog("info", `config ${CONFIG_PATH}`);
|
|
36
|
+
await writeLog("info", `workdir root ${config.workdirRoot}`);
|
|
37
|
+
await writeLog("info", `log file ${LOG_PATH}`);
|
|
38
|
+
await writeLog("info", `bunny daemon ${bunnyDaemon.baseUrl}`);
|
|
39
|
+
|
|
40
|
+
const interval = setInterval(() => {
|
|
41
|
+
heartbeatConnector(config, launchConfig, bunnyDaemon).catch((error) => {
|
|
42
|
+
void writeLog(
|
|
43
|
+
"warn",
|
|
44
|
+
`heartbeat failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
48
|
+
|
|
49
|
+
const shutdown = async () => {
|
|
50
|
+
clearInterval(interval);
|
|
51
|
+
relay.stop();
|
|
52
|
+
await unregisterConnector(config, launchConfig);
|
|
53
|
+
await stopEmbeddedBunnyDaemon(bunnyDaemon);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
process.on("SIGINT", shutdown);
|
|
58
|
+
process.on("SIGTERM", shutdown);
|
|
59
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Server } from "node:http";
|
|
2
|
+
|
|
3
|
+
export type ConnectorCliCommand = "new" | "dev" | "daemon";
|
|
4
|
+
|
|
5
|
+
export interface DeviceConfig {
|
|
6
|
+
connectorId: string;
|
|
7
|
+
deviceId: string;
|
|
8
|
+
deviceName: string;
|
|
9
|
+
hostLabel: string;
|
|
10
|
+
serverUrl: string;
|
|
11
|
+
workdirRoot: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ConnectorLaunchConfig {
|
|
16
|
+
serverUrl?: string;
|
|
17
|
+
workdirRoot?: string;
|
|
18
|
+
oauthToken?: string;
|
|
19
|
+
spaceId?: string;
|
|
20
|
+
bunnyDaemonHost?: string;
|
|
21
|
+
bunnyDaemonPort?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface EmbeddedBunnyDaemon {
|
|
25
|
+
server: Server;
|
|
26
|
+
host: string;
|
|
27
|
+
port: number;
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
root: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ConnectorLogEntry {
|
|
33
|
+
level: "info" | "warn" | "error";
|
|
34
|
+
message: string;
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM"],
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"types": ["node"],
|
|
13
|
+
"paths": {
|
|
14
|
+
"kui/ai-elements/*": ["../../packages/kui/src/components/ai-elements/*"],
|
|
15
|
+
"kui/hooks/*": ["../../packages/kui/src/hooks/*"],
|
|
16
|
+
"kui/tour": ["../../packages/kui/src/components/tour.tsx"],
|
|
17
|
+
"kui/utils": ["../../packages/kui/src/lib/utils.ts"],
|
|
18
|
+
"kui/*": ["../../packages/kui/src/components/ui/*"],
|
|
19
|
+
"sharelib": ["../../packages/sharelib/index.ts"],
|
|
20
|
+
"sharelib/onboarding-types": ["../../packages/sharelib/src/onboarding-types.ts"],
|
|
21
|
+
"sharelib/scheduler/cache": ["../../packages/sharelib/scheduler/scheduler-cache.ts"],
|
|
22
|
+
"sharelib/scheduler/cron": ["../../packages/sharelib/scheduler/cron-parser.ts"],
|
|
23
|
+
"sharelib/ui/ProductScreenshot": ["../../packages/sharelib/ui/common/ProductScreenshot.tsx"],
|
|
24
|
+
"sharelib/ui/ProductShowcase": ["../../packages/sharelib/ui/common/ProductShowcase.tsx"],
|
|
25
|
+
"sharelib/ui/LanguageSwitcher": ["../../packages/sharelib/ui/common/LanguageSwitcher.tsx"],
|
|
26
|
+
"sharelib/ui/dashboard": ["../../packages/sharelib/ui/dashboard/index.ts"],
|
|
27
|
+
"sharelib/*": ["../../packages/sharelib/*"],
|
|
28
|
+
"share-domains/*": ["../../packages/share-domains/*"],
|
|
29
|
+
"billing": ["../../packages/billing/index.ts"],
|
|
30
|
+
"billing/*": ["../../packages/billing/*"],
|
|
31
|
+
"emaillib": ["../../packages/emaillib/index.ts"],
|
|
32
|
+
"emaillib/types": ["../../packages/emaillib/types/index.ts"],
|
|
33
|
+
"transactional/components": ["../../packages/transactional/emails/_components/index.ts"],
|
|
34
|
+
"transactional/emails/*": ["../../packages/transactional/emails/*.tsx"],
|
|
35
|
+
"transactional/*": ["../../packages/transactional/emails/*.tsx"],
|
|
36
|
+
"@mcpsdk/sdk-ts": ["../../packages/@mcpsdk/sdk-ts/src/index.ts"],
|
|
37
|
+
"@mcpsdk/sdk-ts/*": ["../../packages/@mcpsdk/sdk-ts/src/*"],
|
|
38
|
+
"@mcpsdk/plugin-core": ["../../packages/@mcpsdk/plugin-core/src/index.ts"],
|
|
39
|
+
"@mcpsdk/plugin-core/*": ["../../packages/@mcpsdk/plugin-core/src/*"],
|
|
40
|
+
"@mcpsdk/mcp-server": ["../../packages/@mcpsdk/mcp-server/index.js"]
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"include": ["src/**/*.ts"]
|
|
44
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { defineConfig } from "tsup";
|
|
3
|
+
|
|
4
|
+
// relaylib ships TypeScript source (its package exports point at `./src/*.ts`),
|
|
5
|
+
// so it cannot be a runtime dependency of this published npm CLI. Bundle it
|
|
6
|
+
// straight into dist instead — the connector stays standalone. `ws` and
|
|
7
|
+
// @bunny-agent/* stay external (real runtime deps).
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
entry: { cli: "src/cli.ts" },
|
|
10
|
+
format: ["esm"],
|
|
11
|
+
target: "node20",
|
|
12
|
+
platform: "node",
|
|
13
|
+
outDir: "dist",
|
|
14
|
+
clean: true,
|
|
15
|
+
dts: false,
|
|
16
|
+
noExternal: [/^relaylib/],
|
|
17
|
+
esbuildOptions(options) {
|
|
18
|
+
options.nodePaths = [resolve(process.cwd(), "node_modules")];
|
|
19
|
+
},
|
|
20
|
+
});
|