@relayfile/client 0.10.26 → 0.10.27

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/dist/client.d.ts CHANGED
@@ -13,9 +13,8 @@ import type { components } from './generated/control-plane.js';
13
13
  /** Control-plane API version this client speaks. Bump when adding required endpoint support. */
14
14
  export declare const RELAYFILE_API_VERSION = 3;
15
15
  /**
16
- * Minimum `relayfile` daemon version this client requires. The control-plane
17
- * first shipped in 0.10.17, so anything that answers `/v1/hello` is already >=
18
- * this — API compatibility is enforced by RELAYFILE_API_VERSION.
16
+ * Minimum `relayfile` daemon version this client requires. API compatibility is
17
+ * enforced independently through supportedApiVersions.
19
18
  */
20
19
  export declare const MIN_RELAYFILE_VERSION = "0.10.17";
21
20
  /** First semver-looking token in a string (tolerates a leading `v` and surrounding words). */
@@ -34,7 +33,11 @@ export declare function defaultRelayfileSocketPath(): string;
34
33
  export declare class RelayfileControlPlaneError extends Error {
35
34
  readonly code: string;
36
35
  readonly status?: number | undefined;
37
- constructor(code: string, message: string, status?: number | undefined);
36
+ /** True only for connection failures that may clear while a daemon starts. */
37
+ readonly transient: boolean;
38
+ constructor(code: string, message: string, status?: number | undefined,
39
+ /** True only for connection failures that may clear while a daemon starts. */
40
+ transient?: boolean);
38
41
  }
39
42
  type Schemas = components['schemas'];
40
43
  export type HelloResponse = Schemas['HelloResponse'];
@@ -75,12 +78,23 @@ export declare class RelayfileControlPlaneClient {
75
78
  private readonly requestTimeoutMs;
76
79
  private ready;
77
80
  constructor(options?: RelayfileClientOptions);
81
+ private createHTTPRequest;
78
82
  /** Low-level request over the unix socket. Throws RelayfileControlPlaneError. */
79
83
  private rawRequest;
80
84
  /** Connect (auto-starting the daemon if configured), version-negotiating once. */
81
85
  ensureReady(): Promise<void>;
82
86
  private connectAndNegotiate;
87
+ private spawnDaemon;
83
88
  private startDaemonAndConnect;
89
+ private stopStaleDaemon;
90
+ private socketOwnerPids;
91
+ /** Resolve a Unix-socket owner without external tools on standard Linux hosts. */
92
+ private linuxSocketOwnerPids;
93
+ private versionMismatchError;
94
+ private versionRejectionError;
95
+ private restartCommand;
96
+ private installedBinaryVersion;
97
+ private runCommand;
84
98
  hello(): Promise<HelloResponse>;
85
99
  resolvePath(provider: string, resource: string): Promise<ResolvePathResult>;
86
100
  bind(input: BindRequestBody): Promise<BindResult>;
package/dist/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { request } from 'node:http';
2
+ import { existsSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
3
+ import { request, } from 'node:http';
3
4
  import { tmpdir } from 'node:os';
4
5
  import { join } from 'node:path';
5
6
  /**
@@ -16,11 +17,12 @@ import { join } from 'node:path';
16
17
  /** Control-plane API version this client speaks. Bump when adding required endpoint support. */
17
18
  export const RELAYFILE_API_VERSION = 3;
18
19
  /**
19
- * Minimum `relayfile` daemon version this client requires. The control-plane
20
- * first shipped in 0.10.17, so anything that answers `/v1/hello` is already >=
21
- * this — API compatibility is enforced by RELAYFILE_API_VERSION.
20
+ * Minimum `relayfile` daemon version this client requires. API compatibility is
21
+ * enforced independently through supportedApiVersions.
22
22
  */
23
23
  export const MIN_RELAYFILE_VERSION = '0.10.17';
24
+ /** First published relayfile binary that can replace a stale daemon for API v3. */
25
+ const MIN_RELAYFILE_VERSION_FOR_CURRENT_API = '0.10.21';
24
26
  const SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+].*)?$/;
25
27
  /** First semver-looking token in a string (tolerates a leading `v` and surrounding words). */
26
28
  export function firstSemver(value) {
@@ -68,14 +70,36 @@ function relayfileBinary() {
68
70
  export class RelayfileControlPlaneError extends Error {
69
71
  code;
70
72
  status;
71
- constructor(code, message, status) {
73
+ transient;
74
+ constructor(code, message, status,
75
+ /** True only for connection failures that may clear while a daemon starts. */
76
+ transient = false) {
72
77
  super(message);
73
78
  this.code = code;
74
79
  this.status = status;
80
+ this.transient = transient;
75
81
  this.name = 'RelayfileControlPlaneError';
76
82
  }
77
83
  }
78
84
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
85
+ const TRANSIENT_CONNECTION_CODES = new Set([
86
+ 'ENOENT',
87
+ 'ECONNREFUSED',
88
+ 'ETIMEDOUT',
89
+ // A daemon that binds the socket before it is ready to serve can accept and
90
+ // then drop the connection mid-request; both surface during startup polling.
91
+ 'ECONNRESET',
92
+ 'EPIPE',
93
+ ]);
94
+ function isTransientConnectionCode(code) {
95
+ return code != null && TRANSIENT_CONNECTION_CODES.has(code);
96
+ }
97
+ function isTransientControlPlaneError(err) {
98
+ return (err instanceof RelayfileControlPlaneError && err.code === 'DAEMON_UNAVAILABLE' && err.transient);
99
+ }
100
+ function shellQuote(value) {
101
+ return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
102
+ }
79
103
  export class RelayfileControlPlaneClient {
80
104
  socketPath;
81
105
  binary;
@@ -90,6 +114,9 @@ export class RelayfileControlPlaneClient {
90
114
  this.startTimeoutMs = options.startTimeoutMs ?? 5000;
91
115
  this.requestTimeoutMs = options.requestTimeoutMs ?? 10000;
92
116
  }
117
+ createHTTPRequest(options, onResponse) {
118
+ return request(options, onResponse);
119
+ }
93
120
  /** Low-level request over the unix socket. Throws RelayfileControlPlaneError. */
94
121
  rawRequest(opts) {
95
122
  const query = opts.query
@@ -114,13 +141,18 @@ export class RelayfileControlPlaneClient {
114
141
  settled = true;
115
142
  resolve(value);
116
143
  };
117
- const req = request({
144
+ const req = this.createHTTPRequest({
118
145
  socketPath: this.socketPath,
119
146
  method: opts.method,
120
147
  path,
121
148
  timeout: this.requestTimeoutMs,
149
+ // Discovery must open a fresh socket. A pooled keep-alive connection
150
+ // would keep reaching the unlinked stale daemon after replacement.
151
+ agent: opts.freshConnection ? false : undefined,
122
152
  headers: {
123
- 'X-Relayfile-API-Version': String(RELAYFILE_API_VERSION),
153
+ ...(opts.includeVersionHeader === false
154
+ ? {}
155
+ : { 'X-Relayfile-API-Version': String(RELAYFILE_API_VERSION) }),
124
156
  Accept: 'application/json',
125
157
  ...(payload
126
158
  ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
@@ -129,7 +161,7 @@ export class RelayfileControlPlaneClient {
129
161
  }, (res) => {
130
162
  // The response stream can emit 'error' (e.g. socket reset mid-body);
131
163
  // without a listener that's an unhandled error -> crash.
132
- res.on('error', (err) => fail(new RelayfileControlPlaneError('DAEMON_UNAVAILABLE', err.message)));
164
+ res.on('error', (err) => fail(new RelayfileControlPlaneError('DAEMON_UNAVAILABLE', err.message, undefined, isTransientConnectionCode(err.code))));
133
165
  const chunks = [];
134
166
  res.on('data', (c) => chunks.push(c));
135
167
  res.on('end', () => {
@@ -152,7 +184,7 @@ export class RelayfileControlPlaneClient {
152
184
  });
153
185
  });
154
186
  req.on('timeout', () => {
155
- req.destroy(new RelayfileControlPlaneError('DAEMON_UNAVAILABLE', `relayfile control-plane request timed out after ${this.requestTimeoutMs}ms (${opts.method} ${opts.path})`));
187
+ req.destroy(new RelayfileControlPlaneError('DAEMON_UNAVAILABLE', `relayfile control-plane request timed out after ${this.requestTimeoutMs}ms (${opts.method} ${opts.path})`, undefined, true));
156
188
  });
157
189
  req.on('error', (err) => {
158
190
  if (err instanceof RelayfileControlPlaneError) {
@@ -162,7 +194,7 @@ export class RelayfileControlPlaneClient {
162
194
  // ENOENT (no socket file) / ECONNREFUSED (nothing listening) => daemon down.
163
195
  fail(new RelayfileControlPlaneError('DAEMON_UNAVAILABLE', err.code === 'ENOENT' || err.code === 'ECONNREFUSED'
164
196
  ? `relayfile control-plane is not running at ${this.socketPath} (${err.code})`
165
- : err.message, undefined));
197
+ : err.message, undefined, isTransientConnectionCode(err.code)));
166
198
  });
167
199
  if (payload)
168
200
  req.write(payload);
@@ -186,7 +218,10 @@ export class RelayfileControlPlaneClient {
186
218
  hello = await this.hello();
187
219
  }
188
220
  catch (err) {
189
- if (!(err instanceof RelayfileControlPlaneError) || err.code !== 'DAEMON_UNAVAILABLE')
221
+ if (err instanceof RelayfileControlPlaneError && err.code === 'VERSION_INCOMPATIBLE') {
222
+ throw await this.versionRejectionError(err);
223
+ }
224
+ if (!isTransientControlPlaneError(err))
190
225
  throw err;
191
226
  if (!this.autoStart) {
192
227
  throw new RelayfileControlPlaneError('DAEMON_UNAVAILABLE', `relayfile control-plane is not running at ${this.socketPath}. ` +
@@ -195,15 +230,34 @@ export class RelayfileControlPlaneClient {
195
230
  hello = await this.startDaemonAndConnect();
196
231
  }
197
232
  if (!hello.supportedApiVersions?.includes(RELAYFILE_API_VERSION)) {
198
- throw new RelayfileControlPlaneError('VERSION_INCOMPATIBLE', `relayfile daemon speaks API v${hello.apiVersion} (supports ${hello.supportedApiVersions?.join(', ') || 'none'}); this client needs v${RELAYFILE_API_VERSION}. Upgrade relayfile (or agent-relay).`);
233
+ const installedVersion = await this.installedBinaryVersion();
234
+ const daemonVersion = firstSemver(hello.daemonVersion);
235
+ const canReplaceStaleDaemon = this.autoStart &&
236
+ installedVersion != null &&
237
+ daemonVersion !== '' &&
238
+ compareSemver(installedVersion, MIN_RELAYFILE_VERSION_FOR_CURRENT_API) >= 0 &&
239
+ compareSemver(installedVersion, daemonVersion) > 0;
240
+ if (!canReplaceStaleDaemon) {
241
+ throw this.versionMismatchError(hello, installedVersion);
242
+ }
243
+ try {
244
+ await this.stopStaleDaemon();
245
+ hello = await this.startDaemonAndConnect(installedVersion);
246
+ }
247
+ catch (err) {
248
+ if (err instanceof RelayfileControlPlaneError && err.code === 'VERSION_INCOMPATIBLE') {
249
+ throw err;
250
+ }
251
+ throw this.versionMismatchError(hello, installedVersion, `Automatic stale-daemon replacement failed: ${err instanceof Error ? err.message : String(err)}`);
252
+ }
199
253
  }
200
254
  assertRelayfileVersion(hello.daemonVersion);
201
255
  }
202
- async startDaemonAndConnect() {
203
- let child;
256
+ spawnDaemon() {
204
257
  // A missing binary / bad RELAYFILE_BIN surfaces as an async 'error' event on
205
258
  // the child (ENOENT), not a throw from spawn().
206
259
  let spawnError;
260
+ let child;
207
261
  try {
208
262
  child = spawn(this.binary, ['control-plane', 'serve', '--sock', this.socketPath], {
209
263
  detached: true,
@@ -212,33 +266,223 @@ export class RelayfileControlPlaneClient {
212
266
  child.on('error', (err) => {
213
267
  spawnError = err;
214
268
  });
269
+ child.once('exit', (code, signal) => {
270
+ spawnError = new Error(`relayfile control-plane exited before readiness (${signal ? `signal ${signal}` : `code ${code}`})`);
271
+ });
215
272
  }
216
273
  catch (err) {
217
274
  throw new RelayfileControlPlaneError('DAEMON_UNAVAILABLE', `failed to start relayfile control-plane (${err instanceof Error ? err.message : String(err)}). ` +
218
275
  `Install relayfile or set RELAYFILE_BIN.`);
219
276
  }
220
277
  child.unref?.();
278
+ return { getError: () => spawnError };
279
+ }
280
+ async startDaemonAndConnect(installedVersion) {
281
+ const spawned = this.spawnDaemon();
221
282
  const deadline = Date.now() + this.startTimeoutMs;
222
283
  let lastErr;
223
284
  while (Date.now() < deadline) {
285
+ const spawnError = spawned.getError();
224
286
  if (spawnError) {
225
287
  throw new RelayfileControlPlaneError('DAEMON_UNAVAILABLE', `failed to start relayfile control-plane (${spawnError.message}). ` +
226
288
  `Install relayfile or set RELAYFILE_BIN.`);
227
289
  }
228
290
  await sleep(100);
291
+ let hello;
229
292
  try {
230
- return await this.hello();
293
+ hello = await this.hello();
231
294
  }
232
295
  catch (err) {
296
+ if (err instanceof RelayfileControlPlaneError && err.code === 'VERSION_INCOMPATIBLE') {
297
+ throw await this.versionRejectionError(err, installedVersion);
298
+ }
299
+ if (!isTransientControlPlaneError(err))
300
+ throw err;
233
301
  lastErr = err;
302
+ continue;
234
303
  }
304
+ if (hello.supportedApiVersions?.includes(RELAYFILE_API_VERSION)) {
305
+ return hello;
306
+ }
307
+ const mismatch = this.versionMismatchError(hello, installedVersion ?? (await this.installedBinaryVersion()));
308
+ throw mismatch;
235
309
  }
236
310
  throw new RelayfileControlPlaneError('DAEMON_UNAVAILABLE', `relayfile control-plane did not become ready within ${this.startTimeoutMs}ms ` +
237
311
  `(${lastErr instanceof Error ? lastErr.message : String(lastErr)}).`);
238
312
  }
313
+ async stopStaleDaemon() {
314
+ const pids = await this.socketOwnerPids();
315
+ if (pids.length !== 1) {
316
+ throw new Error(pids.length === 0
317
+ ? `could not identify the process serving ${this.socketPath}`
318
+ : `refusing to stop multiple processes serving ${this.socketPath}: ${pids.join(', ')}`);
319
+ }
320
+ try {
321
+ process.kill(pids[0], 'SIGTERM');
322
+ }
323
+ catch (err) {
324
+ throw new Error(`could not stop stale relayfile control-plane pid ${pids[0]}: ${err instanceof Error ? err.message : String(err)}`);
325
+ }
326
+ const deadline = Date.now() + this.startTimeoutMs;
327
+ while (existsSync(this.socketPath) && Date.now() < deadline) {
328
+ await sleep(50);
329
+ }
330
+ if (existsSync(this.socketPath)) {
331
+ throw new Error(`stale relayfile control-plane pid ${pids[0]} did not release ${this.socketPath} within ${this.startTimeoutMs}ms`);
332
+ }
333
+ }
334
+ async socketOwnerPids() {
335
+ if (process.platform === 'linux') {
336
+ const procPids = this.linuxSocketOwnerPids();
337
+ if (procPids.length > 0)
338
+ return procPids;
339
+ }
340
+ if (process.platform === 'win32') {
341
+ throw new Error('automatic stale-daemon replacement is not supported on Windows; stop the running relayfile control-plane manually');
342
+ }
343
+ const result = await this.runCommand('lsof', ['-t', '--', this.socketPath], 1000);
344
+ if (!result) {
345
+ throw new Error(`could not run \`lsof\` to identify the process serving ${this.socketPath} ` +
346
+ `(it is not installed, not on PATH, or did not exit within 1000ms); ` +
347
+ `install lsof or stop the stale relayfile control-plane manually`);
348
+ }
349
+ if (result.code !== 0) {
350
+ throw new Error(`lsof could not identify the process serving ${this.socketPath} (exit ${result.code}${result.stderr.trim() ? `: ${result.stderr.trim()}` : ''})`);
351
+ }
352
+ return [
353
+ ...new Set(result.stdout
354
+ .split(/\s+/)
355
+ .map((value) => Number.parseInt(value, 10))
356
+ .filter((pid) => Number.isSafeInteger(pid) && pid > 1 && pid !== process.pid)),
357
+ ];
358
+ }
359
+ /** Resolve a Unix-socket owner without external tools on standard Linux hosts. */
360
+ linuxSocketOwnerPids(procRoot = '/proc') {
361
+ let socketInodes;
362
+ try {
363
+ socketInodes = new Set(readFileSync(join(procRoot, 'net', 'unix'), 'utf8')
364
+ .split('\n')
365
+ .slice(1)
366
+ .map((line) => line.trim().split(/\s+/))
367
+ .filter((fields) => fields.length >= 8 && fields.slice(7).join(' ') === this.socketPath)
368
+ .map((fields) => fields[6]));
369
+ }
370
+ catch {
371
+ return [];
372
+ }
373
+ if (socketInodes.size === 0)
374
+ return [];
375
+ const pids = new Set();
376
+ let processes;
377
+ try {
378
+ processes = readdirSync(procRoot).filter((name) => /^\d+$/.test(name));
379
+ }
380
+ catch {
381
+ return [];
382
+ }
383
+ for (const processName of processes) {
384
+ const pid = Number.parseInt(processName, 10);
385
+ if (pid <= 1 || pid === process.pid)
386
+ continue;
387
+ const fdDir = join(procRoot, processName, 'fd');
388
+ let descriptors;
389
+ try {
390
+ descriptors = readdirSync(fdDir);
391
+ }
392
+ catch {
393
+ continue;
394
+ }
395
+ for (const descriptor of descriptors) {
396
+ try {
397
+ const target = readlinkSync(join(fdDir, descriptor));
398
+ const inode = target.match(/^socket:\[(\d+)\]$/)?.[1];
399
+ if (inode && socketInodes.has(inode)) {
400
+ pids.add(pid);
401
+ break;
402
+ }
403
+ }
404
+ catch {
405
+ // File descriptors may disappear while /proc is scanned.
406
+ }
407
+ }
408
+ }
409
+ return [...pids];
410
+ }
411
+ versionMismatchError(hello, installedVersion, detail) {
412
+ const supported = hello.supportedApiVersions?.length
413
+ ? hello.supportedApiVersions.map((version) => `v${version}`).join(', ')
414
+ : 'none reported';
415
+ return new RelayfileControlPlaneError('VERSION_INCOMPATIBLE', `relayfile daemon ${hello.daemonVersion} speaks API v${hello.apiVersion} ` +
416
+ `(supports ${supported}); this client requires API v${RELAYFILE_API_VERSION}. ` +
417
+ `Installed relayfile binary: ${installedVersion ?? 'version unknown'}. ` +
418
+ `${detail ? `${detail} ` : ''}` +
419
+ `Upgrade with \`npm install -g relayfile@latest\`, then restart the running control-plane with ` +
420
+ `\`${this.restartCommand()}\`.`);
421
+ }
422
+ async versionRejectionError(err, installedVersion) {
423
+ installedVersion ??= await this.installedBinaryVersion();
424
+ const spoken = err.message.match(/speaks API v(\d+)/i)?.[1];
425
+ return new RelayfileControlPlaneError('VERSION_INCOMPATIBLE', `relayfile daemon version unknown (discovery hello was rejected) speaks API ` +
426
+ `${spoken ? `v${spoken}` : 'version unknown'} (supported versions were not reported); ` +
427
+ `this client requires API v${RELAYFILE_API_VERSION}. Installed relayfile binary: ` +
428
+ `${installedVersion ?? 'version unknown'}. Upgrade with \`npm install -g relayfile@latest\`, ` +
429
+ `then restart the running control-plane with \`${this.restartCommand()}\`. ` +
430
+ `Daemon response: ${err.message}`, err.status);
431
+ }
432
+ restartCommand() {
433
+ const socket = shellQuote(this.socketPath);
434
+ return (`kill $(lsof -t -- ${socket}) && ` +
435
+ `${shellQuote(this.binary)} control-plane serve --sock ${socket}`);
436
+ }
437
+ async installedBinaryVersion() {
438
+ const result = await this.runCommand(this.binary, ['--version'], Math.min(this.requestTimeoutMs, 2000));
439
+ return firstSemver(`${result?.stdout ?? ''} ${result?.stderr ?? ''}`) || undefined;
440
+ }
441
+ runCommand(command, args, timeoutMs) {
442
+ return new Promise((resolve) => {
443
+ let child;
444
+ try {
445
+ child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
446
+ }
447
+ catch {
448
+ resolve(undefined);
449
+ return;
450
+ }
451
+ let stdout = '';
452
+ let stderr = '';
453
+ let settled = false;
454
+ let timeout;
455
+ const finish = (result) => {
456
+ if (settled)
457
+ return;
458
+ settled = true;
459
+ if (timeout)
460
+ clearTimeout(timeout);
461
+ resolve(result);
462
+ };
463
+ child.stdout?.on('data', (chunk) => {
464
+ stdout += String(chunk);
465
+ });
466
+ child.stderr?.on('data', (chunk) => {
467
+ stderr += String(chunk);
468
+ });
469
+ child.once('error', () => finish());
470
+ child.once('close', (code) => finish({ stdout, stderr, code }));
471
+ timeout = setTimeout(() => {
472
+ child.kill();
473
+ finish();
474
+ }, timeoutMs);
475
+ timeout.unref?.();
476
+ });
477
+ }
239
478
  // ── endpoints ──────────────────────────────────────────────────────────────
240
479
  hello() {
241
- return this.rawRequest({ method: 'GET', path: '/v1/hello' });
480
+ return this.rawRequest({
481
+ method: 'GET',
482
+ path: '/v1/hello',
483
+ includeVersionHeader: false,
484
+ freshConnection: true,
485
+ });
242
486
  }
243
487
  resolvePath(provider, resource) {
244
488
  return this.request({
@@ -10,7 +10,14 @@ export interface paths {
10
10
  path?: never;
11
11
  cookie?: never;
12
12
  };
13
- /** Negotiate control-plane API compatibility */
13
+ /**
14
+ * Negotiate control-plane API compatibility
15
+ * @description Unversioned discovery endpoint. The daemon always returns its current
16
+ * and supported API versions; any version header or query is ignored.
17
+ * No version parameters are declared here on purpose: discovery must stay
18
+ * callable by clients of any API version, including versions this contract
19
+ * does not know about.
20
+ */
14
21
  get: operations["hello"];
15
22
  put?: never;
16
23
  /** Negotiate control-plane API compatibility */
@@ -366,7 +373,6 @@ export interface components {
366
373
  };
367
374
  parameters: {
368
375
  ApiVersionHeader: 3;
369
- ApiVersionQuery: number;
370
376
  };
371
377
  requestBodies: never;
372
378
  headers: never;
@@ -376,12 +382,8 @@ export type $defs = Record<string, never>;
376
382
  export interface operations {
377
383
  hello: {
378
384
  parameters: {
379
- query?: {
380
- apiVersion?: components["parameters"]["ApiVersionQuery"];
381
- };
382
- header?: {
383
- "X-Relayfile-API-Version"?: components["parameters"]["ApiVersionHeader"];
384
- };
385
+ query?: never;
386
+ header?: never;
385
387
  path?: never;
386
388
  cookie?: never;
387
389
  };
@@ -396,7 +398,6 @@ export interface operations {
396
398
  "application/json": components["schemas"]["HelloResponse"];
397
399
  };
398
400
  };
399
- 426: components["responses"]["VersionIncompatible"];
400
401
  };
401
402
  };
402
403
  helloPost: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayfile/client",
3
- "version": "0.10.26",
3
+ "version": "0.10.27",
4
4
  "description": "Typed client for the relayfile local control-plane (unix-socket HTTP/JSON), generated from the authoritative OpenAPI contract",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",