@greatlhd/ailo-endpoint-sdk 1.0.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 (89) hide show
  1. package/dist/blob/client.d.ts +15 -0
  2. package/dist/blob/client.d.ts.map +1 -0
  3. package/dist/blob/client.js +48 -0
  4. package/dist/blob/client.js.map +1 -0
  5. package/dist/blob/index.d.ts +3 -0
  6. package/dist/blob/index.d.ts.map +1 -0
  7. package/dist/blob/index.js +2 -0
  8. package/dist/blob/index.js.map +1 -0
  9. package/dist/bootstrap.d.ts +37 -0
  10. package/dist/bootstrap.d.ts.map +1 -0
  11. package/dist/bootstrap.js +145 -0
  12. package/dist/bootstrap.js.map +1 -0
  13. package/dist/config-io.d.ts +6 -0
  14. package/dist/config-io.d.ts.map +1 -0
  15. package/dist/config-io.js +43 -0
  16. package/dist/config-io.js.map +1 -0
  17. package/dist/connection-state.d.ts +24 -0
  18. package/dist/connection-state.d.ts.map +1 -0
  19. package/dist/connection-state.js +83 -0
  20. package/dist/connection-state.js.map +1 -0
  21. package/dist/connection-util.d.ts +12 -0
  22. package/dist/connection-util.d.ts.map +1 -0
  23. package/dist/connection-util.js +15 -0
  24. package/dist/connection-util.js.map +1 -0
  25. package/dist/endpoint-client.d.ts +92 -0
  26. package/dist/endpoint-client.d.ts.map +1 -0
  27. package/dist/endpoint-client.js +669 -0
  28. package/dist/endpoint-client.js.map +1 -0
  29. package/dist/endpoint-config.d.ts +28 -0
  30. package/dist/endpoint-config.d.ts.map +1 -0
  31. package/dist/endpoint-config.js +277 -0
  32. package/dist/endpoint-config.js.map +1 -0
  33. package/dist/errors.d.ts +17 -0
  34. package/dist/errors.d.ts.map +1 -0
  35. package/dist/errors.js +40 -0
  36. package/dist/errors.js.map +1 -0
  37. package/dist/fileref.d.ts +22 -0
  38. package/dist/fileref.d.ts.map +1 -0
  39. package/dist/fileref.js +41 -0
  40. package/dist/fileref.js.map +1 -0
  41. package/dist/index.d.ts +29 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +18 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/local-endpoint-storage.d.ts +11 -0
  46. package/dist/local-endpoint-storage.d.ts.map +1 -0
  47. package/dist/local-endpoint-storage.js +143 -0
  48. package/dist/local-endpoint-storage.js.map +1 -0
  49. package/dist/logger.d.ts +25 -0
  50. package/dist/logger.d.ts.map +1 -0
  51. package/dist/logger.js +27 -0
  52. package/dist/logger.js.map +1 -0
  53. package/dist/media-util.d.ts +8 -0
  54. package/dist/media-util.d.ts.map +1 -0
  55. package/dist/media-util.js +78 -0
  56. package/dist/media-util.js.map +1 -0
  57. package/dist/middleware/index.d.ts +2 -0
  58. package/dist/middleware/index.d.ts.map +1 -0
  59. package/dist/middleware/index.js +2 -0
  60. package/dist/middleware/index.js.map +1 -0
  61. package/dist/middleware/media.d.ts +7 -0
  62. package/dist/middleware/media.d.ts.map +1 -0
  63. package/dist/middleware/media.js +112 -0
  64. package/dist/middleware/media.js.map +1 -0
  65. package/dist/skill-loader.d.ts +19 -0
  66. package/dist/skill-loader.d.ts.map +1 -0
  67. package/dist/skill-loader.js +131 -0
  68. package/dist/skill-loader.js.map +1 -0
  69. package/dist/tool-dispatch/index.d.ts +3 -0
  70. package/dist/tool-dispatch/index.d.ts.map +1 -0
  71. package/dist/tool-dispatch/index.js +2 -0
  72. package/dist/tool-dispatch/index.js.map +1 -0
  73. package/dist/tool-dispatch/normalize.d.ts +5 -0
  74. package/dist/tool-dispatch/normalize.d.ts.map +1 -0
  75. package/dist/tool-dispatch/normalize.js +100 -0
  76. package/dist/tool-dispatch/normalize.js.map +1 -0
  77. package/dist/types.d.ts +150 -0
  78. package/dist/types.d.ts.map +1 -0
  79. package/dist/types.js +13 -0
  80. package/dist/types.js.map +1 -0
  81. package/dist/utils/content.d.ts +7 -0
  82. package/dist/utils/content.d.ts.map +1 -0
  83. package/dist/utils/content.js +30 -0
  84. package/dist/utils/content.js.map +1 -0
  85. package/dist/utils/index.d.ts +2 -0
  86. package/dist/utils/index.d.ts.map +1 -0
  87. package/dist/utils/index.js +2 -0
  88. package/dist/utils/index.js.map +1 -0
  89. package/package.json +36 -0
@@ -0,0 +1,669 @@
1
+ import WebSocket from "ws";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import crypto from "crypto";
6
+ import { EndpointError } from "./errors.js";
7
+ import { ConsoleLogger } from "./logger.js";
8
+ import { ConnectionFSM } from "./connection-state.js";
9
+ import { toContentParts } from "./utils/index.js";
10
+ const SDK_VERSION = "1.0.0";
11
+ function resolveBlueprintPath(blueprint) {
12
+ if (!blueprint)
13
+ return "";
14
+ if (blueprint.startsWith("http://") || blueprint.startsWith("https://") || blueprint.startsWith("file://")) {
15
+ return blueprint;
16
+ }
17
+ return path.resolve(blueprint);
18
+ }
19
+ function jsonTextContent(value) {
20
+ const text = JSON.stringify(value);
21
+ if (text === undefined) {
22
+ throw new Error("structured payload must be JSON-serializable");
23
+ }
24
+ return [{ type: "text", text }];
25
+ }
26
+ const DEFAULT_OPTIONS = {
27
+ handshakeTimeout: 30_000,
28
+ heartbeatInterval: 30_000,
29
+ heartbeatTimeout: 10_000,
30
+ reconnectBaseDelay: 1_000,
31
+ reconnectMaxDelay: 60_000,
32
+ offlineBufferSize: 200,
33
+ logger: new ConsoleLogger('[endpoint]'),
34
+ };
35
+ export const RECONNECT_COOLDOWN_MS = 1000;
36
+ export class EndpointClient {
37
+ ws = null;
38
+ cfg;
39
+ opts;
40
+ fsm;
41
+ reqId = 0;
42
+ pending = new Map();
43
+ heartbeatTimer = null;
44
+ pongTimer = null;
45
+ reconnectTimer = null;
46
+ reconnectAttempt = 0;
47
+ offlineBuffer = [];
48
+ offlineBufferMax;
49
+ toolRequestHandler = null;
50
+ intentHandler = null;
51
+ worldEnrichmentHandler = null;
52
+ streamHandler = null;
53
+ signalHandlers = new Map();
54
+ evictedHandler = null;
55
+ fsProbeMarker = null;
56
+ reconnectWaiters = [];
57
+ constructor(config, options) {
58
+ this.cfg = config;
59
+ this.opts = {
60
+ handshakeTimeout: options?.handshakeTimeout ?? DEFAULT_OPTIONS.handshakeTimeout,
61
+ heartbeatInterval: options?.heartbeatInterval ?? DEFAULT_OPTIONS.heartbeatInterval,
62
+ heartbeatTimeout: options?.heartbeatTimeout ?? DEFAULT_OPTIONS.heartbeatTimeout,
63
+ reconnectBaseDelay: options?.reconnectBaseDelay ?? DEFAULT_OPTIONS.reconnectBaseDelay,
64
+ reconnectMaxDelay: options?.reconnectMaxDelay ?? DEFAULT_OPTIONS.reconnectMaxDelay,
65
+ offlineBufferSize: options?.offlineBufferSize ?? DEFAULT_OPTIONS.offlineBufferSize,
66
+ logger: options?.logger ?? DEFAULT_OPTIONS.logger,
67
+ };
68
+ this.offlineBufferMax = config.offlineBufferSize ?? this.opts.offlineBufferSize;
69
+ this.fsm = new ConnectionFSM();
70
+ this.fsProbeMarker = this.writeFsProbeFile();
71
+ this.fsm.onStateChange((transition) => {
72
+ this.opts.logger.debug('state_change', {
73
+ from: transition.from,
74
+ to: transition.to,
75
+ reason: transition.reason,
76
+ });
77
+ });
78
+ }
79
+ get state() {
80
+ return this.fsm.state;
81
+ }
82
+ get isConnected() {
83
+ return this.fsm.isConnected;
84
+ }
85
+ onStateChange(listener) {
86
+ return this.fsm.onStateChange(listener);
87
+ }
88
+ setLogger(logger) {
89
+ this.opts.logger = logger;
90
+ }
91
+ async connect() {
92
+ if (!this.fsm.canTransitionTo('connecting')) {
93
+ throw EndpointError.notConnected();
94
+ }
95
+ this.fsm.transition('connecting', 'user initiated');
96
+ await this.dial();
97
+ }
98
+ close() {
99
+ if (this.fsm.state === 'disconnected')
100
+ return;
101
+ this.fsm.transition('closing', 'user close');
102
+ if (this.reconnectTimer) {
103
+ clearTimeout(this.reconnectTimer);
104
+ this.reconnectTimer = null;
105
+ }
106
+ this.stopHeartbeat();
107
+ this.rejectAllPending(new Error("client closed"));
108
+ this.rejectReconnectWaiters(new Error("client closed"));
109
+ this.offlineBuffer.length = 0;
110
+ if (this.ws) {
111
+ this.ws.close();
112
+ this.ws = null;
113
+ }
114
+ if (this.fsProbeMarker) {
115
+ try {
116
+ fs.unlinkSync(this.fsProbeMarker.path);
117
+ }
118
+ catch { }
119
+ this.fsProbeMarker = null;
120
+ }
121
+ this.fsm.forceTransition('disconnected', 'close complete');
122
+ }
123
+ async reconnect(skills, connectionOverrides, tools, blueprints) {
124
+ if (skills !== undefined)
125
+ this.cfg = { ...this.cfg, skills };
126
+ if (tools !== undefined)
127
+ this.cfg = { ...this.cfg, tools };
128
+ if (blueprints !== undefined)
129
+ this.cfg = { ...this.cfg, blueprints };
130
+ if (connectionOverrides) {
131
+ const { url, apiKey, endpointId } = connectionOverrides;
132
+ if (url !== undefined)
133
+ this.cfg = { ...this.cfg, url };
134
+ if (apiKey !== undefined)
135
+ this.cfg = { ...this.cfg, apiKey };
136
+ if (endpointId !== undefined)
137
+ this.cfg = { ...this.cfg, endpointId };
138
+ }
139
+ this.fsm.forceTransition('closing', 'reconnect initiated');
140
+ if (this.reconnectTimer) {
141
+ clearTimeout(this.reconnectTimer);
142
+ this.reconnectTimer = null;
143
+ }
144
+ this.stopHeartbeat();
145
+ this.rejectAllPending(new Error("reconnecting"));
146
+ if (this.ws) {
147
+ this.ws.close();
148
+ this.ws = null;
149
+ }
150
+ this.fsm.forceTransition('disconnected', 'reconnect cleanup');
151
+ this.reconnectAttempt = 0;
152
+ if (connectionOverrides) {
153
+ await new Promise((r) => setTimeout(r, RECONNECT_COOLDOWN_MS));
154
+ }
155
+ await this.connect();
156
+ }
157
+ async update(params) {
158
+ await this.request("endpoint.update", params);
159
+ }
160
+ onToolRequest(handler) {
161
+ this.toolRequestHandler = handler;
162
+ return this;
163
+ }
164
+ onIntent(handler) {
165
+ this.intentHandler = handler;
166
+ return this;
167
+ }
168
+ onWorldEnrichment(handler) {
169
+ this.worldEnrichmentHandler = handler;
170
+ return this;
171
+ }
172
+ onStream(handler) {
173
+ this.streamHandler = handler;
174
+ return this;
175
+ }
176
+ onSignal(signal, handler) {
177
+ const list = this.signalHandlers.get(signal) ?? [];
178
+ list.push(handler);
179
+ this.signalHandlers.set(signal, list);
180
+ return this;
181
+ }
182
+ onEvicted(handler) {
183
+ this.evictedHandler = handler;
184
+ return this;
185
+ }
186
+ async accept(msg) {
187
+ if (!this.fsm.isConnected) {
188
+ if (this.fsm.state === 'closing' || this.fsm.state === 'disconnected' || this.offlineBufferMax <= 0) {
189
+ throw EndpointError.notConnected();
190
+ }
191
+ if (this.offlineBuffer.length >= this.offlineBufferMax) {
192
+ throw new Error(`offline buffer full (${this.offlineBufferMax})`);
193
+ }
194
+ this.offlineBuffer.push(msg);
195
+ return;
196
+ }
197
+ await this.acceptDirect(msg);
198
+ }
199
+ async toolResponse(payload) {
200
+ if (!this.fsm.isConnected) {
201
+ if (this.fsm.state !== 'closing' && this.fsm.state !== 'disconnected') {
202
+ await this.waitForConnection();
203
+ }
204
+ }
205
+ await this.request("tool_response", payload);
206
+ }
207
+ sendSignal(signal, data) {
208
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
209
+ return;
210
+ this.ws.send(JSON.stringify({ type: "signal", id: signal, payload: data }));
211
+ }
212
+ dial() {
213
+ return new Promise((resolve, reject) => {
214
+ const ws = new WebSocket(this.cfg.url);
215
+ let settled = false;
216
+ const settle = (ok, err) => {
217
+ if (settled)
218
+ return;
219
+ settled = true;
220
+ if (ok) {
221
+ this.fsm.transition('connected', 'handshake complete');
222
+ resolve();
223
+ }
224
+ else {
225
+ this.fsm.transition('disconnected', 'dial failed');
226
+ reject(err);
227
+ }
228
+ };
229
+ ws.on("open", async () => {
230
+ try {
231
+ await this.handshake(ws);
232
+ this.ws = ws;
233
+ this.reconnectAttempt = 0;
234
+ this.attachHandlers(ws);
235
+ this.startHeartbeat();
236
+ this.flushOfflineBuffer();
237
+ this.resolveReconnectWaiters();
238
+ settle(true);
239
+ }
240
+ catch (err) {
241
+ ws.close();
242
+ settle(false, err);
243
+ }
244
+ });
245
+ ws.on("error", (err) => {
246
+ this.opts.logger.error('ws_error', { error: err.message });
247
+ settle(false, EndpointError.network('WebSocket error', err));
248
+ });
249
+ ws.on("close", () => settle(false, EndpointError.network("closed before handshake")));
250
+ });
251
+ }
252
+ handshake(ws) {
253
+ const id = `connect-${++this.reqId}`;
254
+ return new Promise((resolve, reject) => {
255
+ const timer = setTimeout(() => {
256
+ ws.off("message", handler);
257
+ this.opts.logger.warn('handshake_timeout', { timeout: this.opts.handshakeTimeout });
258
+ reject(EndpointError.timeout("handshake timeout"));
259
+ }, this.opts.handshakeTimeout);
260
+ const handler = (raw) => {
261
+ const frame = JSON.parse(raw.toString("utf-8"));
262
+ if (frame.type === "res" && frame.id === id) {
263
+ clearTimeout(timer);
264
+ ws.off("message", handler);
265
+ if (frame.ok) {
266
+ resolve();
267
+ }
268
+ else {
269
+ const errMsg = frame.error?.message ?? "connect rejected";
270
+ this.opts.logger.error('handshake_failed', { error: errMsg });
271
+ if (frame.error?.code === 'AUTH_FAILED') {
272
+ reject(EndpointError.auth(errMsg));
273
+ }
274
+ else {
275
+ reject(EndpointError.handshakeFailed(errMsg));
276
+ }
277
+ }
278
+ }
279
+ };
280
+ ws.on("message", handler);
281
+ const connectParams = {
282
+ role: "endpoint",
283
+ apiKey: this.cfg.apiKey,
284
+ endpointId: this.cfg.endpointId,
285
+ caps: this.cfg.caps,
286
+ sdkVersion: SDK_VERSION,
287
+ };
288
+ if (this.cfg.instructions)
289
+ connectParams.instructions = this.cfg.instructions;
290
+ if (this.cfg.tools && this.cfg.tools.length > 0)
291
+ connectParams.tools = this.cfg.tools;
292
+ if (this.cfg.blueprints && this.cfg.blueprints.length > 0) {
293
+ connectParams.blueprints = this.cfg.blueprints.map(resolveBlueprintPath);
294
+ }
295
+ if (this.cfg.skills && this.cfg.skills.length > 0)
296
+ connectParams.skills = this.cfg.skills;
297
+ if (this.fsProbeMarker)
298
+ connectParams.fsProbe = this.fsProbeMarker;
299
+ ws.send(JSON.stringify({ type: "req", id, method: "connect", params: connectParams }));
300
+ });
301
+ }
302
+ attachHandlers(ws) {
303
+ ws.on("message", (raw) => {
304
+ const frame = JSON.parse(raw.toString("utf-8"));
305
+ if (frame.type === "res" && frame.id) {
306
+ const req = this.pending.get(frame.id);
307
+ if (!req)
308
+ return;
309
+ this.pending.delete(frame.id);
310
+ frame.ok
311
+ ? req.resolve(frame.payload ?? {})
312
+ : req.reject(new Error(frame.error?.message ?? "request failed"));
313
+ return;
314
+ }
315
+ if (frame.type === "event") {
316
+ this.handleEvent(frame);
317
+ return;
318
+ }
319
+ if (frame.type === "signal" && frame.id) {
320
+ const handlers = this.signalHandlers.get(frame.id);
321
+ if (handlers) {
322
+ for (const h of handlers)
323
+ h(frame.id, frame.payload);
324
+ }
325
+ return;
326
+ }
327
+ });
328
+ ws.on("close", (code, reason) => {
329
+ const isEvicted = code === 1001 && reason?.toString("utf-8").includes("replaced");
330
+ if (isEvicted) {
331
+ this.opts.logger.warn('evicted', { code, reason: reason.toString() });
332
+ this.fsm.forceTransition('disconnected', 'evicted');
333
+ this.rejectReconnectWaiters(EndpointError.evicted());
334
+ this.evictedHandler?.();
335
+ return;
336
+ }
337
+ if (ws !== this.ws)
338
+ return;
339
+ this.onDisconnect();
340
+ });
341
+ ws.on("pong", () => {
342
+ if (this.pongTimer) {
343
+ clearTimeout(this.pongTimer);
344
+ this.pongTimer = null;
345
+ }
346
+ });
347
+ }
348
+ handleEvent(frame) {
349
+ switch (frame.event) {
350
+ case "tool_request": {
351
+ const payload = frame.payload;
352
+ if (!this.toolRequestHandler || !payload?.id)
353
+ return;
354
+ void this.toolRequestHandler(payload)
355
+ .then((result) => {
356
+ return this.toolResponse({
357
+ id: payload.id,
358
+ success: true,
359
+ content: toContentParts(result),
360
+ });
361
+ })
362
+ .catch((err) => this.toolResponse({ id: payload.id, success: false, error: err.message }))
363
+ .catch((sendErr) => {
364
+ this.opts.logger.error('tool_response_failed', {
365
+ toolId: payload.id,
366
+ error: sendErr.message,
367
+ });
368
+ });
369
+ break;
370
+ }
371
+ case "intent": {
372
+ const payload = frame.payload;
373
+ this.intentHandler?.(payload);
374
+ break;
375
+ }
376
+ case "world_enrichment": {
377
+ const payload = frame.payload;
378
+ this.worldEnrichmentHandler?.(payload);
379
+ break;
380
+ }
381
+ case "stream": {
382
+ const payload = frame.payload;
383
+ this.streamHandler?.(payload);
384
+ break;
385
+ }
386
+ case "file_fetch": {
387
+ const reqId = frame.id;
388
+ if (!reqId)
389
+ break;
390
+ const payload = frame.payload;
391
+ void this.handleFileFetch(reqId, payload);
392
+ break;
393
+ }
394
+ case "dir_list": {
395
+ const reqId = frame.id;
396
+ if (!reqId)
397
+ break;
398
+ const payload = frame.payload;
399
+ void this.handleDirList(reqId, payload);
400
+ break;
401
+ }
402
+ case "file_push": {
403
+ const reqId = frame.id;
404
+ if (!reqId)
405
+ break;
406
+ const payload = frame.payload;
407
+ void this.handleFilePush(reqId, payload);
408
+ break;
409
+ }
410
+ case "fs_probe": {
411
+ const reqId = frame.id;
412
+ if (!reqId)
413
+ break;
414
+ const payload = frame.payload;
415
+ void this.handleFsProbe(reqId, payload);
416
+ break;
417
+ }
418
+ default:
419
+ break;
420
+ }
421
+ }
422
+ request(method, params) {
423
+ return new Promise((resolve, reject) => {
424
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
425
+ reject(EndpointError.notConnected());
426
+ return;
427
+ }
428
+ const id = `${method}-${++this.reqId}`;
429
+ this.pending.set(id, { resolve: resolve, reject });
430
+ this.ws.send(JSON.stringify({ type: "req", id, method, params }));
431
+ });
432
+ }
433
+ async acceptDirect(msg) {
434
+ const params = {
435
+ content: msg.content,
436
+ contextTags: msg.contextTags,
437
+ };
438
+ await this.request("endpoint.accept", params);
439
+ }
440
+ flushOfflineBuffer() {
441
+ if (this.offlineBuffer.length === 0)
442
+ return;
443
+ const buffered = this.offlineBuffer.splice(0);
444
+ this.opts.logger.info('flushing_offline_buffer', { count: buffered.length });
445
+ for (const msg of buffered) {
446
+ this.acceptDirect(msg).catch((err) => this.opts.logger.error('replay_failed', { error: err.message }));
447
+ }
448
+ }
449
+ startHeartbeat() {
450
+ this.stopHeartbeat();
451
+ this.heartbeatTimer = setInterval(() => {
452
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
453
+ return;
454
+ this.ws.ping();
455
+ this.pongTimer = setTimeout(() => {
456
+ this.opts.logger.error('pong_timeout', { timeout: this.opts.heartbeatTimeout });
457
+ this.ws?.terminate();
458
+ }, this.opts.heartbeatTimeout);
459
+ }, this.opts.heartbeatInterval);
460
+ }
461
+ stopHeartbeat() {
462
+ if (this.heartbeatTimer) {
463
+ clearInterval(this.heartbeatTimer);
464
+ this.heartbeatTimer = null;
465
+ }
466
+ if (this.pongTimer) {
467
+ clearTimeout(this.pongTimer);
468
+ this.pongTimer = null;
469
+ }
470
+ }
471
+ waitForConnection() {
472
+ if (this.fsm.isConnected)
473
+ return Promise.resolve();
474
+ if (this.fsm.state === 'closing' || this.fsm.state === 'disconnected') {
475
+ return Promise.reject(EndpointError.notConnected());
476
+ }
477
+ return new Promise((resolve, reject) => {
478
+ this.reconnectWaiters.push({ resolve, reject });
479
+ });
480
+ }
481
+ resolveReconnectWaiters() {
482
+ const waiters = this.reconnectWaiters.splice(0);
483
+ for (const w of waiters)
484
+ w.resolve();
485
+ }
486
+ rejectReconnectWaiters(err) {
487
+ const waiters = this.reconnectWaiters.splice(0);
488
+ for (const w of waiters)
489
+ w.reject(err);
490
+ }
491
+ onDisconnect() {
492
+ this.ws = null;
493
+ this.stopHeartbeat();
494
+ this.rejectAllPending(new Error("disconnected"));
495
+ if (this.fsm.state !== 'closing' && this.fsm.state !== 'disconnected') {
496
+ this.fsm.transition('reconnecting', 'unexpected disconnect');
497
+ this.scheduleReconnect();
498
+ }
499
+ }
500
+ rejectAllPending(err) {
501
+ for (const [, req] of this.pending) {
502
+ req.reject(err);
503
+ }
504
+ this.pending.clear();
505
+ }
506
+ async handleFileFetch(reqId, payload) {
507
+ try {
508
+ const localPath = payload.path;
509
+ if (!path.isAbsolute(localPath)) {
510
+ await this.toolResponse({ id: reqId, success: false, error: `path must be absolute, got: "${localPath}"` });
511
+ return;
512
+ }
513
+ if (!fs.existsSync(localPath)) {
514
+ await this.toolResponse({ id: reqId, success: false, error: `file not found: ${localPath}` });
515
+ return;
516
+ }
517
+ const fileBuffer = fs.readFileSync(localPath);
518
+ const fileName = path.basename(localPath);
519
+ const form = new FormData();
520
+ form.append("file", new Blob([fileBuffer]), fileName);
521
+ const res = await fetch(payload.upload_url, {
522
+ method: "POST",
523
+ body: form,
524
+ });
525
+ if (!res.ok) {
526
+ await this.toolResponse({ id: reqId, success: false, error: `upload failed: ${res.status}` });
527
+ return;
528
+ }
529
+ const result = await res.json();
530
+ await this.toolResponse({ id: reqId, success: true, content: jsonTextContent(result) });
531
+ }
532
+ catch (err) {
533
+ const msg = err instanceof Error ? err.message : String(err);
534
+ this.opts.logger.error('file_fetch_error', { error: msg });
535
+ await this.toolResponse({ id: reqId, success: false, error: msg }).catch(() => { });
536
+ }
537
+ }
538
+ async handleDirList(reqId, payload) {
539
+ try {
540
+ const dirPath = payload.path;
541
+ if (!path.isAbsolute(dirPath)) {
542
+ await this.toolResponse({ id: reqId, success: false, error: `path must be absolute, got: "${dirPath}"` });
543
+ return;
544
+ }
545
+ if (!fs.existsSync(dirPath)) {
546
+ await this.toolResponse({ id: reqId, success: false, error: `directory not found: ${dirPath}` });
547
+ return;
548
+ }
549
+ const stat = fs.statSync(dirPath);
550
+ if (!stat.isDirectory()) {
551
+ await this.toolResponse({ id: reqId, success: false, error: `not a directory: ${dirPath}` });
552
+ return;
553
+ }
554
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
555
+ const result = {
556
+ entries: entries
557
+ .filter((e) => e.isFile() || e.isDirectory())
558
+ .map((e) => {
559
+ const fullPath = path.join(dirPath, e.name);
560
+ try {
561
+ const s = fs.statSync(fullPath);
562
+ return {
563
+ name: e.name,
564
+ type: e.isDirectory() ? "dir" : "file",
565
+ size: s.size,
566
+ mtime: s.mtime.toISOString(),
567
+ };
568
+ }
569
+ catch {
570
+ return { name: e.name, type: e.isDirectory() ? "dir" : "file", size: 0 };
571
+ }
572
+ }),
573
+ };
574
+ await this.toolResponse({ id: reqId, success: true, content: jsonTextContent(result) });
575
+ }
576
+ catch (err) {
577
+ const msg = err instanceof Error ? err.message : String(err);
578
+ this.opts.logger.error('dir_list_error', { error: msg });
579
+ await this.toolResponse({ id: reqId, success: false, error: msg }).catch(() => { });
580
+ }
581
+ }
582
+ async handleFilePush(reqId, payload) {
583
+ try {
584
+ const targetPath = payload.target_path;
585
+ if (!path.isAbsolute(targetPath)) {
586
+ await this.toolResponse({ id: reqId, success: false, error: `target_path must be absolute, got: "${targetPath}"` });
587
+ return;
588
+ }
589
+ const dir = path.dirname(targetPath);
590
+ fs.mkdirSync(dir, { recursive: true });
591
+ if (payload.local_source) {
592
+ if (!path.isAbsolute(payload.local_source)) {
593
+ await this.toolResponse({ id: reqId, success: false, error: `local_source must be absolute, got: "${payload.local_source}"` });
594
+ return;
595
+ }
596
+ fs.copyFileSync(payload.local_source, targetPath);
597
+ const stat = fs.statSync(targetPath);
598
+ await this.toolResponse({ id: reqId, success: true, content: jsonTextContent({ size: stat.size }) });
599
+ return;
600
+ }
601
+ if (!payload.url) {
602
+ await this.toolResponse({ id: reqId, success: false, error: "neither url nor local_source provided" });
603
+ return;
604
+ }
605
+ const res = await fetch(payload.url);
606
+ if (!res.ok) {
607
+ await this.toolResponse({ id: reqId, success: false, error: `download failed: ${res.status}` });
608
+ return;
609
+ }
610
+ const buffer = Buffer.from(await res.arrayBuffer());
611
+ fs.writeFileSync(targetPath, buffer);
612
+ await this.toolResponse({ id: reqId, success: true, content: jsonTextContent({ size: buffer.length }) });
613
+ }
614
+ catch (err) {
615
+ const msg = err instanceof Error ? err.message : String(err);
616
+ this.opts.logger.error('file_push_error', { error: msg });
617
+ await this.toolResponse({ id: reqId, success: false, error: msg }).catch(() => { });
618
+ }
619
+ }
620
+ async handleFsProbe(reqId, payload) {
621
+ try {
622
+ if (!path.isAbsolute(payload.path)) {
623
+ await this.toolResponse({ id: reqId, success: true, content: jsonTextContent({ found: false, content: "" }) });
624
+ return;
625
+ }
626
+ if (fs.existsSync(payload.path)) {
627
+ const content = fs.readFileSync(payload.path, "utf-8");
628
+ await this.toolResponse({ id: reqId, success: true, content: jsonTextContent({ found: true, content }) });
629
+ }
630
+ else {
631
+ await this.toolResponse({ id: reqId, success: true, content: jsonTextContent({ found: false, content: "" }) });
632
+ }
633
+ }
634
+ catch (err) {
635
+ const msg = err instanceof Error ? err.message : String(err);
636
+ await this.toolResponse({ id: reqId, success: true, content: jsonTextContent({ found: false, content: "" }) }).catch(() => { });
637
+ this.opts.logger.error('fs_probe_error', { error: msg });
638
+ }
639
+ }
640
+ writeFsProbeFile() {
641
+ try {
642
+ const nonce = crypto.randomUUID();
643
+ const probePath = path.join(os.tmpdir(), `ailo-ep-${this.cfg.endpointId}.probe`);
644
+ fs.writeFileSync(probePath, nonce, "utf-8");
645
+ return { path: probePath, nonce };
646
+ }
647
+ catch (err) {
648
+ this.opts.logger.error('fs_probe_write_failed', { error: String(err) });
649
+ return null;
650
+ }
651
+ }
652
+ scheduleReconnect() {
653
+ if (this.reconnectTimer)
654
+ return;
655
+ const delay = Math.min(this.opts.reconnectBaseDelay * 2 ** this.reconnectAttempt, this.opts.reconnectMaxDelay);
656
+ this.reconnectAttempt++;
657
+ this.opts.logger.info('scheduling_reconnect', { delay, attempt: this.reconnectAttempt });
658
+ this.reconnectTimer = setTimeout(() => {
659
+ this.reconnectTimer = null;
660
+ this.fsm.transition('connecting', 'reconnect attempt');
661
+ this.dial().catch((err) => {
662
+ this.opts.logger.error('reconnect_failed', { error: err.message });
663
+ this.fsm.transition('reconnecting', 'reconnect failed');
664
+ this.scheduleReconnect();
665
+ });
666
+ }, delay);
667
+ }
668
+ }
669
+ //# sourceMappingURL=endpoint-client.js.map