pilotprotocol 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/bin/libpilot.h ADDED
@@ -0,0 +1,158 @@
1
+ /* Code generated by cmd/cgo; DO NOT EDIT. */
2
+
3
+ /* package github.com/TeoSlayer/pilotprotocol/sdk/cgo */
4
+
5
+
6
+ #line 1 "cgo-builtin-export-prolog"
7
+
8
+ #include <stddef.h>
9
+
10
+ #ifndef GO_CGO_EXPORT_PROLOGUE_H
11
+ #define GO_CGO_EXPORT_PROLOGUE_H
12
+
13
+ #ifndef GO_CGO_GOSTRING_TYPEDEF
14
+ typedef struct { const char *p; ptrdiff_t n; } _GoString_;
15
+ extern size_t _GoStringLen(_GoString_ s);
16
+ extern const char *_GoStringPtr(_GoString_ s);
17
+ #endif
18
+
19
+ #endif
20
+
21
+ /* Start of preamble from import "C" comments. */
22
+
23
+
24
+ #line 3 "bindings.go"
25
+
26
+ #include <stdlib.h>
27
+ #include <stdint.h>
28
+
29
+ #line 1 "cgo-generated-wrapper"
30
+
31
+
32
+ /* End of preamble from import "C" comments. */
33
+
34
+
35
+ /* Start of boilerplate cgo prologue. */
36
+ #line 1 "cgo-gcc-export-header-prolog"
37
+
38
+ #ifndef GO_CGO_PROLOGUE_H
39
+ #define GO_CGO_PROLOGUE_H
40
+
41
+ typedef signed char GoInt8;
42
+ typedef unsigned char GoUint8;
43
+ typedef short GoInt16;
44
+ typedef unsigned short GoUint16;
45
+ typedef int GoInt32;
46
+ typedef unsigned int GoUint32;
47
+ typedef long long GoInt64;
48
+ typedef unsigned long long GoUint64;
49
+ typedef GoInt64 GoInt;
50
+ typedef GoUint64 GoUint;
51
+ typedef size_t GoUintptr;
52
+ typedef float GoFloat32;
53
+ typedef double GoFloat64;
54
+ #ifdef _MSC_VER
55
+ #if !defined(__cplusplus) || _MSVC_LANG <= 201402L
56
+ #include <complex.h>
57
+ typedef _Fcomplex GoComplex64;
58
+ typedef _Dcomplex GoComplex128;
59
+ #else
60
+ #include <complex>
61
+ typedef std::complex<float> GoComplex64;
62
+ typedef std::complex<double> GoComplex128;
63
+ #endif
64
+ #else
65
+ typedef float _Complex GoComplex64;
66
+ typedef double _Complex GoComplex128;
67
+ #endif
68
+
69
+ /*
70
+ static assertion to make sure the file is being used on architecture
71
+ at least with matching size of GoInt.
72
+ */
73
+ typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
74
+
75
+ #ifndef GO_CGO_GOSTRING_TYPEDEF
76
+ typedef _GoString_ GoString;
77
+ #endif
78
+ typedef void *GoMap;
79
+ typedef void *GoChan;
80
+ typedef struct { void *t; void *v; } GoInterface;
81
+ typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
82
+
83
+ #endif
84
+
85
+ /* End of boilerplate cgo prologue. */
86
+
87
+ #ifdef __cplusplus
88
+ extern "C" {
89
+ #endif
90
+
91
+ extern void FreeString(char* s);
92
+
93
+ /* Return type for PilotConnect */
94
+ struct PilotConnect_return {
95
+ uint64_t r0;
96
+ char* r1;
97
+ };
98
+ extern struct PilotConnect_return PilotConnect(char* socketPath);
99
+ extern char* PilotClose(uint64_t h);
100
+ extern char* PilotInfo(uint64_t h);
101
+ extern char* PilotHandshake(uint64_t h, uint32_t nodeID, char* justification);
102
+ extern char* PilotApproveHandshake(uint64_t h, uint32_t nodeID);
103
+ extern char* PilotRejectHandshake(uint64_t h, uint32_t nodeID, char* reason);
104
+ extern char* PilotPendingHandshakes(uint64_t h);
105
+ extern char* PilotTrustedPeers(uint64_t h);
106
+ extern char* PilotRevokeTrust(uint64_t h, uint32_t nodeID);
107
+ extern char* PilotResolveHostname(uint64_t h, char* hostname);
108
+ extern char* PilotSetHostname(uint64_t h, char* hostname);
109
+ extern char* PilotSetVisibility(uint64_t h, int public);
110
+ extern char* PilotSetTaskExec(uint64_t h, int enabled);
111
+ extern char* PilotDeregister(uint64_t h);
112
+ extern char* PilotSetTags(uint64_t h, char* tagsJSON);
113
+ extern char* PilotSetWebhook(uint64_t h, char* url);
114
+ extern char* PilotDisconnect(uint64_t h, uint32_t connID);
115
+
116
+ /* Return type for PilotDial */
117
+ struct PilotDial_return {
118
+ uint64_t r0;
119
+ char* r1;
120
+ };
121
+ extern struct PilotDial_return PilotDial(uint64_t h, char* addr);
122
+
123
+ /* Return type for PilotListen */
124
+ struct PilotListen_return {
125
+ uint64_t r0;
126
+ char* r1;
127
+ };
128
+ extern struct PilotListen_return PilotListen(uint64_t h, uint16_t port);
129
+
130
+ /* Return type for PilotListenerAccept */
131
+ struct PilotListenerAccept_return {
132
+ uint64_t r0;
133
+ char* r1;
134
+ };
135
+ extern struct PilotListenerAccept_return PilotListenerAccept(uint64_t lh);
136
+ extern char* PilotListenerClose(uint64_t lh);
137
+
138
+ /* Return type for PilotConnRead */
139
+ struct PilotConnRead_return {
140
+ int r0;
141
+ char* r1;
142
+ char* r2;
143
+ };
144
+ extern struct PilotConnRead_return PilotConnRead(uint64_t ch, int bufSize);
145
+
146
+ /* Return type for PilotConnWrite */
147
+ struct PilotConnWrite_return {
148
+ int r0;
149
+ char* r1;
150
+ };
151
+ extern struct PilotConnWrite_return PilotConnWrite(uint64_t ch, void* data, int dataLen);
152
+ extern char* PilotConnClose(uint64_t ch);
153
+ extern char* PilotSendTo(uint64_t h, char* fullAddr, void* data, int dataLen);
154
+ extern char* PilotRecvFrom(uint64_t h);
155
+
156
+ #ifdef __cplusplus
157
+ }
158
+ #endif
Binary file
Binary file
Binary file
Binary file
package/bin/pilotctl ADDED
Binary file
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runDaemon } from '../dist/cli.js';
3
+ runDaemon();
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runGateway } from '../dist/cli.js';
3
+ runGateway();
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runUpdater } from '../dist/cli.js';
3
+ runUpdater();
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runPilotctl } from '../dist/cli.js';
3
+ runPilotctl();
package/dist/cli.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * CLI wrappers for bundled Pilot Protocol binaries.
3
+ *
4
+ * These functions are used as npm "bin" entry points. Each wrapper:
5
+ * 1. Ensures ~/.pilot/ directory and default config.json exist
6
+ * 2. Locates the bundled Go binary
7
+ * 3. Executes it with all CLI arguments passed through
8
+ *
9
+ * This mirrors the Python SDK's cli.py approach.
10
+ */
11
+ export declare function runPilotctl(): void;
12
+ export declare function runDaemon(): void;
13
+ export declare function runGateway(): void;
14
+ export declare function runUpdater(): void;
package/dist/cli.js ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * CLI wrappers for bundled Pilot Protocol binaries.
3
+ *
4
+ * These functions are used as npm "bin" entry points. Each wrapper:
5
+ * 1. Ensures ~/.pilot/ directory and default config.json exist
6
+ * 2. Locates the bundled Go binary
7
+ * 3. Executes it with all CLI arguments passed through
8
+ *
9
+ * This mirrors the Python SDK's cli.py approach.
10
+ */
11
+ import { execFileSync } from 'node:child_process';
12
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
13
+ import { homedir } from 'node:os';
14
+ import { join, resolve } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ /**
17
+ * Ensure ~/.pilot/ directory and config.json exist.
18
+ * Called before every binary execution to initialize the runtime environment.
19
+ */
20
+ function ensurePilotEnv() {
21
+ const home = homedir();
22
+ const pilotDir = join(home, '.pilot');
23
+ const configFile = join(pilotDir, 'config.json');
24
+ // Create ~/.pilot/ if it doesn't exist
25
+ if (!existsSync(pilotDir)) {
26
+ mkdirSync(pilotDir, { recursive: true });
27
+ }
28
+ // Create default config.json if it doesn't exist
29
+ if (!existsSync(configFile)) {
30
+ const defaultConfig = {
31
+ registry: '34.71.57.205:9000',
32
+ beacon: '34.71.57.205:9001',
33
+ socket: '/tmp/pilot.sock',
34
+ encrypt: true,
35
+ identity: join(pilotDir, 'identity.json'),
36
+ };
37
+ writeFileSync(configFile, JSON.stringify(defaultConfig, null, 2));
38
+ }
39
+ }
40
+ /**
41
+ * Get absolute path to a bundled binary.
42
+ * Searches in the package's bin/ directory (relative to this file's location).
43
+ */
44
+ function getBinaryPath(binaryName) {
45
+ const thisDir = resolve(fileURLToPath(import.meta.url), '..');
46
+ // When compiled: dist/cli.js → look for ../bin/
47
+ const pkgBin = resolve(thisDir, '..', 'bin', binaryName);
48
+ if (existsSync(pkgBin))
49
+ return pkgBin;
50
+ // Development: src/cli.ts → look for ../../bin/ (through sdk/node/)
51
+ const devBin = resolve(thisDir, '..', '..', 'bin', binaryName);
52
+ if (existsSync(devBin))
53
+ return devBin;
54
+ throw new Error(`Binary '${binaryName}' not found.\n` +
55
+ '\n' +
56
+ 'Expected locations:\n' +
57
+ ` - ${pkgBin} (npm package)\n` +
58
+ ` - ${devBin} (development)\n` +
59
+ '\n' +
60
+ 'Build binaries with:\n' +
61
+ ' cd sdk/node && ./scripts/build-binaries.sh\n');
62
+ }
63
+ /**
64
+ * Execute a bundled binary with all CLI arguments passed through.
65
+ * Exits with the same code as the binary.
66
+ */
67
+ function runBinary(binaryName) {
68
+ ensurePilotEnv();
69
+ const binaryPath = getBinaryPath(binaryName);
70
+ const args = process.argv.slice(2);
71
+ try {
72
+ execFileSync(binaryPath, args, {
73
+ stdio: 'inherit',
74
+ env: process.env,
75
+ });
76
+ }
77
+ catch (err) {
78
+ // execFileSync throws on non-zero exit codes
79
+ const exitCode = err.status ?? 1;
80
+ process.exit(exitCode);
81
+ }
82
+ }
83
+ // --- Entry points (one per binary) ---
84
+ export function runPilotctl() {
85
+ runBinary('pilotctl');
86
+ }
87
+ export function runDaemon() {
88
+ runBinary('pilot-daemon');
89
+ }
90
+ export function runGateway() {
91
+ runBinary('pilot-gateway');
92
+ }
93
+ export function runUpdater() {
94
+ runBinary('pilot-updater');
95
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Pilot Protocol Node.js SDK — koffi wrapper around libpilot shared library.
3
+ *
4
+ * This module provides a TypeScript/JavaScript interface to the Pilot Protocol
5
+ * daemon by calling into the Go driver compiled as a C-shared library
6
+ * (.so/.dylib/.dll). The Go library is the *single source of truth*; this
7
+ * wrapper is a thin FFI boundary that marshals arguments and unmarshals JSON.
8
+ *
9
+ * Usage:
10
+ *
11
+ * import { Driver } from 'pilotprotocol';
12
+ *
13
+ * const d = new Driver(); // connects to /tmp/pilot.sock
14
+ * console.log(d.info()); // returns object
15
+ * d.close();
16
+ *
17
+ * Or with explicit resource management:
18
+ *
19
+ * using d = new Driver();
20
+ * console.log(d.info());
21
+ * // auto-closed at end of scope
22
+ */
23
+ import type { PilotLib } from './ffi.js';
24
+ export { PilotError } from './ffi.js';
25
+ export declare const DEFAULT_SOCKET_PATH = "/tmp/pilot.sock";
26
+ /** Override the library instance (for testing). */
27
+ export declare function _setLib(lib: PilotLib | null): void;
28
+ /** Get the current library instance (for testing). */
29
+ export declare function _getLib(): PilotLib | null;
30
+ export declare class Conn {
31
+ private _h;
32
+ private _closed;
33
+ constructor(handle: bigint);
34
+ /** Read up to `size` bytes. Blocks until data arrives. */
35
+ read(size?: number): Buffer;
36
+ /** Write bytes to the connection. Returns bytes written. */
37
+ write(data: Buffer | Uint8Array | string): number;
38
+ /** Close the connection. Idempotent. */
39
+ close(): void;
40
+ /** Support TC39 explicit resource management. */
41
+ [Symbol.dispose](): void;
42
+ }
43
+ export declare class Listener {
44
+ private _h;
45
+ private _closed;
46
+ constructor(handle: bigint);
47
+ /** Block until a new connection arrives and return it. */
48
+ accept(): Conn;
49
+ /** Close the listener. Idempotent. */
50
+ close(): void;
51
+ /** Support TC39 explicit resource management. */
52
+ [Symbol.dispose](): void;
53
+ }
54
+ export declare class Driver {
55
+ private _h;
56
+ private _closed;
57
+ constructor(socketPath?: string);
58
+ /** Disconnect from the daemon. Idempotent. */
59
+ close(): void;
60
+ /** Support TC39 explicit resource management. */
61
+ [Symbol.dispose](): void;
62
+ private _callJSON;
63
+ /** Return the daemon's status information. */
64
+ info(): Record<string, unknown>;
65
+ /** Send a trust handshake request to a remote node. */
66
+ handshake(nodeId: number, justification?: string): Record<string, unknown>;
67
+ /** Approve a pending handshake request. */
68
+ approveHandshake(nodeId: number): Record<string, unknown>;
69
+ /** Reject a pending handshake request. */
70
+ rejectHandshake(nodeId: number, reason?: string): Record<string, unknown>;
71
+ /** Return pending trust handshake requests. */
72
+ pendingHandshakes(): Record<string, unknown>;
73
+ /** Return all trusted peers. */
74
+ trustedPeers(): Record<string, unknown>;
75
+ /** Remove a peer from the trusted set. */
76
+ revokeTrust(nodeId: number): Record<string, unknown>;
77
+ /** Resolve a hostname to node info. */
78
+ resolveHostname(hostname: string): Record<string, unknown>;
79
+ /** Set or clear the daemon's hostname. */
80
+ setHostname(hostname: string): Record<string, unknown>;
81
+ /** Set the daemon's visibility on the registry. */
82
+ setVisibility(isPublic: boolean): Record<string, unknown>;
83
+ /** Enable or disable task execution capability. */
84
+ setTaskExec(enabled: boolean): Record<string, unknown>;
85
+ /** Remove the daemon from the registry. */
86
+ deregister(): Record<string, unknown>;
87
+ /** Set capability tags for this node. */
88
+ setTags(tags: string[]): Record<string, unknown>;
89
+ /** Set or clear the webhook URL. */
90
+ setWebhook(url: string): Record<string, unknown>;
91
+ /** Close a connection by ID (administrative). */
92
+ disconnect(connId: number): void;
93
+ /** Open a stream connection to addr (format: "N:XXXX.YYYY.YYYY:PORT"). */
94
+ dial(addr: string): Conn;
95
+ /** Bind a port and return a Listener that accepts connections. */
96
+ listen(port: number): Listener;
97
+ /** Send an unreliable datagram. addr = "N:XXXX.YYYY.YYYY:PORT". */
98
+ sendTo(addr: string, data: Buffer | Uint8Array): void;
99
+ /** Receive the next incoming datagram (blocks). */
100
+ recvFrom(): Record<string, unknown>;
101
+ /** Resolve a target to a protocol address. Passes through if already an address. */
102
+ private _resolveTarget;
103
+ /**
104
+ * Send a message via the data exchange service (port 1001).
105
+ *
106
+ * @param target - Hostname or protocol address (N:XXXX.YYYY.YYYY)
107
+ * @param data - Message data
108
+ * @param msgType - Message type: "text", "json", or "binary"
109
+ */
110
+ sendMessage(target: string, data: Buffer | string, msgType?: 'text' | 'json' | 'binary'): Record<string, unknown>;
111
+ /**
112
+ * Send a file via the data exchange service (port 1001).
113
+ *
114
+ * @param target - Hostname or protocol address
115
+ * @param filePath - Path to file to send
116
+ */
117
+ sendFile(target: string, filePath: string): Record<string, unknown>;
118
+ /**
119
+ * Publish an event via the event stream service (port 1002).
120
+ *
121
+ * Wire format: [2-byte topic len][topic][4-byte payload len][payload]
122
+ * Protocol: first event = subscribe, subsequent events = publish
123
+ *
124
+ * @param target - Hostname or protocol address of event stream server
125
+ * @param topic - Event topic (e.g., "sensor/temperature")
126
+ * @param data - Event payload
127
+ */
128
+ publishEvent(target: string, topic: string, data: Buffer | string): Record<string, unknown>;
129
+ /**
130
+ * Subscribe to events from the event stream service (port 1002).
131
+ *
132
+ * @param target - Hostname or protocol address
133
+ * @param topic - Topic pattern to subscribe to (use "*" for all)
134
+ * @param callback - Callback function(topic, data) for each event
135
+ * @param timeout - Timeout in seconds (default: 30)
136
+ */
137
+ subscribeEvent(target: string, topic: string, callback: (eventTopic: string, eventData: Buffer) => void, timeout?: number): void;
138
+ /**
139
+ * Submit a task via the task submit service (port 1003).
140
+ *
141
+ * @param target - Hostname or protocol address of task execution server
142
+ * @param taskData - Task specification. Must include 'task_description'.
143
+ */
144
+ submitTask(target: string, taskData: Record<string, unknown>): Record<string, unknown>;
145
+ }
package/dist/client.js ADDED
@@ -0,0 +1,491 @@
1
+ /**
2
+ * Pilot Protocol Node.js SDK — koffi wrapper around libpilot shared library.
3
+ *
4
+ * This module provides a TypeScript/JavaScript interface to the Pilot Protocol
5
+ * daemon by calling into the Go driver compiled as a C-shared library
6
+ * (.so/.dylib/.dll). The Go library is the *single source of truth*; this
7
+ * wrapper is a thin FFI boundary that marshals arguments and unmarshals JSON.
8
+ *
9
+ * Usage:
10
+ *
11
+ * import { Driver } from 'pilotprotocol';
12
+ *
13
+ * const d = new Driver(); // connects to /tmp/pilot.sock
14
+ * console.log(d.info()); // returns object
15
+ * d.close();
16
+ *
17
+ * Or with explicit resource management:
18
+ *
19
+ * using d = new Driver();
20
+ * console.log(d.info());
21
+ * // auto-closed at end of scope
22
+ */
23
+ import { existsSync, readFileSync } from 'node:fs';
24
+ import { basename } from 'node:path';
25
+ import { PilotError, checkErr, loadLibrary, parseJSON, unwrapHandleErr, } from './ffi.js';
26
+ // Re-export PilotError for public API
27
+ export { PilotError } from './ffi.js';
28
+ export const DEFAULT_SOCKET_PATH = '/tmp/pilot.sock';
29
+ // Module-level singleton for the loaded library
30
+ let _lib = null;
31
+ function getLib() {
32
+ if (!_lib) {
33
+ _lib = loadLibrary();
34
+ }
35
+ return _lib;
36
+ }
37
+ /** Override the library instance (for testing). */
38
+ export function _setLib(lib) {
39
+ _lib = lib;
40
+ }
41
+ /** Get the current library instance (for testing). */
42
+ export function _getLib() {
43
+ return _lib;
44
+ }
45
+ // ---------------------------------------------------------------------------
46
+ // Conn — stream connection wrapper
47
+ // ---------------------------------------------------------------------------
48
+ export class Conn {
49
+ _h;
50
+ _closed = false;
51
+ constructor(handle) {
52
+ this._h = handle;
53
+ }
54
+ /** Read up to `size` bytes. Blocks until data arrives. */
55
+ read(size = 4096) {
56
+ if (this._closed)
57
+ throw new PilotError('connection closed');
58
+ if (size <= 0)
59
+ return Buffer.alloc(0);
60
+ if (size > 16 * 1024 * 1024)
61
+ size = 16 * 1024 * 1024; // cap at 16MB
62
+ const lib = getLib();
63
+ const res = lib.PilotConnRead(this._h, size);
64
+ if (res.err) {
65
+ const obj = JSON.parse(res.err);
66
+ throw new PilotError(obj.error ?? 'read error');
67
+ }
68
+ if (res.n === 0 || !res.data)
69
+ return Buffer.alloc(0);
70
+ return res.data;
71
+ }
72
+ /** Write bytes to the connection. Returns bytes written. */
73
+ write(data) {
74
+ if (this._closed)
75
+ throw new PilotError('connection closed');
76
+ const lib = getLib();
77
+ // Allocate a dedicated Buffer to avoid shared-pool byteOffset issues
78
+ const src = typeof data === 'string' ? Buffer.from(data) : data;
79
+ const buf = Buffer.allocUnsafe(src.length);
80
+ Buffer.from(src).copy(buf);
81
+ const res = lib.PilotConnWrite(this._h, buf, buf.length);
82
+ if (res.err) {
83
+ const obj = JSON.parse(res.err);
84
+ throw new PilotError(obj.error ?? 'write error');
85
+ }
86
+ return res.n;
87
+ }
88
+ /** Close the connection. Idempotent. */
89
+ close() {
90
+ if (this._closed)
91
+ return;
92
+ this._closed = true;
93
+ const lib = getLib();
94
+ const ptr = lib.PilotConnClose(this._h);
95
+ checkErr(ptr);
96
+ }
97
+ /** Support TC39 explicit resource management. */
98
+ [Symbol.dispose]() {
99
+ this.close();
100
+ }
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // Listener — server socket wrapper
104
+ // ---------------------------------------------------------------------------
105
+ export class Listener {
106
+ _h;
107
+ _closed = false;
108
+ constructor(handle) {
109
+ this._h = handle;
110
+ }
111
+ /** Block until a new connection arrives and return it. */
112
+ accept() {
113
+ if (this._closed)
114
+ throw new PilotError('listener closed');
115
+ const lib = getLib();
116
+ const res = lib.PilotListenerAccept(this._h);
117
+ const handle = unwrapHandleErr(res);
118
+ return new Conn(handle);
119
+ }
120
+ /** Close the listener. Idempotent. */
121
+ close() {
122
+ if (this._closed)
123
+ return;
124
+ this._closed = true;
125
+ const lib = getLib();
126
+ const ptr = lib.PilotListenerClose(this._h);
127
+ checkErr(ptr);
128
+ }
129
+ /** Support TC39 explicit resource management. */
130
+ [Symbol.dispose]() {
131
+ this.close();
132
+ }
133
+ }
134
+ // ---------------------------------------------------------------------------
135
+ // Driver — main SDK entry point
136
+ // ---------------------------------------------------------------------------
137
+ export class Driver {
138
+ _h;
139
+ _closed = false;
140
+ constructor(socketPath = DEFAULT_SOCKET_PATH) {
141
+ const lib = getLib();
142
+ const res = lib.PilotConnect(socketPath);
143
+ this._h = unwrapHandleErr(res);
144
+ }
145
+ /** Disconnect from the daemon. Idempotent. */
146
+ close() {
147
+ if (this._closed)
148
+ return;
149
+ this._closed = true;
150
+ const lib = getLib();
151
+ const ptr = lib.PilotClose(this._h);
152
+ checkErr(ptr);
153
+ }
154
+ /** Support TC39 explicit resource management. */
155
+ [Symbol.dispose]() {
156
+ this.close();
157
+ }
158
+ // -- JSON-RPC helper --
159
+ _callJSON(fnName, ...args) {
160
+ const lib = getLib();
161
+ const fn = lib[fnName];
162
+ const str = fn(this._h, ...args);
163
+ return parseJSON(str);
164
+ }
165
+ // -- Info --
166
+ /** Return the daemon's status information. */
167
+ info() {
168
+ return this._callJSON('PilotInfo');
169
+ }
170
+ // -- Handshake / Trust --
171
+ /** Send a trust handshake request to a remote node. */
172
+ handshake(nodeId, justification = '') {
173
+ return this._callJSON('PilotHandshake', nodeId, justification);
174
+ }
175
+ /** Approve a pending handshake request. */
176
+ approveHandshake(nodeId) {
177
+ return this._callJSON('PilotApproveHandshake', nodeId);
178
+ }
179
+ /** Reject a pending handshake request. */
180
+ rejectHandshake(nodeId, reason = '') {
181
+ return this._callJSON('PilotRejectHandshake', nodeId, reason);
182
+ }
183
+ /** Return pending trust handshake requests. */
184
+ pendingHandshakes() {
185
+ return this._callJSON('PilotPendingHandshakes');
186
+ }
187
+ /** Return all trusted peers. */
188
+ trustedPeers() {
189
+ return this._callJSON('PilotTrustedPeers');
190
+ }
191
+ /** Remove a peer from the trusted set. */
192
+ revokeTrust(nodeId) {
193
+ return this._callJSON('PilotRevokeTrust', nodeId);
194
+ }
195
+ // -- Hostname --
196
+ /** Resolve a hostname to node info. */
197
+ resolveHostname(hostname) {
198
+ return this._callJSON('PilotResolveHostname', hostname);
199
+ }
200
+ /** Set or clear the daemon's hostname. */
201
+ setHostname(hostname) {
202
+ return this._callJSON('PilotSetHostname', hostname);
203
+ }
204
+ // -- Visibility / capabilities --
205
+ /** Set the daemon's visibility on the registry. */
206
+ setVisibility(isPublic) {
207
+ return this._callJSON('PilotSetVisibility', isPublic ? 1 : 0);
208
+ }
209
+ /** Enable or disable task execution capability. */
210
+ setTaskExec(enabled) {
211
+ return this._callJSON('PilotSetTaskExec', enabled ? 1 : 0);
212
+ }
213
+ /** Remove the daemon from the registry. */
214
+ deregister() {
215
+ return this._callJSON('PilotDeregister');
216
+ }
217
+ /** Set capability tags for this node. */
218
+ setTags(tags) {
219
+ return this._callJSON('PilotSetTags', JSON.stringify(tags));
220
+ }
221
+ /** Set or clear the webhook URL. */
222
+ setWebhook(url) {
223
+ return this._callJSON('PilotSetWebhook', url);
224
+ }
225
+ // -- Connection management --
226
+ /** Close a connection by ID (administrative). */
227
+ disconnect(connId) {
228
+ const lib = getLib();
229
+ const ptr = lib.PilotDisconnect(this._h, connId);
230
+ checkErr(ptr);
231
+ }
232
+ // -- Streams --
233
+ /** Open a stream connection to addr (format: "N:XXXX.YYYY.YYYY:PORT"). */
234
+ dial(addr) {
235
+ const lib = getLib();
236
+ const res = lib.PilotDial(this._h, addr);
237
+ const handle = unwrapHandleErr(res);
238
+ return new Conn(handle);
239
+ }
240
+ /** Bind a port and return a Listener that accepts connections. */
241
+ listen(port) {
242
+ const lib = getLib();
243
+ const res = lib.PilotListen(this._h, port);
244
+ const handle = unwrapHandleErr(res);
245
+ return new Listener(handle);
246
+ }
247
+ // -- Datagrams --
248
+ /** Send an unreliable datagram. addr = "N:XXXX.YYYY.YYYY:PORT". */
249
+ sendTo(addr, data) {
250
+ const lib = getLib();
251
+ // Allocate a dedicated Buffer to avoid shared-pool byteOffset issues
252
+ const buf = Buffer.allocUnsafe(data.length);
253
+ Buffer.from(data).copy(buf);
254
+ const ptr = lib.PilotSendTo(this._h, addr, buf, buf.length);
255
+ checkErr(ptr);
256
+ }
257
+ /** Receive the next incoming datagram (blocks). */
258
+ recvFrom() {
259
+ return this._callJSON('PilotRecvFrom');
260
+ }
261
+ // -- High-level service methods --
262
+ /** Resolve a target to a protocol address. Passes through if already an address. */
263
+ _resolveTarget(target) {
264
+ if (!target.startsWith('0:')) {
265
+ const result = this.resolveHostname(target);
266
+ const addr = result['address'];
267
+ if (!addr)
268
+ throw new PilotError(`Could not resolve hostname: ${target}`);
269
+ return addr;
270
+ }
271
+ return target;
272
+ }
273
+ /**
274
+ * Send a message via the data exchange service (port 1001).
275
+ *
276
+ * @param target - Hostname or protocol address (N:XXXX.YYYY.YYYY)
277
+ * @param data - Message data
278
+ * @param msgType - Message type: "text", "json", or "binary"
279
+ */
280
+ sendMessage(target, data, msgType = 'text') {
281
+ const buf = typeof data === 'string' ? Buffer.from(data) : Buffer.from(data);
282
+ const addr = this._resolveTarget(target);
283
+ // Map type to frame type: 1=text, 2=binary, 3=json, 4=file
284
+ const typeMap = { text: 1, binary: 2, json: 3, file: 4 };
285
+ const frameType = typeMap[msgType] ?? 1;
286
+ // Build frame: [4-byte type][4-byte length][payload]
287
+ const header = Buffer.alloc(8);
288
+ header.writeUInt32BE(frameType, 0);
289
+ header.writeUInt32BE(buf.length, 4);
290
+ const frame = Buffer.concat([header, buf]);
291
+ const conn = this.dial(`${addr}:1001`);
292
+ try {
293
+ conn.write(frame);
294
+ try {
295
+ const ackHeader = conn.read(8);
296
+ if (ackHeader && ackHeader.length === 8) {
297
+ const ackLen = ackHeader.readUInt32BE(4);
298
+ const ackPayload = conn.read(ackLen);
299
+ if (ackPayload && ackPayload.length > 0) {
300
+ const ackMsg = ackPayload.toString('utf-8');
301
+ return { sent: buf.length, type: msgType, target: addr, ack: ackMsg };
302
+ }
303
+ }
304
+ }
305
+ catch {
306
+ // ACK read failed, but message was sent
307
+ }
308
+ return { sent: buf.length, type: msgType, target: addr };
309
+ }
310
+ finally {
311
+ conn.close();
312
+ }
313
+ }
314
+ /**
315
+ * Send a file via the data exchange service (port 1001).
316
+ *
317
+ * @param target - Hostname or protocol address
318
+ * @param filePath - Path to file to send
319
+ */
320
+ sendFile(target, filePath) {
321
+ if (!existsSync(filePath)) {
322
+ throw new PilotError(`File not found: ${filePath}`);
323
+ }
324
+ const fileData = readFileSync(filePath);
325
+ const filename = basename(filePath);
326
+ const filenameBytes = Buffer.from(filename, 'utf-8');
327
+ // For TypeFile: payload = [2-byte name len][name][file data]
328
+ const nameHeader = Buffer.alloc(2);
329
+ nameHeader.writeUInt16BE(filenameBytes.length, 0);
330
+ const payload = Buffer.concat([nameHeader, filenameBytes, fileData]);
331
+ // Build frame: [4-byte type=4][4-byte length][payload]
332
+ const header = Buffer.alloc(8);
333
+ header.writeUInt32BE(4, 0);
334
+ header.writeUInt32BE(payload.length, 4);
335
+ const frame = Buffer.concat([header, payload]);
336
+ const addr = this._resolveTarget(target);
337
+ const conn = this.dial(`${addr}:1001`);
338
+ try {
339
+ conn.write(frame);
340
+ try {
341
+ const ackHeader = conn.read(8);
342
+ if (ackHeader && ackHeader.length === 8) {
343
+ const ackLen = ackHeader.readUInt32BE(4);
344
+ const ackPayload = conn.read(ackLen);
345
+ if (ackPayload && ackPayload.length > 0) {
346
+ const ackMsg = ackPayload.toString('utf-8');
347
+ return { sent: fileData.length, filename, target: addr, ack: ackMsg };
348
+ }
349
+ }
350
+ }
351
+ catch {
352
+ // ACK read failed, but file was sent
353
+ }
354
+ return { sent: fileData.length, filename, target: addr };
355
+ }
356
+ finally {
357
+ conn.close();
358
+ }
359
+ }
360
+ /**
361
+ * Publish an event via the event stream service (port 1002).
362
+ *
363
+ * Wire format: [2-byte topic len][topic][4-byte payload len][payload]
364
+ * Protocol: first event = subscribe, subsequent events = publish
365
+ *
366
+ * @param target - Hostname or protocol address of event stream server
367
+ * @param topic - Event topic (e.g., "sensor/temperature")
368
+ * @param data - Event payload
369
+ */
370
+ publishEvent(target, topic, data) {
371
+ const addr = this._resolveTarget(target);
372
+ const payload = typeof data === 'string' ? Buffer.from(data) : Buffer.from(data);
373
+ const conn = this.dial(`${addr}:1002`);
374
+ try {
375
+ // Subscribe to topic first (empty payload)
376
+ conn.write(buildEventFrame(topic, Buffer.alloc(0)));
377
+ // Now publish the actual event
378
+ conn.write(buildEventFrame(topic, payload));
379
+ return { status: 'published', topic, bytes: payload.length };
380
+ }
381
+ finally {
382
+ conn.close();
383
+ }
384
+ }
385
+ /**
386
+ * Subscribe to events from the event stream service (port 1002).
387
+ *
388
+ * @param target - Hostname or protocol address
389
+ * @param topic - Topic pattern to subscribe to (use "*" for all)
390
+ * @param callback - Callback function(topic, data) for each event
391
+ * @param timeout - Timeout in seconds (default: 30)
392
+ */
393
+ subscribeEvent(target, topic, callback, timeout = 30) {
394
+ const addr = this._resolveTarget(target);
395
+ const conn = this.dial(`${addr}:1002`);
396
+ try {
397
+ // Send subscription (empty payload)
398
+ conn.write(buildEventFrame(topic, Buffer.alloc(0)));
399
+ const deadline = Date.now() + timeout * 1000;
400
+ while (Date.now() < deadline) {
401
+ try {
402
+ const event = readEventFrame(conn);
403
+ if (!event)
404
+ break;
405
+ callback(event.topic, event.data);
406
+ }
407
+ catch (e) {
408
+ const msg = String(e);
409
+ if (msg.includes('connection closed') || msg.includes('EOF'))
410
+ break;
411
+ throw e;
412
+ }
413
+ }
414
+ }
415
+ finally {
416
+ conn.close();
417
+ }
418
+ }
419
+ /**
420
+ * Submit a task via the task submit service (port 1003).
421
+ *
422
+ * @param target - Hostname or protocol address of task execution server
423
+ * @param taskData - Task specification. Must include 'task_description'.
424
+ */
425
+ submitTask(target, taskData) {
426
+ const addr = this._resolveTarget(target);
427
+ const nodeInfo = this.info();
428
+ const fromAddr = nodeInfo['address'] ?? 'unknown';
429
+ const submitReq = {
430
+ task_id: taskData['task_id'] ?? crypto.randomUUID(),
431
+ task_description: taskData['task_description'] ?? JSON.stringify(taskData),
432
+ from_addr: fromAddr,
433
+ to_addr: addr,
434
+ };
435
+ const taskJson = Buffer.from(JSON.stringify(submitReq), 'utf-8');
436
+ // Build submit frame: [4-byte type=1][4-byte length][JSON payload]
437
+ const header = Buffer.alloc(8);
438
+ header.writeUInt32BE(1, 0);
439
+ header.writeUInt32BE(taskJson.length, 4);
440
+ const frame = Buffer.concat([header, taskJson]);
441
+ const conn = this.dial(`${addr}:1003`);
442
+ try {
443
+ conn.write(frame);
444
+ // Read response frame: [4-byte type][4-byte length][JSON payload]
445
+ const respHeader = conn.read(8);
446
+ if (!respHeader || respHeader.length < 8) {
447
+ throw new PilotError('No response from task submit service');
448
+ }
449
+ const respLen = respHeader.readUInt32BE(4);
450
+ const respData = conn.read(respLen);
451
+ if (!respData || respData.length < respLen) {
452
+ throw new PilotError('Incomplete response from task submit service');
453
+ }
454
+ return JSON.parse(respData.toString('utf-8'));
455
+ }
456
+ finally {
457
+ conn.close();
458
+ }
459
+ }
460
+ }
461
+ // ---------------------------------------------------------------------------
462
+ // Event stream helpers
463
+ // ---------------------------------------------------------------------------
464
+ /** Build an event frame: [2-byte topic len][topic][4-byte payload len][payload]. */
465
+ function buildEventFrame(topic, payload) {
466
+ const topicBytes = Buffer.from(topic, 'utf-8');
467
+ const header = Buffer.alloc(2 + topicBytes.length + 4);
468
+ header.writeUInt16BE(topicBytes.length, 0);
469
+ topicBytes.copy(header, 2);
470
+ header.writeUInt32BE(payload.length, 2 + topicBytes.length);
471
+ return Buffer.concat([header, payload]);
472
+ }
473
+ /** Read an event frame from a connection. Returns null on incomplete read. */
474
+ function readEventFrame(conn) {
475
+ const topicLenBuf = conn.read(2);
476
+ if (!topicLenBuf || topicLenBuf.length < 2)
477
+ return null;
478
+ const topicLen = topicLenBuf.readUInt16BE(0);
479
+ const topicBuf = conn.read(topicLen);
480
+ if (!topicBuf || topicBuf.length < topicLen)
481
+ return null;
482
+ const topic = topicBuf.toString('utf-8');
483
+ const payloadLenBuf = conn.read(4);
484
+ if (!payloadLenBuf || payloadLenBuf.length < 4)
485
+ return null;
486
+ const payloadLen = payloadLenBuf.readUInt32BE(0);
487
+ const data = conn.read(payloadLen);
488
+ if (!data || data.length < payloadLen)
489
+ return null;
490
+ return { topic, data };
491
+ }
package/dist/ffi.d.ts ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * FFI binding layer — loads libpilot and declares all C function signatures.
3
+ *
4
+ * Uses koffi (pure-JS FFI, no native compilation) to call into the Go
5
+ * shared library that is the single source of truth for the protocol.
6
+ *
7
+ * IMPORTANT: All char* returns from Go are allocated with C.CString (malloc).
8
+ * We MUST call FreeString on every returned pointer to avoid memory leaks.
9
+ * To achieve this, loadLibrary() returns wrapper functions that:
10
+ * 1. Declare return types as 'void *' (not 'char *') to get raw pointers
11
+ * 2. Decode the string with koffi.decode()
12
+ * 3. Free the pointer with FreeString()
13
+ * 4. Return clean JS types (string | null, Buffer)
14
+ *
15
+ * This mirrors the Python SDK's approach of using c_void_p instead of c_char_p
16
+ * (see Python client.py lines 122-126).
17
+ */
18
+ export declare class PilotError extends Error {
19
+ constructor(message: string);
20
+ }
21
+ export declare function findLibrary(): string;
22
+ export interface PilotLib {
23
+ PilotConnect(socketPath: string): {
24
+ handle: bigint;
25
+ err: string | null;
26
+ };
27
+ PilotClose(h: bigint): string | null;
28
+ PilotInfo(h: bigint): string | null;
29
+ PilotHandshake(h: bigint, nodeId: number, justification: string): string | null;
30
+ PilotApproveHandshake(h: bigint, nodeId: number): string | null;
31
+ PilotRejectHandshake(h: bigint, nodeId: number, reason: string): string | null;
32
+ PilotPendingHandshakes(h: bigint): string | null;
33
+ PilotTrustedPeers(h: bigint): string | null;
34
+ PilotRevokeTrust(h: bigint, nodeId: number): string | null;
35
+ PilotResolveHostname(h: bigint, hostname: string): string | null;
36
+ PilotSetHostname(h: bigint, hostname: string): string | null;
37
+ PilotSetVisibility(h: bigint, public_: number): string | null;
38
+ PilotSetTaskExec(h: bigint, enabled: number): string | null;
39
+ PilotDeregister(h: bigint): string | null;
40
+ PilotSetTags(h: bigint, tagsJson: string): string | null;
41
+ PilotSetWebhook(h: bigint, url: string): string | null;
42
+ PilotDisconnect(h: bigint, connId: number): string | null;
43
+ PilotRecvFrom(h: bigint): string | null;
44
+ PilotDial(h: bigint, addr: string): {
45
+ handle: bigint;
46
+ err: string | null;
47
+ };
48
+ PilotListen(h: bigint, port: number): {
49
+ handle: bigint;
50
+ err: string | null;
51
+ };
52
+ PilotListenerAccept(h: bigint): {
53
+ handle: bigint;
54
+ err: string | null;
55
+ };
56
+ PilotListenerClose(h: bigint): string | null;
57
+ PilotConnRead(h: bigint, bufSize: number): {
58
+ n: number;
59
+ data: Buffer | null;
60
+ err: string | null;
61
+ };
62
+ PilotConnWrite(h: bigint, data: Buffer, dataLen: number): {
63
+ n: number;
64
+ err: string | null;
65
+ };
66
+ PilotConnClose(h: bigint): string | null;
67
+ PilotSendTo(h: bigint, addr: string, data: Buffer, dataLen: number): string | null;
68
+ }
69
+ export declare function loadLibrary(path?: string): PilotLib;
70
+ /** Parse a JSON string return. Raises PilotError if it contains {"error": ...}. */
71
+ export declare function parseJSON(str: string | null): Record<string, unknown>;
72
+ /** Check a string error result. Raises PilotError if non-null. */
73
+ export declare function checkErr(str: string | null): void;
74
+ /** Check HandleErr result and throw if err is set. Returns handle. */
75
+ export declare function unwrapHandleErr(res: {
76
+ handle: bigint;
77
+ err: string | null;
78
+ }): bigint;
package/dist/ffi.js ADDED
@@ -0,0 +1,228 @@
1
+ /**
2
+ * FFI binding layer — loads libpilot and declares all C function signatures.
3
+ *
4
+ * Uses koffi (pure-JS FFI, no native compilation) to call into the Go
5
+ * shared library that is the single source of truth for the protocol.
6
+ *
7
+ * IMPORTANT: All char* returns from Go are allocated with C.CString (malloc).
8
+ * We MUST call FreeString on every returned pointer to avoid memory leaks.
9
+ * To achieve this, loadLibrary() returns wrapper functions that:
10
+ * 1. Declare return types as 'void *' (not 'char *') to get raw pointers
11
+ * 2. Decode the string with koffi.decode()
12
+ * 3. Free the pointer with FreeString()
13
+ * 4. Return clean JS types (string | null, Buffer)
14
+ *
15
+ * This mirrors the Python SDK's approach of using c_void_p instead of c_char_p
16
+ * (see Python client.py lines 122-126).
17
+ */
18
+ import koffi from 'koffi';
19
+ import { existsSync } from 'node:fs';
20
+ import { homedir, platform } from 'node:os';
21
+ import { join, resolve } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ // ---------------------------------------------------------------------------
24
+ // Error class (defined here to avoid circular deps with client.ts)
25
+ // ---------------------------------------------------------------------------
26
+ export class PilotError extends Error {
27
+ constructor(message) {
28
+ super(message);
29
+ this.name = 'PilotError';
30
+ }
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Library discovery
34
+ // ---------------------------------------------------------------------------
35
+ const LIB_NAMES = {
36
+ darwin: 'libpilot.dylib',
37
+ linux: 'libpilot.so',
38
+ win32: 'libpilot.dll',
39
+ };
40
+ export function findLibrary() {
41
+ const libName = LIB_NAMES[platform()];
42
+ if (!libName) {
43
+ throw new Error(`unsupported platform: ${platform()}`);
44
+ }
45
+ // 1. PILOT_LIB_PATH env var
46
+ const envPath = process.env['PILOT_LIB_PATH'];
47
+ if (envPath) {
48
+ if (existsSync(envPath))
49
+ return envPath;
50
+ throw new Error(`PILOT_LIB_PATH=${envPath} does not exist`);
51
+ }
52
+ // 2. ~/.pilot/bin/
53
+ const pilotBin = join(homedir(), '.pilot', 'bin', libName);
54
+ if (existsSync(pilotBin))
55
+ return pilotBin;
56
+ // 3. <package>/bin/ (npm package layout: dist/ffi.js → ../bin/)
57
+ const thisDir = resolve(fileURLToPath(import.meta.url), '..');
58
+ const pkgBin = resolve(thisDir, '..', 'bin', libName);
59
+ if (existsSync(pkgBin))
60
+ return pkgBin;
61
+ // 4. Same directory as this file
62
+ const colocated = join(thisDir, libName);
63
+ if (existsSync(colocated))
64
+ return colocated;
65
+ // 5. <repo>/bin/ (development layout — 3 levels up from dist/)
66
+ const repoBin = resolve(thisDir, '..', '..', '..', 'bin', libName);
67
+ if (existsSync(repoBin))
68
+ return repoBin;
69
+ throw new Error(`Cannot find ${libName}.\n` +
70
+ '\n' +
71
+ 'Expected locations:\n' +
72
+ ` - ~/.pilot/bin/${libName}\n` +
73
+ ` - ${pkgBin} (npm package)\n` +
74
+ ` - ${colocated} (colocated)\n` +
75
+ ` - ${repoBin} (development)\n` +
76
+ '\n' +
77
+ 'Build it with:\n' +
78
+ ' cd sdk/node && ./scripts/build-binaries.sh\n' +
79
+ '\n' +
80
+ 'Or set PILOT_LIB_PATH:\n' +
81
+ ` export PILOT_LIB_PATH=/path/to/${libName}`);
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // Library loading with memory-safe wrappers
85
+ // ---------------------------------------------------------------------------
86
+ /**
87
+ * Struct definitions use 'void *' for all char* fields to preserve raw
88
+ * pointers. koffi's 'char *' auto-decodes to JS string and discards the
89
+ * pointer — making it impossible to call FreeString. Using 'void *' gives
90
+ * us the raw pointer so we can decode + free explicitly.
91
+ */
92
+ const HandleErrStruct = koffi.struct('HandleErr', {
93
+ handle: 'uint64',
94
+ err: 'void *',
95
+ });
96
+ const ReadResultStruct = koffi.struct('ReadResult', {
97
+ n: 'int',
98
+ data: 'void *',
99
+ err: 'void *',
100
+ });
101
+ const WriteResultStruct = koffi.struct('WriteResult', {
102
+ n: 'int',
103
+ err: 'void *',
104
+ });
105
+ export function loadLibrary(path) {
106
+ const libPath = path ?? findLibrary();
107
+ const lib = koffi.load(libPath);
108
+ // Raw FFI declarations — all char* returns use 'void *'
109
+ const rawFree = lib.func('FreeString', 'void', ['void *']);
110
+ const rawConnect = lib.func('PilotConnect', HandleErrStruct, ['str']);
111
+ const rawClose = lib.func('PilotClose', 'void *', ['uint64']);
112
+ const rawInfo = lib.func('PilotInfo', 'void *', ['uint64']);
113
+ const rawHandshake = lib.func('PilotHandshake', 'void *', ['uint64', 'uint32', 'str']);
114
+ const rawApproveHandshake = lib.func('PilotApproveHandshake', 'void *', ['uint64', 'uint32']);
115
+ const rawRejectHandshake = lib.func('PilotRejectHandshake', 'void *', ['uint64', 'uint32', 'str']);
116
+ const rawPendingHandshakes = lib.func('PilotPendingHandshakes', 'void *', ['uint64']);
117
+ const rawTrustedPeers = lib.func('PilotTrustedPeers', 'void *', ['uint64']);
118
+ const rawRevokeTrust = lib.func('PilotRevokeTrust', 'void *', ['uint64', 'uint32']);
119
+ const rawResolveHostname = lib.func('PilotResolveHostname', 'void *', ['uint64', 'str']);
120
+ const rawSetHostname = lib.func('PilotSetHostname', 'void *', ['uint64', 'str']);
121
+ const rawSetVisibility = lib.func('PilotSetVisibility', 'void *', ['uint64', 'int']);
122
+ const rawSetTaskExec = lib.func('PilotSetTaskExec', 'void *', ['uint64', 'int']);
123
+ const rawDeregister = lib.func('PilotDeregister', 'void *', ['uint64']);
124
+ const rawSetTags = lib.func('PilotSetTags', 'void *', ['uint64', 'str']);
125
+ const rawSetWebhook = lib.func('PilotSetWebhook', 'void *', ['uint64', 'str']);
126
+ const rawDisconnect = lib.func('PilotDisconnect', 'void *', ['uint64', 'uint32']);
127
+ const rawRecvFrom = lib.func('PilotRecvFrom', 'void *', ['uint64']);
128
+ const rawDial = lib.func('PilotDial', HandleErrStruct, ['uint64', 'str']);
129
+ const rawListen = lib.func('PilotListen', HandleErrStruct, ['uint64', 'uint16']);
130
+ const rawListenerAccept = lib.func('PilotListenerAccept', HandleErrStruct, ['uint64']);
131
+ const rawListenerClose = lib.func('PilotListenerClose', 'void *', ['uint64']);
132
+ const rawConnRead = lib.func('PilotConnRead', ReadResultStruct, ['uint64', 'int']);
133
+ const rawConnWrite = lib.func('PilotConnWrite', WriteResultStruct, ['uint64', 'void *', 'int']);
134
+ const rawConnClose = lib.func('PilotConnClose', 'void *', ['uint64']);
135
+ const rawSendTo = lib.func('PilotSendTo', 'void *', ['uint64', 'str', 'void *', 'int']);
136
+ /** Decode a void* C string, free the pointer, return JS string. */
137
+ function decodeAndFree(ptr) {
138
+ if (!ptr)
139
+ return null;
140
+ const str = koffi.decode(ptr, 'char', -1);
141
+ rawFree(ptr);
142
+ return str;
143
+ }
144
+ /** Unwrap a HandleErr struct: decode+free err, return clean result. */
145
+ function unwrapHandle(res) {
146
+ return { handle: res.handle, err: decodeAndFree(res.err) };
147
+ }
148
+ /** Wrap a raw FFI function that returns void* (JSON char*). */
149
+ function wrapJSON(fn) {
150
+ return (...args) => decodeAndFree(fn(...args));
151
+ }
152
+ return {
153
+ PilotConnect: (socketPath) => unwrapHandle(rawConnect(socketPath)),
154
+ PilotClose: (h) => decodeAndFree(rawClose(h)),
155
+ PilotInfo: wrapJSON(rawInfo),
156
+ PilotHandshake: wrapJSON(rawHandshake),
157
+ PilotApproveHandshake: wrapJSON(rawApproveHandshake),
158
+ PilotRejectHandshake: wrapJSON(rawRejectHandshake),
159
+ PilotPendingHandshakes: wrapJSON(rawPendingHandshakes),
160
+ PilotTrustedPeers: wrapJSON(rawTrustedPeers),
161
+ PilotRevokeTrust: wrapJSON(rawRevokeTrust),
162
+ PilotResolveHostname: wrapJSON(rawResolveHostname),
163
+ PilotSetHostname: wrapJSON(rawSetHostname),
164
+ PilotSetVisibility: wrapJSON(rawSetVisibility),
165
+ PilotSetTaskExec: wrapJSON(rawSetTaskExec),
166
+ PilotDeregister: wrapJSON(rawDeregister),
167
+ PilotSetTags: wrapJSON(rawSetTags),
168
+ PilotSetWebhook: wrapJSON(rawSetWebhook),
169
+ PilotDisconnect: wrapJSON(rawDisconnect),
170
+ PilotRecvFrom: wrapJSON(rawRecvFrom),
171
+ PilotDial: (h, addr) => unwrapHandle(rawDial(h, addr)),
172
+ PilotListen: (h, port) => unwrapHandle(rawListen(h, port)),
173
+ PilotListenerAccept: (h) => unwrapHandle(rawListenerAccept(h)),
174
+ PilotListenerClose: (h) => decodeAndFree(rawListenerClose(h)),
175
+ PilotConnRead(h, bufSize) {
176
+ const res = rawConnRead(h, bufSize);
177
+ const err = decodeAndFree(res.err);
178
+ let data = null;
179
+ if (res.data && res.n > 0) {
180
+ // Decode n bytes from the C.CBytes-allocated pointer into a Buffer
181
+ const bytes = koffi.decode(res.data, 'uint8', res.n);
182
+ data = Buffer.from(bytes);
183
+ rawFree(res.data); // Free the C.CBytes allocation
184
+ }
185
+ return { n: res.n, data, err };
186
+ },
187
+ PilotConnWrite(h, buf, dataLen) {
188
+ // Pass Buffer directly — koffi handles byteOffset correctly for void*
189
+ const res = rawConnWrite(h, buf, dataLen);
190
+ return { n: res.n, err: decodeAndFree(res.err) };
191
+ },
192
+ PilotConnClose: (h) => decodeAndFree(rawConnClose(h)),
193
+ PilotSendTo(h, addr, buf, dataLen) {
194
+ // Pass Buffer directly — koffi handles byteOffset correctly for void*
195
+ return decodeAndFree(rawSendTo(h, addr, buf, dataLen));
196
+ },
197
+ };
198
+ }
199
+ // ---------------------------------------------------------------------------
200
+ // Helpers (operate on clean JS types, not raw pointers)
201
+ // ---------------------------------------------------------------------------
202
+ /** Parse a JSON string return. Raises PilotError if it contains {"error": ...}. */
203
+ export function parseJSON(str) {
204
+ if (!str)
205
+ return {};
206
+ const obj = JSON.parse(str);
207
+ if (obj.error) {
208
+ throw new PilotError(obj.error);
209
+ }
210
+ return obj;
211
+ }
212
+ /** Check a string error result. Raises PilotError if non-null. */
213
+ export function checkErr(str) {
214
+ if (!str)
215
+ return;
216
+ const obj = JSON.parse(str);
217
+ if (obj.error) {
218
+ throw new PilotError(obj.error);
219
+ }
220
+ }
221
+ /** Check HandleErr result and throw if err is set. Returns handle. */
222
+ export function unwrapHandleErr(res) {
223
+ if (res.err) {
224
+ const obj = JSON.parse(res.err);
225
+ throw new PilotError(obj.error ?? 'unknown error');
226
+ }
227
+ return res.handle;
228
+ }
@@ -0,0 +1,3 @@
1
+ export { Driver, Conn, Listener, DEFAULT_SOCKET_PATH, PilotError } from './client.js';
2
+ export { findLibrary, loadLibrary } from './ffi.js';
3
+ export type { PilotLib } from './ffi.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { Driver, Conn, Listener, DEFAULT_SOCKET_PATH, PilotError } from './client.js';
2
+ export { findLibrary, loadLibrary } from './ffi.js';
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "pilotprotocol",
3
+ "version": "0.1.0",
4
+ "description": "Node.js SDK for Pilot Protocol — the network stack for AI agents",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "bin": {
15
+ "pilotctl": "bin-stubs/pilotctl.js",
16
+ "pilot-daemon": "bin-stubs/pilot-daemon.js",
17
+ "pilot-gateway": "bin-stubs/pilot-gateway.js",
18
+ "pilot-updater": "bin-stubs/pilot-updater.js"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "test": "vitest run",
23
+ "clean": "rm -rf dist/",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "pilot-protocol",
28
+ "networking",
29
+ "p2p",
30
+ "agent",
31
+ "ai",
32
+ "protocol",
33
+ "overlay",
34
+ "udp",
35
+ "encryption",
36
+ "ffi"
37
+ ],
38
+ "license": "AGPL-3.0-or-later",
39
+ "author": {
40
+ "name": "Alexandru Godoroja",
41
+ "email": "alex@vulturelabs.com"
42
+ },
43
+ "contributors": [
44
+ {
45
+ "name": "Teodor Calin",
46
+ "email": "teodor@vulturelabs.com"
47
+ }
48
+ ],
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/TeoSlayer/pilotprotocol",
52
+ "directory": "sdk/node"
53
+ },
54
+ "homepage": "https://pilotprotocol.network",
55
+ "bugs": {
56
+ "url": "https://github.com/TeoSlayer/pilotprotocol/issues"
57
+ },
58
+ "dependencies": {
59
+ "koffi": "^2.9.0"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^25.5.0",
63
+ "typescript": "^5.7.0",
64
+ "vitest": "^3.0.0"
65
+ },
66
+ "engines": {
67
+ "node": ">=20"
68
+ },
69
+ "os": [
70
+ "darwin",
71
+ "linux"
72
+ ],
73
+ "cpu": [
74
+ "x64",
75
+ "arm64"
76
+ ],
77
+ "files": [
78
+ "dist/",
79
+ "bin/",
80
+ "bin-stubs/"
81
+ ]
82
+ }