@slock-ai/computer 0.0.16 → 0.0.17

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/lib/index.js CHANGED
@@ -31,73 +31,253 @@ var StateReaderError = class extends Error {
31
31
  this.code = code;
32
32
  }
33
33
  };
34
- var MIGRATION_DETECTION_KINDS = ["match", "no-match", "ambiguous"];
34
+ var MIGRATION_DETECTION_KINDS = ["candidates", "server-unavailable"];
35
35
  function isMigrationDetectionKind(value) {
36
36
  return typeof value === "string" && MIGRATION_DETECTION_KINDS.includes(value);
37
37
  }
38
38
 
39
39
  // src/lib/migration.ts
40
- import { readdir, readFile } from "fs/promises";
41
- import { join } from "path";
40
+ import { readdir, readFile, stat } from "fs/promises";
41
+ import { join, resolve } from "path";
42
42
  var MACHINE_DIR_PREFIX = "machine-";
43
- async function readOwnerEvidence(installRoot, machineDirName) {
44
- const ownerFile = join(
45
- installRoot,
46
- "machines",
47
- machineDirName,
48
- "daemon.lock",
49
- "owner.json"
50
- );
43
+ var FINGERPRINT_HEX_RE = /^[0-9a-f]{16}$/;
44
+ async function readLocalOwners(installRoot) {
45
+ const machinesDir = join(installRoot, "machines");
46
+ let entries;
47
+ try {
48
+ entries = await readdir(machinesDir);
49
+ } catch {
50
+ return [];
51
+ }
52
+ const owners = [];
53
+ for (const name of entries) {
54
+ if (!name.startsWith(MACHINE_DIR_PREFIX)) continue;
55
+ const ownerPath = join(machinesDir, name, "daemon.lock", "owner.json");
56
+ let raw;
57
+ try {
58
+ raw = await readFile(ownerPath, "utf8");
59
+ } catch {
60
+ continue;
61
+ }
62
+ let parsed;
63
+ try {
64
+ parsed = JSON.parse(raw);
65
+ } catch {
66
+ continue;
67
+ }
68
+ const fp = parsed.apiKeyFingerprint;
69
+ if (typeof fp !== "string" || !FINGERPRINT_HEX_RE.test(fp)) continue;
70
+ owners.push({ apiKeyFingerprint: fp, localPath: ownerPath });
71
+ }
72
+ return owners;
73
+ }
74
+ function indexLocalByFingerprint(owners) {
75
+ const map = /* @__PURE__ */ new Map();
76
+ for (const owner of owners) {
77
+ if (!map.has(owner.apiKeyFingerprint)) {
78
+ map.set(owner.apiKeyFingerprint, owner);
79
+ }
80
+ }
81
+ return map;
82
+ }
83
+ function intersect(localByFp, roster) {
84
+ const out = [];
85
+ for (const entry of roster) {
86
+ const local = localByFp.get(entry.apiKeyFingerprint);
87
+ if (!local) continue;
88
+ out.push({
89
+ apiKeyFingerprint: entry.apiKeyFingerprint,
90
+ daemonId: entry.daemonId,
91
+ localPath: local.localPath,
92
+ machineName: entry.machineName,
93
+ ...entry.hostname ? { hostname: entry.hostname } : {},
94
+ ...entry.lastSeenAt ? { lastSeenAt: entry.lastSeenAt } : {}
95
+ });
96
+ }
97
+ out.sort((a, b) => {
98
+ const aSeen = a.lastSeenAt ?? "";
99
+ const bSeen = b.lastSeenAt ?? "";
100
+ if (aSeen !== bSeen) {
101
+ if (aSeen === "") return 1;
102
+ if (bSeen === "") return -1;
103
+ return aSeen < bSeen ? 1 : -1;
104
+ }
105
+ return a.apiKeyFingerprint < b.apiKeyFingerprint ? -1 : a.apiKeyFingerprint > b.apiKeyFingerprint ? 1 : 0;
106
+ });
107
+ return out;
108
+ }
109
+ async function detectLegacyMigration(installRoot, serverSlug, client) {
110
+ const localOwners = await readLocalOwners(installRoot);
111
+ if (localOwners.length === 0) {
112
+ return { kind: "candidates", candidates: [] };
113
+ }
114
+ const result = await client.list(serverSlug);
115
+ if (result.status !== "success") {
116
+ return { kind: "server-unavailable" };
117
+ }
118
+ const localByFp = indexLocalByFingerprint(localOwners);
119
+ return { kind: "candidates", candidates: intersect(localByFp, result.entries) };
120
+ }
121
+ async function validateManualMigratePath(inputPath, roster) {
122
+ const absInput = resolve(inputPath);
123
+ let stats;
124
+ try {
125
+ stats = await stat(absInput);
126
+ } catch {
127
+ return { ok: false, code: "MIGRATE_FROM_NOT_FOUND" };
128
+ }
129
+ const ownerPath = stats.isDirectory() ? join(absInput, "daemon.lock", "owner.json") : absInput;
130
+ const candidateOwnerPath = await firstReadableOwner([
131
+ ownerPath,
132
+ join(absInput, "owner.json"),
133
+ absInput
134
+ ]);
135
+ if (!candidateOwnerPath) {
136
+ return { ok: false, code: "MIGRATE_FROM_INVALID" };
137
+ }
51
138
  let raw;
52
139
  try {
53
- raw = await readFile(ownerFile, "utf8");
140
+ raw = await readFile(candidateOwnerPath, "utf8");
54
141
  } catch {
55
- return {};
142
+ return { ok: false, code: "MIGRATE_FROM_INVALID" };
56
143
  }
57
144
  let parsed;
58
145
  try {
59
146
  parsed = JSON.parse(raw);
60
147
  } catch {
61
- return {};
148
+ return { ok: false, code: "MIGRATE_FROM_INVALID" };
62
149
  }
63
- const machineName = typeof parsed.hostname === "string" && parsed.hostname.length > 0 ? parsed.hostname : void 0;
64
- const serverUrl = typeof parsed.serverUrl === "string" && parsed.serverUrl.length > 0 ? parsed.serverUrl : void 0;
65
- return { machineName, serverUrl };
150
+ const fp = parsed.apiKeyFingerprint;
151
+ if (typeof fp !== "string" || !FINGERPRINT_HEX_RE.test(fp)) {
152
+ return { ok: false, code: "MIGRATE_FROM_INVALID" };
153
+ }
154
+ const match = roster.find((r) => r.apiKeyFingerprint === fp);
155
+ if (!match) {
156
+ return { ok: false, code: "MIGRATE_FROM_NOT_OWNED" };
157
+ }
158
+ return {
159
+ ok: true,
160
+ candidate: {
161
+ apiKeyFingerprint: match.apiKeyFingerprint,
162
+ daemonId: match.daemonId,
163
+ localPath: candidateOwnerPath,
164
+ machineName: match.machineName,
165
+ ...match.hostname ? { hostname: match.hostname } : {},
166
+ ...match.lastSeenAt ? { lastSeenAt: match.lastSeenAt } : {}
167
+ }
168
+ };
66
169
  }
67
- async function detectLegacyMigration(installRoot, loggedInUserId) {
68
- void loggedInUserId;
69
- const machinesDir = join(installRoot, "machines");
70
- let entries;
71
- try {
72
- entries = await readdir(machinesDir);
73
- } catch {
74
- return { kind: "no-match" };
170
+ async function firstReadableOwner(paths) {
171
+ const seen = /* @__PURE__ */ new Set();
172
+ for (const p of paths) {
173
+ if (seen.has(p)) continue;
174
+ seen.add(p);
175
+ try {
176
+ const st = await stat(p);
177
+ if (st.isFile()) return p;
178
+ } catch {
179
+ }
75
180
  }
76
- const candidates = [];
77
- for (const name of entries) {
78
- if (!name.startsWith(MACHINE_DIR_PREFIX)) continue;
79
- const evidence = await readOwnerEvidence(installRoot, name);
80
- candidates.push({
81
- machineId: name,
82
- machineName: evidence.machineName,
83
- serverUrl: evidence.serverUrl
181
+ return null;
182
+ }
183
+
184
+ // src/apiClient.ts
185
+ import { fetch } from "undici";
186
+ var LegacyMachinesClient = class {
187
+ constructor(baseUrl, accessToken) {
188
+ this.baseUrl = baseUrl;
189
+ this.accessToken = accessToken;
190
+ }
191
+ url(p) {
192
+ return new URL(p, this.baseUrl).toString();
193
+ }
194
+ async list(serverSlug) {
195
+ let res;
196
+ try {
197
+ res = await fetch(
198
+ this.url(`/api/computer/legacy-machines?serverSlug=${encodeURIComponent(serverSlug)}`),
199
+ {
200
+ method: "GET",
201
+ headers: { Authorization: `Bearer ${this.accessToken}` }
202
+ }
203
+ );
204
+ } catch {
205
+ return { status: "error", code: "request_failed" };
206
+ }
207
+ const body = await res.json().catch(() => null);
208
+ if (res.status === 200 && body && Array.isArray(body.entries)) {
209
+ const entries = body.entries.map((raw) => {
210
+ if (!raw || typeof raw !== "object") return null;
211
+ const e = raw;
212
+ if (typeof e.daemonId !== "string" || typeof e.apiKeyFingerprint !== "string" || typeof e.machineName !== "string") {
213
+ return null;
214
+ }
215
+ return {
216
+ daemonId: e.daemonId,
217
+ apiKeyFingerprint: e.apiKeyFingerprint,
218
+ machineName: e.machineName,
219
+ hostname: typeof e.hostname === "string" ? e.hostname : null,
220
+ lastSeenAt: typeof e.lastSeenAt === "string" ? e.lastSeenAt : null
221
+ };
222
+ }).filter((entry) => entry !== null);
223
+ return { status: "success", entries };
224
+ }
225
+ if (res.status === 401) return { status: "auth_required" };
226
+ if (res.status === 403) return { status: "not_authorized" };
227
+ if (res.status === 404) return { status: "disabled" };
228
+ const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
229
+ return { status: "error", code };
230
+ }
231
+ };
232
+ var RunnersClient = class {
233
+ constructor(baseUrl, computerApiKey) {
234
+ this.baseUrl = baseUrl;
235
+ this.computerApiKey = computerApiKey;
236
+ }
237
+ url(p) {
238
+ return new URL(p, this.baseUrl).toString();
239
+ }
240
+ async list() {
241
+ const res = await fetch(this.url("/internal/computer/runners"), {
242
+ method: "GET",
243
+ headers: { Authorization: `Bearer ${this.computerApiKey}` }
84
244
  });
245
+ const body = await res.json().catch(() => null);
246
+ if (res.status === 200 && body && Array.isArray(body.runners)) {
247
+ return {
248
+ status: "success",
249
+ whitelist: Array.isArray(body.whitelist) ? body.whitelist : [],
250
+ runners: body.runners
251
+ };
252
+ }
253
+ if (res.status === 401 || res.status === 403) return { status: "unauthorized" };
254
+ const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
255
+ return { status: "error", code };
85
256
  }
86
- if (candidates.length === 0) return { kind: "no-match" };
87
- if (candidates.length === 1) {
88
- const only = candidates[0];
89
- return {
90
- kind: "match",
91
- machineId: only.machineId,
92
- machineName: only.machineName,
93
- serverUrl: only.serverUrl
94
- };
257
+ async stop(agentId) {
258
+ const res = await fetch(this.url(`/internal/computer/runners/${encodeURIComponent(agentId)}/stop`), {
259
+ method: "POST",
260
+ headers: { Authorization: `Bearer ${this.computerApiKey}`, "Content-Type": "application/json" },
261
+ body: "{}"
262
+ });
263
+ if (res.status === 200) return { status: "success" };
264
+ if (res.status === 404) return { status: "not_found" };
265
+ if (res.status === 401 || res.status === 403) return { status: "unauthorized" };
266
+ const body = await res.json().catch(() => null);
267
+ const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
268
+ return { status: "error", code };
95
269
  }
96
- return { kind: "ambiguous", candidates };
97
- }
270
+ };
98
271
 
99
- // src/status.ts
100
- import { readFile as readFile5 } from "fs/promises";
272
+ // src/setup.ts
273
+ import { chmod as chmod4, mkdir as mkdir9, readFile as readFile8, rename as rename3, rm as rm2, writeFile as writeFile8 } from "fs/promises";
274
+ import { dirname as dirname8 } from "path";
275
+ import { fetch as fetch3 } from "undici";
276
+
277
+ // src/serverState.ts
278
+ import { readFile as readFile2, readdir as readdir2, writeFile, mkdir, unlink, access, chmod } from "fs/promises";
279
+ import { dirname } from "path";
280
+ import { constants as fsConstants } from "fs";
101
281
 
102
282
  // src/paths.ts
103
283
  import { createHash } from "crypto";
@@ -161,9 +341,6 @@ function serviceLogPath(slockHome) {
161
341
  }
162
342
 
163
343
  // src/serverState.ts
164
- import { readFile as readFile2, readdir as readdir2, writeFile, mkdir, unlink, access, chmod } from "fs/promises";
165
- import { dirname } from "path";
166
- import { constants as fsConstants } from "fs";
167
344
  function parseAttachment(raw) {
168
345
  try {
169
346
  const a = JSON.parse(raw);
@@ -214,12 +391,37 @@ async function listServerAttachments(slockHome) {
214
391
  return out;
215
392
  }
216
393
 
217
- // src/internal/process-primitives.ts
218
- import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2, unlink as unlink2 } from "fs/promises";
394
+ // src/services/attach.ts
395
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
219
396
  import { dirname as dirname2 } from "path";
397
+
398
+ // src/services/login.ts
399
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
400
+ import { dirname as dirname3 } from "path";
401
+
402
+ // src/services/adoptLegacy.ts
403
+ import { chmod as chmod3, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4, appendFile, stat as stat2 } from "fs/promises";
404
+ import { createHash as createHash2 } from "crypto";
405
+ import { dirname as dirname4, join as join2 } from "path";
406
+ import { setTimeout as delay } from "timers/promises";
407
+
408
+ // src/service.ts
409
+ import { spawn as spawn2 } from "child_process";
410
+ import { mkdir as mkdir8, readFile as readFile7, writeFile as writeFile7, open, rename as rename2 } from "fs/promises";
411
+ import { dirname as dirname7, join as joinPath } from "path";
412
+ import { fileURLToPath } from "url";
413
+
414
+ // src/cleanup.ts
415
+ import { readdir as readdir3, stat as stat3, unlink as unlink3, rm, rmdir, rename, mkdir as mkdir6 } from "fs/promises";
416
+ import { spawn } from "child_process";
417
+ import { join as join3 } from "path";
418
+
419
+ // src/internal/process-primitives.ts
420
+ import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, unlink as unlink2 } from "fs/promises";
421
+ import { dirname as dirname5 } from "path";
220
422
  async function readPidfileAt(pidfilePath) {
221
423
  try {
222
- const raw = (await readFile3(pidfilePath, "utf8")).trim();
424
+ const raw = (await readFile5(pidfilePath, "utf8")).trim();
223
425
  const pid = Number.parseInt(raw, 10);
224
426
  return Number.isInteger(pid) && pid > 0 ? pid : null;
225
427
  } catch {
@@ -236,6 +438,37 @@ function isProcessAlive(pid) {
236
438
  }
237
439
  }
238
440
 
441
+ // src/cleanup.ts
442
+ var TMP_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
443
+
444
+ // src/health.ts
445
+ import { readFile as readFile6, writeFile as writeFile6, unlink as unlink4, mkdir as mkdir7, appendFile as appendFile2 } from "fs/promises";
446
+ import { dirname as dirname6 } from "path";
447
+ var CRASH_WINDOW_MS = 6e4;
448
+ var DEGRADED_THRESHOLD = 3;
449
+ async function readHealthFile(slockHome, serverId) {
450
+ if (!isValidServerId(serverId)) return { crashes: [] };
451
+ try {
452
+ const raw = await readFile6(serverHealthPath(slockHome, serverId), "utf8");
453
+ const parsed = JSON.parse(raw);
454
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.crashes)) {
455
+ return parsed;
456
+ }
457
+ } catch {
458
+ }
459
+ return { crashes: [] };
460
+ }
461
+ async function isDegraded(slockHome, serverId, nowMs = Date.now()) {
462
+ const file = await readHealthFile(slockHome, serverId);
463
+ if (file.fatalConfig) return true;
464
+ const cutoffMs = nowMs - CRASH_WINDOW_MS;
465
+ const recent = file.crashes.filter((c) => {
466
+ const t = new Date(c.at).getTime();
467
+ return Number.isFinite(t) && t >= cutoffMs;
468
+ });
469
+ return recent.length >= DEGRADED_THRESHOLD;
470
+ }
471
+
239
472
  // src/internal/service-pid-fallback.ts
240
473
  async function findLiveServicePidReadOnly(slockHome, deps = {}) {
241
474
  const readPidfile = deps.readPidfile ?? readPidfileAt;
@@ -271,38 +504,60 @@ async function walkFallback(slockHome, readPidfile, isAlive, clearStale) {
271
504
  };
272
505
  }
273
506
 
274
- // src/health.ts
275
- import { readFile as readFile4, writeFile as writeFile3, unlink as unlink3, mkdir as mkdir3, appendFile } from "fs/promises";
276
- import { dirname as dirname3 } from "path";
277
- var CRASH_WINDOW_MS = 6e4;
278
- var DEGRADED_THRESHOLD = 3;
279
- async function readHealthFile(slockHome, serverId) {
280
- if (!isValidServerId(serverId)) return { crashes: [] };
281
- try {
282
- const raw = await readFile4(serverHealthPath(slockHome, serverId), "utf8");
283
- const parsed = JSON.parse(raw);
284
- if (parsed && typeof parsed === "object" && Array.isArray(parsed.crashes)) {
285
- return parsed;
507
+ // src/services/detach.ts
508
+ import { fetch as fetch2 } from "undici";
509
+
510
+ // src/setup.ts
511
+ var MIGRATION_FRESH_TRIGGERS = [
512
+ "empty-intersection",
513
+ "explicit-zero",
514
+ "eof",
515
+ "non-tty",
516
+ "server-unavailable"
517
+ ];
518
+ async function pickMigrationCandidateFromInput(candidates, read, write) {
519
+ write(
520
+ "Migration: detected legacy daemon(s) on this Computer that match the target server.\n"
521
+ );
522
+ candidates.forEach((c, i) => {
523
+ const host = c.hostname ? ` (${c.hostname})` : "";
524
+ const seen = c.lastSeenAt ? ` last seen ${c.lastSeenAt}` : "";
525
+ write(` ${i + 1}. ${c.machineName}${host}${seen}
526
+ `);
527
+ write(` local: ${c.localPath}
528
+ `);
529
+ });
530
+ write(" 0. Fresh attach (skip migration)\n");
531
+ write(" m. Migrate from a different on-disk path\n");
532
+ while (true) {
533
+ write("Choose [1]: ");
534
+ const { line, eof } = await read();
535
+ if (eof) return { kind: "fresh" };
536
+ const trimmed = line.trim();
537
+ const norm = trimmed.toLowerCase();
538
+ if (norm === "m") {
539
+ write("Path to legacy machine dir or owner.json: ");
540
+ const { line: pathLine } = await read();
541
+ return { kind: "manual", path: pathLine.trim() };
286
542
  }
287
- } catch {
543
+ if (norm.length === 0) return { kind: "candidate", index: 0 };
544
+ if (norm === "0") return { kind: "fresh" };
545
+ const n = Number.parseInt(norm, 10);
546
+ if (Number.isFinite(n) && String(n) === norm && n >= 1 && n <= candidates.length) {
547
+ return { kind: "candidate", index: n - 1 };
548
+ }
549
+ write(
550
+ `Invalid selection "${trimmed}". Enter 1..${candidates.length}, 0, or m.
551
+ `
552
+ );
288
553
  }
289
- return { crashes: [] };
290
- }
291
- async function isDegraded(slockHome, serverId, nowMs = Date.now()) {
292
- const file = await readHealthFile(slockHome, serverId);
293
- if (file.fatalConfig) return true;
294
- const cutoffMs = nowMs - CRASH_WINDOW_MS;
295
- const recent = file.crashes.filter((c) => {
296
- const t = new Date(c.at).getTime();
297
- return Number.isFinite(t) && t >= cutoffMs;
298
- });
299
- return recent.length >= DEGRADED_THRESHOLD;
300
554
  }
301
555
 
302
556
  // src/status.ts
557
+ import { readFile as readFile9 } from "fs/promises";
303
558
  async function readUserSession(path2) {
304
559
  try {
305
- const parsed = JSON.parse(await readFile5(path2, "utf8"));
560
+ const parsed = JSON.parse(await readFile9(path2, "utf8"));
306
561
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
307
562
  return { state: "present", session: parsed, error: null };
308
563
  }
@@ -368,48 +623,6 @@ async function buildStatusReport(installRoot) {
368
623
  };
369
624
  }
370
625
 
371
- // src/apiClient.ts
372
- import { fetch } from "undici";
373
- var RunnersClient = class {
374
- constructor(baseUrl, computerApiKey) {
375
- this.baseUrl = baseUrl;
376
- this.computerApiKey = computerApiKey;
377
- }
378
- url(p) {
379
- return new URL(p, this.baseUrl).toString();
380
- }
381
- async list() {
382
- const res = await fetch(this.url("/internal/computer/runners"), {
383
- method: "GET",
384
- headers: { Authorization: `Bearer ${this.computerApiKey}` }
385
- });
386
- const body = await res.json().catch(() => null);
387
- if (res.status === 200 && body && Array.isArray(body.runners)) {
388
- return {
389
- status: "success",
390
- whitelist: Array.isArray(body.whitelist) ? body.whitelist : [],
391
- runners: body.runners
392
- };
393
- }
394
- if (res.status === 401 || res.status === 403) return { status: "unauthorized" };
395
- const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
396
- return { status: "error", code };
397
- }
398
- async stop(agentId) {
399
- const res = await fetch(this.url(`/internal/computer/runners/${encodeURIComponent(agentId)}/stop`), {
400
- method: "POST",
401
- headers: { Authorization: `Bearer ${this.computerApiKey}`, "Content-Type": "application/json" },
402
- body: "{}"
403
- });
404
- if (res.status === 200) return { status: "success" };
405
- if (res.status === 404) return { status: "not_found" };
406
- if (res.status === 401 || res.status === 403) return { status: "unauthorized" };
407
- const body = await res.json().catch(() => null);
408
- const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
409
- return { status: "error", code };
410
- }
411
- };
412
-
413
626
  // src/lib/readers.ts
414
627
  async function readServiceStatus(installRoot) {
415
628
  return buildStatusReport(installRoot);
@@ -497,7 +710,7 @@ function isServiceState(value) {
497
710
  }
498
711
 
499
712
  // src/upgradeLog.ts
500
- import { chmod as chmod2, mkdir as mkdir4, open } from "fs/promises";
713
+ import { chmod as chmod5, mkdir as mkdir10, open as open2 } from "fs/promises";
501
714
  var UPGRADE_ERROR_CODES = [
502
715
  "UPGRADE_DEPS_CHANGED",
503
716
  "UPGRADE_NETWORK_FAILED",
@@ -541,7 +754,9 @@ function assertUpgradeLogEntry(entry) {
541
754
  }
542
755
  export {
543
756
  IPC_ERROR_CODES,
757
+ LegacyMachinesClient,
544
758
  MIGRATION_DETECTION_KINDS,
759
+ MIGRATION_FRESH_TRIGGERS,
545
760
  RUNNER_STATE_VALUES,
546
761
  SERVICE_STATE_VALUES,
547
762
  STATE_READER_ERROR_CODES,
@@ -555,6 +770,8 @@ export {
555
770
  isRunnerState,
556
771
  isServiceState,
557
772
  listRunners,
773
+ pickMigrationCandidateFromInput,
558
774
  readRunnerStatus,
559
- readServiceStatus
775
+ readServiceStatus,
776
+ validateManualMigratePath
560
777
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/computer",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "Slock Computer — standalone human/local-machine control-plane CLI (login + attach). Distinct from the agent-facing @slock-ai/cli.",
5
5
  "type": "module",
6
6
  "bin": {