@runtypelabs/cli 2.59.0 → 2.59.2

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 (84) hide show
  1. package/dist/chunk-0-T9NWjH.js +3899 -0
  2. package/dist/chunk-0x7UKC1U.js +17 -0
  3. package/dist/chunk-2ySxXtcg.js +418 -0
  4. package/dist/chunk-3An_BRHv.js +501 -0
  5. package/dist/chunk-6uTxgdiY.js +16218 -0
  6. package/dist/chunk-87nRnNRG.js +1363 -0
  7. package/dist/chunk-B3wo8uRx.js +322 -0
  8. package/dist/chunk-BGp3ZdBy.js +560 -0
  9. package/dist/chunk-BJQv9Elp.js +107 -0
  10. package/dist/chunk-BOpO6xE-.js +6053 -0
  11. package/dist/chunk-BQXXCL8B.js +102 -0
  12. package/dist/chunk-BV-8isOd.js +34 -0
  13. package/dist/chunk-BVMhjGVe.js +826 -0
  14. package/dist/chunk-BW2fg46T.js +141 -0
  15. package/dist/chunk-BWJdY5Ag.js +36 -0
  16. package/dist/chunk-BYJOrLhh.js +302 -0
  17. package/dist/chunk-BYab8EVL.js +127 -0
  18. package/dist/chunk-BfEKvp7h.js +82 -0
  19. package/dist/chunk-BhgLSfH6.js +1123 -0
  20. package/dist/chunk-BirUxTtH.js +296 -0
  21. package/dist/chunk-BmB6UVZ9.js +1559 -0
  22. package/dist/chunk-BnWhIqW1.js +109 -0
  23. package/dist/chunk-Bpfnfk0t.js +123 -0
  24. package/dist/chunk-BrhgdnM3.js +108 -0
  25. package/dist/chunk-Bt0XAgX5.js +327 -0
  26. package/dist/chunk-Bu0FJr6S.js +84 -0
  27. package/dist/chunk-BvEoKpN3.js +255 -0
  28. package/dist/chunk-BxNJgYbE.js +28 -0
  29. package/dist/chunk-C1OFhd7F.js +6131 -0
  30. package/dist/chunk-C7e7WwbU.js +331 -0
  31. package/dist/chunk-CDTYP6bN.js +558 -0
  32. package/dist/chunk-CGDQoqDC.js +24 -0
  33. package/dist/chunk-CJraEBVd.js +127 -0
  34. package/dist/chunk-CQrNhOtv.js +40 -0
  35. package/dist/chunk-CW6AzBH6.js +128 -0
  36. package/dist/chunk-CXgL4w-9.js +317 -0
  37. package/dist/chunk-CZrqoPyf.js +233 -0
  38. package/dist/chunk-CcFCyqbB.js +83 -0
  39. package/dist/chunk-CdaIvO-y.js +485 -0
  40. package/dist/chunk-CehSXtgK.js +624 -0
  41. package/dist/chunk-Ciqkop65.js +218 -0
  42. package/dist/chunk-CoAWdSiE.js +43 -0
  43. package/dist/chunk-Crb4y7t4.js +830 -0
  44. package/dist/chunk-CrlqIfmb.js +337 -0
  45. package/dist/chunk-Cs4pwCQ-.js +324 -0
  46. package/dist/chunk-CwM_9y01.js +519 -0
  47. package/dist/chunk-CyRbIJg8.js +440 -0
  48. package/dist/chunk-D0OHsyMy.js +212 -0
  49. package/dist/chunk-D3y2bURs.js +1347 -0
  50. package/dist/chunk-D4_mJk03.js +2338 -0
  51. package/dist/chunk-D6C8rned.js +578 -0
  52. package/dist/chunk-D9ZbUnks.js +11 -0
  53. package/dist/chunk-DEPmLshX.js +834 -0
  54. package/dist/chunk-DFj1UPB2.js +330 -0
  55. package/dist/chunk-DGdFdZVf.js +197 -0
  56. package/dist/chunk-DGv7X9S_.js +63 -0
  57. package/dist/chunk-DJQ47WYZ.js +664 -0
  58. package/dist/chunk-DJw3O61j.js +1974 -0
  59. package/dist/chunk-DK4FbWVK.js +100 -0
  60. package/dist/chunk-DSRYhUus.js +993 -0
  61. package/dist/chunk-DajZ9Eg4.js +299 -0
  62. package/dist/chunk-Df4dunvU.js +195 -0
  63. package/dist/chunk-DjNTPhfO.js +481 -0
  64. package/dist/chunk-Dn8eaJuu.js +1042 -0
  65. package/dist/chunk-DwDWqSxM.js +637 -0
  66. package/dist/chunk-Dz7cEtxl.js +84 -0
  67. package/dist/chunk-E_0bgjrS.js +359 -0
  68. package/dist/chunk-KPYN2vUp.js +776 -0
  69. package/dist/chunk-KcqPQJBi.js +86 -0
  70. package/dist/chunk-N9H0JnoG.js +349 -0
  71. package/dist/chunk-NONFcXkn.js +728 -0
  72. package/dist/chunk-PrFjcpmH.js +256 -0
  73. package/dist/chunk-QSfNYiw9.js +11 -0
  74. package/dist/chunk-R265bzj9.js +226 -0
  75. package/dist/chunk-X9V_r6jM.js +166 -0
  76. package/dist/chunk-ZBtPUtyA.js +55 -0
  77. package/dist/chunk-ZCTfmOs7.js +4939 -0
  78. package/dist/chunk-h6oavj0t.js +97 -0
  79. package/dist/chunk-i7DwkvvD.js +42 -0
  80. package/dist/chunk-mG-4eZ6S.js +203 -0
  81. package/dist/chunk-o0ygUbCf.js +313 -0
  82. package/dist/chunk-ucu5lrDZ.js +5715 -0
  83. package/dist/index.js +11367 -73764
  84. package/package.json +6 -6
@@ -0,0 +1,728 @@
1
+ #!/usr/bin/env node
2
+ import { t as isRecord } from "./chunk-QSfNYiw9.js";
3
+ import { i as getApiUrl, r as createCliClient } from "./chunk-BW2fg46T.js";
4
+ import { n as ensureAuth } from "./chunk-CJraEBVd.js";
5
+ import { a as printJson, i as printDetail } from "./chunk-CcFCyqbB.js";
6
+ import { i as createDeviceListenIdentityStore, n as assertListenForwardTarget, r as runDeviceListen } from "./chunk-Dn8eaJuu.js";
7
+ import { Command } from "commander";
8
+ import chalk from "chalk";
9
+ import { constants } from "node:fs";
10
+ import path from "node:path";
11
+ import { spawn } from "node:child_process";
12
+ import os from "node:os";
13
+ import { randomUUID } from "node:crypto";
14
+ import { access, chmod, copyFile, mkdir, readFile, realpath, rename, rm, stat } from "node:fs/promises";
15
+ // src/lib/device-provider.ts
16
+ var DeviceProviderError = class extends Error {
17
+ code;
18
+ retryable;
19
+ constructor(message, code, retryable = false, options) {
20
+ super(message, options);
21
+ this.code = code;
22
+ this.retryable = retryable;
23
+ this.name = "DeviceProviderError";
24
+ }
25
+ };
26
+ // src/lib/macos-device-provider.ts
27
+ const MAX_SIDECAR_OUTPUT_BYTES = 1048576;
28
+ const MAX_HELPER_BYTES = 104857600;
29
+ const DEFAULT_SIDECAR_TIMEOUT_MS = 3e4;
30
+ function parseHello(value) {
31
+ if (!isRecord(value) || typeof value.providerId !== "string" || typeof value.protocolVersion !== "number" || typeof value.helperVersion !== "string" || typeof value.platform !== "string" || !Array.isArray(value.capabilities) || !value.capabilities.every((capability) => typeof capability === "string")) throw new DeviceProviderError("The native helper returned an invalid hello response", "INVALID_RESPONSE");
32
+ return value;
33
+ }
34
+ async function isExecutable(filePath) {
35
+ try {
36
+ await access(filePath, constants.X_OK);
37
+ return (await stat(filePath)).isFile();
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+ var MacOSDeviceProvider = class {
43
+ id = "com.runtype.device.macos";
44
+ platform;
45
+ architecture;
46
+ operatingSystemRelease;
47
+ homeDirectory;
48
+ workingDirectory;
49
+ environment;
50
+ sidecarTimeoutMs;
51
+ constructor(options = {}) {
52
+ this.platform = options.platform ?? process.platform;
53
+ this.architecture = options.architecture ?? process.arch;
54
+ this.operatingSystemRelease = options.operatingSystemRelease ?? os.release();
55
+ this.homeDirectory = options.homeDirectory ?? os.homedir();
56
+ this.workingDirectory = options.workingDirectory ?? process.cwd();
57
+ this.environment = options.environment ?? process.env;
58
+ this.sidecarTimeoutMs = options.sidecarTimeoutMs ?? DEFAULT_SIDECAR_TIMEOUT_MS;
59
+ }
60
+ async probe() {
61
+ const unsupportedReason = this.unsupportedReason();
62
+ if (unsupportedReason) return {
63
+ providerId: this.id,
64
+ supported: false,
65
+ installed: false,
66
+ reason: unsupportedReason
67
+ };
68
+ const binaryPath = await this.resolveBinary();
69
+ if (!binaryPath) return {
70
+ providerId: this.id,
71
+ supported: true,
72
+ installed: false,
73
+ reason: "The Runtype macOS device helper is not installed"
74
+ };
75
+ try {
76
+ const hello = parseHello(await this.invokeAt(binaryPath, "hello"));
77
+ this.assertCompatible(hello);
78
+ return {
79
+ providerId: this.id,
80
+ supported: true,
81
+ installed: true,
82
+ binaryPath,
83
+ helperVersion: hello.helperVersion,
84
+ protocolVersion: hello.protocolVersion
85
+ };
86
+ } catch (error) {
87
+ return {
88
+ providerId: this.id,
89
+ supported: true,
90
+ installed: true,
91
+ binaryPath,
92
+ reason: error instanceof Error ? error.message : String(error)
93
+ };
94
+ }
95
+ }
96
+ async ensureInstalled(options = {}) {
97
+ this.assertPlatform();
98
+ if (!options.helperPath) {
99
+ const installedPath = await this.resolveInstalledBinary();
100
+ if (installedPath) return this.describeInstallation(installedPath, false);
101
+ }
102
+ const sourcePath = options.helperPath ? path.resolve(options.helperPath) : await this.resolveDevelopmentSource();
103
+ if (!sourcePath) throw new DeviceProviderError([
104
+ "The Runtype macOS device helper is not installed.",
105
+ "For this internal-alpha build, compile it from the core repository and pass:",
106
+ " runtype devices setup --helper-path apps/device-daemon-macos/.build/release/runtype-device"
107
+ ].join("\n"), "HELPER_NOT_INSTALLED");
108
+ const resolvedSource = await realpath(sourcePath).catch(() => sourcePath);
109
+ const sourceDetails = await stat(resolvedSource).catch(() => null);
110
+ if (!sourceDetails?.isFile() || sourceDetails.size > MAX_HELPER_BYTES) throw new DeviceProviderError(`Device helper is not a valid executable file: ${sourcePath}`, "INVALID_HELPER");
111
+ await access(resolvedSource, constants.X_OK).catch((error) => {
112
+ throw new DeviceProviderError(`Device helper is not executable: ${sourcePath}`, "INVALID_HELPER", false, { cause: error });
113
+ });
114
+ const sourceHello = parseHello(await this.invokeAt(resolvedSource, "hello"));
115
+ this.assertCompatible(sourceHello);
116
+ const targetPath = this.stableBinaryPath;
117
+ if (resolvedSource !== await realpath(targetPath).catch(() => targetPath)) {
118
+ const targetDirectory = path.dirname(targetPath);
119
+ await mkdir(targetDirectory, {
120
+ recursive: true,
121
+ mode: 448
122
+ });
123
+ await chmod(targetDirectory, 448);
124
+ const temporaryPath = path.join(targetDirectory, `.runtype-device-${randomUUID()}.tmp`);
125
+ try {
126
+ await copyFile(resolvedSource, temporaryPath, constants.COPYFILE_EXCL);
127
+ await chmod(temporaryPath, 448);
128
+ await rename(temporaryPath, targetPath);
129
+ } finally {
130
+ await rm(temporaryPath, { force: true });
131
+ }
132
+ }
133
+ return this.describeInstallation(targetPath, true);
134
+ }
135
+ async invoke(method, params = {}) {
136
+ this.assertPlatform();
137
+ const binaryPath = await this.resolveBinary();
138
+ if (!binaryPath) throw new DeviceProviderError("The Runtype macOS device helper is not installed. Run `runtype devices setup` first.", "HELPER_NOT_INSTALLED");
139
+ const result = await this.invokeAt(binaryPath, method, params);
140
+ if (!isRecord(result)) throw new DeviceProviderError("The native helper returned a non-object result", "INVALID_RESPONSE");
141
+ return result;
142
+ }
143
+ async removeInstalledBinary() {
144
+ if (!await isExecutable(this.stableBinaryPath)) return false;
145
+ await rm(this.stableBinaryPath, { force: true });
146
+ return true;
147
+ }
148
+ get stableBinaryPath() {
149
+ return path.join(this.homeDirectory, ".runtype", "device", "bin", "runtype-device");
150
+ }
151
+ installedCandidates() {
152
+ return [
153
+ this.stableBinaryPath,
154
+ path.join(this.homeDirectory, "Applications", "Runtype Device.app", "Contents", "MacOS", "runtype-device"),
155
+ "/Applications/Runtype Device.app/Contents/MacOS/runtype-device"
156
+ ];
157
+ }
158
+ developmentCandidates() {
159
+ return [
160
+ ...this.environment.RUNTYPE_DEVICE_HELPER_PATH ? [path.resolve(this.environment.RUNTYPE_DEVICE_HELPER_PATH)] : [],
161
+ path.join(this.workingDirectory, "apps", "device-daemon-macos", ".build", "release", "runtype-device"),
162
+ path.join(this.workingDirectory, "apps", "device-daemon-macos", ".build", "debug", "runtype-device")
163
+ ];
164
+ }
165
+ async firstExecutable(candidates) {
166
+ for (const candidate of candidates) if (await isExecutable(candidate)) return candidate;
167
+ return null;
168
+ }
169
+ resolveInstalledBinary() {
170
+ return this.firstExecutable(this.installedCandidates());
171
+ }
172
+ resolveDevelopmentSource() {
173
+ return this.firstExecutable(this.developmentCandidates());
174
+ }
175
+ async resolveBinary() {
176
+ return await this.resolveInstalledBinary() ?? await this.firstExecutable(this.developmentCandidates());
177
+ }
178
+ assertPlatform() {
179
+ const reason = this.unsupportedReason();
180
+ if (!reason) return;
181
+ throw new DeviceProviderError(reason, this.platform === "darwin" ? "UNSUPPORTED_OS_VERSION" : "UNSUPPORTED_PLATFORM");
182
+ }
183
+ unsupportedReason() {
184
+ if (this.platform !== "darwin") return `Runtype Device currently supports macOS only (detected ${this.platform}/${this.architecture})`;
185
+ const darwinMajor = Number.parseInt(this.operatingSystemRelease.split(".")[0] ?? "", 10);
186
+ if (!Number.isInteger(darwinMajor) || darwinMajor < 23) return `Runtype Device requires macOS 14 or newer (detected Darwin ${this.operatingSystemRelease})`;
187
+ return null;
188
+ }
189
+ assertCompatible(hello) {
190
+ if (hello.providerId !== this.id || hello.platform !== "macos") throw new DeviceProviderError(`Unexpected native provider identity: ${hello.providerId}/${hello.platform}`, "PROVIDER_MISMATCH");
191
+ if (hello.protocolVersion !== 1) throw new DeviceProviderError(`Native helper protocol ${hello.protocolVersion} is incompatible with CLI protocol 1`, "PROTOCOL_MISMATCH");
192
+ }
193
+ async describeInstallation(binaryPath, installed) {
194
+ const hello = parseHello(await this.invokeAt(binaryPath, "hello"));
195
+ this.assertCompatible(hello);
196
+ return {
197
+ providerId: this.id,
198
+ binaryPath,
199
+ helperVersion: hello.helperVersion,
200
+ protocolVersion: hello.protocolVersion,
201
+ installed
202
+ };
203
+ }
204
+ async invokeAt(binaryPath, method, params = {}) {
205
+ const id = randomUUID();
206
+ const request = JSON.stringify({
207
+ protocolVersion: 1,
208
+ id,
209
+ method,
210
+ params
211
+ });
212
+ const child = spawn(binaryPath, ["control"], {
213
+ env: this.environment,
214
+ stdio: [
215
+ "pipe",
216
+ "pipe",
217
+ "pipe"
218
+ ],
219
+ shell: false
220
+ });
221
+ const stdout = [];
222
+ const stderr = [];
223
+ let stdoutBytes = 0;
224
+ let stderrBytes = 0;
225
+ let outputExceeded = false;
226
+ let timedOut = false;
227
+ child.stdout.on("data", (chunk) => {
228
+ stdoutBytes += chunk.length;
229
+ if (stdoutBytes > MAX_SIDECAR_OUTPUT_BYTES) {
230
+ outputExceeded = true;
231
+ child.kill("SIGKILL");
232
+ return;
233
+ }
234
+ stdout.push(chunk);
235
+ });
236
+ child.stderr.on("data", (chunk) => {
237
+ if (stderrBytes >= MAX_SIDECAR_OUTPUT_BYTES) return;
238
+ stderrBytes += chunk.length;
239
+ stderr.push(chunk.subarray(0, MAX_SIDECAR_OUTPUT_BYTES - (stderrBytes - chunk.length)));
240
+ });
241
+ const completion = new Promise((resolve, reject) => {
242
+ child.once("error", reject);
243
+ child.once("close", (code, signal) => resolve({
244
+ code,
245
+ signal
246
+ }));
247
+ });
248
+ const timeout = setTimeout(() => {
249
+ timedOut = true;
250
+ child.kill("SIGKILL");
251
+ }, this.sidecarTimeoutMs);
252
+ child.stdin.on("error", () => {});
253
+ child.stdin.end(`${request}\n`);
254
+ let completionResult;
255
+ try {
256
+ completionResult = await completion;
257
+ } catch (error) {
258
+ throw new DeviceProviderError(`Unable to start the native device helper at ${binaryPath}`, "HELPER_START_FAILED", false, { cause: error });
259
+ } finally {
260
+ clearTimeout(timeout);
261
+ }
262
+ if (timedOut) throw new DeviceProviderError(`Native device helper timed out after ${this.sidecarTimeoutMs}ms`, "HELPER_TIMEOUT", true);
263
+ if (outputExceeded) throw new DeviceProviderError(`Native device helper output exceeded ${MAX_SIDECAR_OUTPUT_BYTES} bytes`, "OUTPUT_TOO_LARGE");
264
+ const output = Buffer.concat(stdout).toString("utf8").trim();
265
+ let response;
266
+ try {
267
+ response = JSON.parse(output);
268
+ } catch (error) {
269
+ const detail = Buffer.concat(stderr).toString("utf8").trim();
270
+ throw new DeviceProviderError(`Native device helper returned invalid JSON${detail ? `: ${detail}` : ""}`, "INVALID_RESPONSE", false, { cause: error });
271
+ }
272
+ if (response.protocolVersion !== 1 || response.id !== id || typeof response.ok !== "boolean") throw new DeviceProviderError("Native device helper returned a mismatched response envelope", "INVALID_RESPONSE");
273
+ if (!response.ok) throw new DeviceProviderError(response.error?.message || "Native device operation failed", response.error?.code || "OPERATION_FAILED", response.error?.retryable === true);
274
+ if (completionResult.code !== 0) throw new DeviceProviderError(`Native device helper exited with status ${completionResult.code ?? completionResult.signal ?? "unknown"}`, "HELPER_EXITED");
275
+ return response.result;
276
+ }
277
+ };
278
+ // src/commands/devices.ts
279
+ const defaultDependencies = {
280
+ ensureAuth,
281
+ createClient: (apiKey) => createCliClient(apiKey),
282
+ createProvider: () => new MacOSDeviceProvider(),
283
+ getApiUrl,
284
+ sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
285
+ setExitCode: (code) => {
286
+ process.exitCode = code;
287
+ },
288
+ createIdentityStore: (workspace) => createDeviceListenIdentityStore({ workspace }),
289
+ runListen: (options) => runDeviceListen(options)
290
+ };
291
+ function unwrapData(response) {
292
+ if (!isRecord(response) || !("data" in response)) throw new Error("Runtype API returned an invalid device response");
293
+ return response.data;
294
+ }
295
+ function errorMessage(error) {
296
+ return error instanceof Error ? error.message : String(error);
297
+ }
298
+ function apiStatusCode(error) {
299
+ return isRecord(error) && typeof error.statusCode === "number" ? error.statusCode : null;
300
+ }
301
+ function isRecordedRevocationError(error) {
302
+ if (apiStatusCode(error) !== 503) return false;
303
+ const data = isRecord(error) && isRecord(error.data) ? error.data : null;
304
+ return (typeof data?.error === "string" ? data.error : errorMessage(error)).toLowerCase().includes("revocation was recorded");
305
+ }
306
+ function printCommandError(label, error) {
307
+ console.error(chalk.red(label));
308
+ console.error(chalk.red(errorMessage(error)));
309
+ }
310
+ function printDevice(device) {
311
+ printDetail("Runtype Device", [
312
+ {
313
+ label: "ID",
314
+ value: device.id
315
+ },
316
+ {
317
+ label: "Name",
318
+ value: device.name
319
+ },
320
+ {
321
+ label: "Platform",
322
+ value: device.platform
323
+ },
324
+ {
325
+ label: "Online",
326
+ value: device.status?.online
327
+ },
328
+ {
329
+ label: "Last seen",
330
+ value: device.status?.lastSeenAt
331
+ },
332
+ {
333
+ label: "Available tools",
334
+ value: device.status?.availableTools.join(", ")
335
+ },
336
+ {
337
+ label: "MCP URL",
338
+ value: device.mcpUrl
339
+ },
340
+ {
341
+ label: "Revoked",
342
+ value: device.revokedAt
343
+ }
344
+ ]);
345
+ }
346
+ function localDeviceId(status) {
347
+ return typeof status.deviceId === "string" && status.deviceId.length > 0 ? status.deviceId : null;
348
+ }
349
+ async function authenticatedClient(dependencies) {
350
+ const apiKey = await dependencies.ensureAuth({ exitOnFailure: false });
351
+ if (!apiKey) {
352
+ dependencies.setExitCode(1);
353
+ return null;
354
+ }
355
+ return dependencies.createClient(apiKey);
356
+ }
357
+ async function waitForDevice(client, deviceId, dependencies) {
358
+ let latest = null;
359
+ for (let attempt = 0; attempt < 8; attempt += 1) {
360
+ try {
361
+ latest = unwrapData(await client.get(`/devices/${encodeURIComponent(deviceId)}`));
362
+ if (latest.status?.online) return latest;
363
+ } catch {}
364
+ if (attempt < 7) await dependencies.sleep(750);
365
+ }
366
+ return latest;
367
+ }
368
+ async function runTail(logPath, lines) {
369
+ const child = spawn("/usr/bin/tail", [
370
+ "-n",
371
+ String(lines),
372
+ "-f",
373
+ logPath
374
+ ], {
375
+ stdio: "inherit",
376
+ shell: false
377
+ });
378
+ return new Promise((resolve, reject) => {
379
+ child.once("error", reject);
380
+ child.once("close", (code) => resolve(code ?? 1));
381
+ });
382
+ }
383
+ function createDevicesCommand(overrides = {}) {
384
+ const dependencies = {
385
+ ...defaultDependencies,
386
+ ...overrides
387
+ };
388
+ const command = new Command("devices").description("Manage local computers connected to Runtype agents");
389
+ command.command("setup").description("Install, pair, and start Runtype Device on this Mac").option("--name <name>", "Device name shown in Runtype").option("--helper-path <path>", "Install a locally built native helper (internal alpha)").option("--json", "Output as JSON").action(async (options) => {
390
+ const provider = dependencies.createProvider();
391
+ let pairedDeviceId = null;
392
+ try {
393
+ const installation = await provider.ensureInstalled({ helperPath: options.helperPath });
394
+ const existingDeviceId = localDeviceId(await provider.invoke("status"));
395
+ if (existingDeviceId) throw new DeviceProviderError(`This Mac is already paired as ${existingDeviceId}. Run \`runtype devices status\` instead.`, "ALREADY_PAIRED");
396
+ const client = await authenticatedClient(dependencies);
397
+ if (!client) return;
398
+ const pairParams = {
399
+ pairingUri: unwrapData(await client.post("/devices/pairing-sessions", {})).pairingUri,
400
+ apiUrl: dependencies.getApiUrl()
401
+ };
402
+ if (options.name) pairParams.name = options.name;
403
+ const paired = await provider.invoke("pair", pairParams);
404
+ pairedDeviceId = paired.deviceId;
405
+ const service = await provider.invoke("install");
406
+ const local = await provider.invoke("status");
407
+ const device = await waitForDevice(client, paired.deviceId, dependencies);
408
+ const result = {
409
+ deviceId: paired.deviceId,
410
+ name: paired.name,
411
+ online: device?.status?.online ?? false,
412
+ mcpUrl: device?.mcpUrl ?? null,
413
+ installation,
414
+ service,
415
+ local
416
+ };
417
+ if (options.json) {
418
+ printJson(result);
419
+ return;
420
+ }
421
+ console.log(chalk.green(`Paired and started ${paired.name}`));
422
+ console.log(` Device ID: ${chalk.green(paired.deviceId)}`);
423
+ console.log(` Relay: ${device?.status?.online ? chalk.green("online") : chalk.yellow("connecting")}`);
424
+ if (device?.mcpUrl) console.log(` MCP URL: ${device.mcpUrl}`);
425
+ if (local.messagesDatabaseReadable !== true) console.log(chalk.yellow(`\nGrant Full Disk Access to ${String(local.executablePath)} to enable iMessage reads.`));
426
+ console.log(chalk.gray("iMessage sending remains disabled by local policy."));
427
+ } catch (error) {
428
+ printCommandError("Failed to set up Runtype Device", error);
429
+ if (pairedDeviceId) console.error(chalk.yellow(`Pairing completed as ${pairedDeviceId}, but startup did not finish. Run \`runtype devices install\` after correcting the error.`));
430
+ dependencies.setExitCode(1);
431
+ }
432
+ });
433
+ command.command("install").description("Install or restart the local Runtype Device background service").option("--helper-path <path>", "Install a locally built native helper (internal alpha)").option("--json", "Output as JSON").action(async (options) => {
434
+ const provider = dependencies.createProvider();
435
+ try {
436
+ const installation = await provider.ensureInstalled({ helperPath: options.helperPath });
437
+ const service = await provider.invoke("install");
438
+ if (options.json) printJson({
439
+ installation,
440
+ service
441
+ });
442
+ else {
443
+ console.log(chalk.green("Runtype Device is installed and running"));
444
+ console.log(` Helper: ${installation.binaryPath}`);
445
+ if (typeof service.logPath === "string") console.log(` Logs: ${service.logPath}`);
446
+ }
447
+ } catch (error) {
448
+ printCommandError("Failed to install Runtype Device", error);
449
+ dependencies.setExitCode(1);
450
+ }
451
+ });
452
+ command.command("listen").description("Forward this Runtype workspace's calls to a localhost agent. Does not publish a URL. Does not bind that port.").option("--forward-to <url>", "Loopback HTTP URL of the local agent").option("--name <name>", "Device name shown in Runtype").option("--json", "Output the connected session as JSON").action(async (options) => {
453
+ let forwardTo;
454
+ try {
455
+ forwardTo = assertListenForwardTarget(options.forwardTo);
456
+ } catch (error) {
457
+ printCommandError("Cannot forward to that URL", error);
458
+ dependencies.setExitCode(1);
459
+ return;
460
+ }
461
+ const client = await authenticatedClient(dependencies);
462
+ if (!client) return;
463
+ try {
464
+ const workspace = await client.get("/auth/me");
465
+ if (typeof workspace.userId !== "string" || !workspace.userId || workspace.orgId !== null && typeof workspace.orgId !== "string") throw new Error("Cannot verify the authenticated workspace");
466
+ await dependencies.runListen({
467
+ client,
468
+ apiUrl: dependencies.getApiUrl(),
469
+ forwardTo,
470
+ name: options.name,
471
+ json: Boolean(options.json),
472
+ identityStore: dependencies.createIdentityStore(workspace),
473
+ sleep: dependencies.sleep,
474
+ setExitCode: dependencies.setExitCode
475
+ });
476
+ } catch (error) {
477
+ printCommandError("Failed to listen for workspace calls", error);
478
+ dependencies.setExitCode(1);
479
+ }
480
+ });
481
+ command.command("list").description("List paired devices and relay status").option("--json", "Output as JSON").action(async (options) => {
482
+ try {
483
+ const client = await authenticatedClient(dependencies);
484
+ if (!client) return;
485
+ const devices = unwrapData(await client.get("/devices"));
486
+ if (options.json) {
487
+ printJson({ data: devices });
488
+ return;
489
+ }
490
+ if (devices.length === 0) {
491
+ console.log(chalk.gray("No paired devices"));
492
+ return;
493
+ }
494
+ console.log(chalk.cyan("Runtype Devices:"));
495
+ for (const device of devices) {
496
+ const state = device.revokedAt ? "revoked" : device.status?.online ? "online" : "offline";
497
+ console.log(` ${chalk.green(device.id)} ${device.name} ${state}`);
498
+ }
499
+ } catch (error) {
500
+ printCommandError("Failed to list Runtype devices", error);
501
+ dependencies.setExitCode(1);
502
+ }
503
+ });
504
+ command.command("status [id]").description("Show local status, or cloud relay status for a device ID").option("--json", "Output as JSON").action(async (id, options) => {
505
+ try {
506
+ if (id) {
507
+ const client = await authenticatedClient(dependencies);
508
+ if (!client) return;
509
+ const device = unwrapData(await client.get(`/devices/${encodeURIComponent(id)}`));
510
+ if (options.json) printJson(device);
511
+ else printDevice(device);
512
+ return;
513
+ }
514
+ const local = await dependencies.createProvider().invoke("status");
515
+ if (options.json) printJson(local);
516
+ else printDetail("Local Runtype Device", [
517
+ {
518
+ label: "Device ID",
519
+ value: local.deviceId ?? "not paired"
520
+ },
521
+ {
522
+ label: "API URL",
523
+ value: local.apiUrl
524
+ },
525
+ {
526
+ label: "Helper version",
527
+ value: local.helperVersion
528
+ },
529
+ {
530
+ label: "Service installed",
531
+ value: local.serviceInstalled
532
+ },
533
+ {
534
+ label: "Service running",
535
+ value: local.serviceRunning
536
+ },
537
+ {
538
+ label: "Messages readable",
539
+ value: local.messagesDatabaseReadable
540
+ },
541
+ {
542
+ label: "Sending enabled",
543
+ value: local.allowSend
544
+ },
545
+ {
546
+ label: "Executable",
547
+ value: local.executablePath
548
+ },
549
+ {
550
+ label: "Logs",
551
+ value: local.logPath
552
+ }
553
+ ]);
554
+ } catch (error) {
555
+ printCommandError("Failed to read Runtype Device status", error);
556
+ dependencies.setExitCode(1);
557
+ }
558
+ });
559
+ command.command("doctor").description("Check native helper, background service, and Messages permissions").option("--json", "Output as JSON").action(async (options) => {
560
+ const provider = dependencies.createProvider();
561
+ try {
562
+ const probe = await provider.probe();
563
+ const local = probe.installed && !probe.reason ? await provider.invoke("doctor") : null;
564
+ const healthy = probe.supported && probe.installed && local?.serviceRunning === true && local.messagesDatabaseReadable === true;
565
+ const result = {
566
+ healthy,
567
+ probe,
568
+ local
569
+ };
570
+ if (options.json) printJson(result);
571
+ else {
572
+ console.log(healthy ? chalk.green("Runtype Device is healthy") : chalk.yellow("Runtype Device needs attention"));
573
+ console.log(` Platform supported: ${probe.supported ? "yes" : "no"}`);
574
+ console.log(` Helper installed: ${probe.installed ? "yes" : "no"}`);
575
+ if (probe.reason) console.log(` Helper: ${probe.reason}`);
576
+ if (local) {
577
+ console.log(` Service running: ${local.serviceRunning === true ? "yes" : "no"}`);
578
+ console.log(` Messages readable: ${local.messagesDatabaseReadable === true ? "yes" : "no"}`);
579
+ if (local.messagesDatabaseReadable !== true) console.log(chalk.yellow(` Grant Full Disk Access to ${String(local.executablePath)}`));
580
+ }
581
+ }
582
+ if (!healthy) dependencies.setExitCode(1);
583
+ } catch (error) {
584
+ printCommandError("Runtype Device diagnostics failed", error);
585
+ dependencies.setExitCode(1);
586
+ }
587
+ });
588
+ command.command("policy <mode>").description("Set local iMessage send policy (allow-send or deny-send)").option("--json", "Output as JSON").action(async (mode, options) => {
589
+ if (mode !== "allow-send" && mode !== "deny-send") {
590
+ console.error(chalk.red("Policy must be allow-send or deny-send"));
591
+ dependencies.setExitCode(1);
592
+ return;
593
+ }
594
+ const provider = dependencies.createProvider();
595
+ let policyUpdated = false;
596
+ try {
597
+ const policy = await provider.invoke("policy", { allowSend: mode === "allow-send" });
598
+ policyUpdated = true;
599
+ const service = (await provider.invoke("status")).serviceInstalled === true ? await provider.invoke("install") : null;
600
+ const result = {
601
+ ...policy,
602
+ capabilitiesRepublished: service !== null,
603
+ service
604
+ };
605
+ if (options.json) printJson(result);
606
+ else {
607
+ console.log(chalk.green(`Local iMessage sending is ${mode === "allow-send" ? "enabled" : "disabled"}`));
608
+ if (service) console.log(chalk.gray("Restarted the device service to publish capabilities."));
609
+ else console.log(chalk.yellow("The device service is not installed; run `runtype devices install` to publish this policy."));
610
+ }
611
+ } catch (error) {
612
+ printCommandError(policyUpdated ? "Local policy was updated, but the device service could not publish it" : "Failed to update local device policy", error);
613
+ dependencies.setExitCode(1);
614
+ }
615
+ });
616
+ command.command("mcp-url <id>").description("Print the managed MCP endpoint for a paired device").option("--json", "Output as JSON").action(async (id, options) => {
617
+ try {
618
+ const client = await authenticatedClient(dependencies);
619
+ if (!client) return;
620
+ const device = unwrapData(await client.get(`/devices/${encodeURIComponent(id)}`));
621
+ if (options.json) printJson({
622
+ deviceId: id,
623
+ mcpUrl: device.mcpUrl ?? null
624
+ });
625
+ else if (device.mcpUrl) console.log(device.mcpUrl);
626
+ else throw new Error("The API did not return an MCP URL for this device");
627
+ } catch (error) {
628
+ printCommandError("Failed to get device MCP URL", error);
629
+ dependencies.setExitCode(1);
630
+ }
631
+ });
632
+ command.command("revoke <id>").description("Revoke a paired device in Runtype").option("--yes", "Confirm irreversible device revocation").option("--json", "Output as JSON").action(async (id, options) => {
633
+ if (!options.yes) {
634
+ console.error(chalk.red("Pass --yes to confirm irreversible device revocation"));
635
+ dependencies.setExitCode(1);
636
+ return;
637
+ }
638
+ try {
639
+ const client = await authenticatedClient(dependencies);
640
+ if (!client) return;
641
+ const device = unwrapData(await client.post(`/devices/${encodeURIComponent(id)}/revoke`, {}));
642
+ if (options.json) printJson(device);
643
+ else {
644
+ console.log(chalk.green(`Revoked ${device.name} (${device.id})`));
645
+ console.log(chalk.gray("If this is the local Mac, run `runtype devices reset --yes`."));
646
+ }
647
+ } catch (error) {
648
+ if (isRecordedRevocationError(error)) {
649
+ const result = {
650
+ deviceId: id,
651
+ revoked: true,
652
+ relayDisconnectConfirmed: false,
653
+ message: errorMessage(error)
654
+ };
655
+ if (options.json) printJson(result);
656
+ else {
657
+ console.error(chalk.yellow(`Revocation was recorded for ${id}, but relay disconnection was not confirmed.`));
658
+ console.error(chalk.yellow("Retry this command, and reset the local Mac if applicable."));
659
+ }
660
+ dependencies.setExitCode(1);
661
+ return;
662
+ }
663
+ printCommandError("Failed to revoke Runtype device", error);
664
+ dependencies.setExitCode(1);
665
+ }
666
+ });
667
+ command.command("logs").description("Read local Runtype Device logs").option("-n, --lines <count>", "Number of lines", "100").option("-f, --follow", "Follow the log file").action(async (options) => {
668
+ try {
669
+ const lines = Number.parseInt(options.lines, 10);
670
+ if (!Number.isInteger(lines) || lines < 1 || lines > 1e4) throw new Error("--lines must be an integer between 1 and 10000");
671
+ const status = await dependencies.createProvider().invoke("status");
672
+ if (typeof status.logPath !== "string") throw new Error("The helper did not return a log path");
673
+ if (options.follow) {
674
+ const code = await runTail(status.logPath, lines);
675
+ if (code !== 0) dependencies.setExitCode(code);
676
+ return;
677
+ }
678
+ const entries = (await readFile(status.logPath, "utf8").catch((error) => {
679
+ if ((isRecord(error) ? error.code : void 0) === "ENOENT") return "";
680
+ throw error;
681
+ })).split(/\r?\n/u);
682
+ if (entries.at(-1) === "") entries.pop();
683
+ const selected = entries.slice(-lines).join("\n");
684
+ if (selected) console.log(selected);
685
+ else console.log(chalk.gray("No local device logs yet"));
686
+ } catch (error) {
687
+ printCommandError("Failed to read Runtype Device logs", error);
688
+ dependencies.setExitCode(1);
689
+ }
690
+ });
691
+ command.command("uninstall").description("Stop the local background service while preserving pairing state").option("--remove-binary", "Also remove the CLI-managed helper binary").option("--json", "Output as JSON").action(async (options) => {
692
+ const provider = dependencies.createProvider();
693
+ try {
694
+ const service = await provider.invoke("uninstall");
695
+ const removedBinary = options.removeBinary ? await provider.removeInstalledBinary() : false;
696
+ if (options.json) printJson({
697
+ service,
698
+ removedBinary
699
+ });
700
+ else console.log(chalk.green("Stopped and uninstalled the local Runtype Device service"));
701
+ } catch (error) {
702
+ printCommandError("Failed to uninstall Runtype Device", error);
703
+ dependencies.setExitCode(1);
704
+ }
705
+ });
706
+ command.command("reset").description("Clear local pairing state and rotate the native signing identity").option("--yes", "Confirm destructive local reset").option("--json", "Output as JSON").action(async (options) => {
707
+ if (!options.yes) {
708
+ console.error(chalk.red("Pass --yes to confirm destructive local reset"));
709
+ dependencies.setExitCode(1);
710
+ return;
711
+ }
712
+ try {
713
+ const result = await dependencies.createProvider().invoke("reset", { rotateIdentity: true });
714
+ if (options.json) printJson(result);
715
+ else {
716
+ console.log(chalk.green("Cleared local device state and rotated the signing identity"));
717
+ console.log(chalk.gray("Revoke the old device in Runtype if it has not already been revoked."));
718
+ }
719
+ } catch (error) {
720
+ printCommandError("Failed to reset local Runtype Device state", error);
721
+ dependencies.setExitCode(1);
722
+ }
723
+ });
724
+ return command;
725
+ }
726
+ const devicesCommand = createDevicesCommand();
727
+ export { devicesCommand };
728
+