@slock-ai/computer 0.0.14 → 0.0.15

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
@@ -2,6 +2,7 @@
2
2
  var IPC_ERROR_CODES = [
3
3
  "IPC_FRAME_TOO_LARGE",
4
4
  "IPC_PROTOCOL_VERSION_UNSUPPORTED",
5
+ "IPC_PROTOCOL_HANDSHAKE_FAILED",
5
6
  "IPC_HEARTBEAT_TIMEOUT",
6
7
  "IPC_MALFORMED_FRAME",
7
8
  "IPC_REQUEST_TIMEOUT",
@@ -21,6 +22,402 @@ var ServiceClientError = class extends Error {
21
22
  if (cause !== void 0) this.cause = cause;
22
23
  }
23
24
  };
25
+ var STATE_READER_ERROR_CODES = ["NOT_ATTACHED", "INVALID_ATTACHMENT"];
26
+ var StateReaderError = class extends Error {
27
+ code;
28
+ constructor(code, message) {
29
+ super(message);
30
+ this.name = "StateReaderError";
31
+ this.code = code;
32
+ }
33
+ };
34
+ var MIGRATION_DETECTION_KINDS = ["match", "no-match", "ambiguous"];
35
+ function isMigrationDetectionKind(value) {
36
+ return typeof value === "string" && MIGRATION_DETECTION_KINDS.includes(value);
37
+ }
38
+
39
+ // src/lib/migration.ts
40
+ import { readdir, readFile } from "fs/promises";
41
+ import { join } from "path";
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
+ );
51
+ let raw;
52
+ try {
53
+ raw = await readFile(ownerFile, "utf8");
54
+ } catch {
55
+ return {};
56
+ }
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(raw);
60
+ } catch {
61
+ return {};
62
+ }
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 };
66
+ }
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" };
75
+ }
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
84
+ });
85
+ }
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
+ };
95
+ }
96
+ return { kind: "ambiguous", candidates };
97
+ }
98
+
99
+ // src/status.ts
100
+ import { readFile as readFile5 } from "fs/promises";
101
+
102
+ // src/paths.ts
103
+ import { createHash } from "crypto";
104
+ import os from "os";
105
+ import path from "path";
106
+ function computerDir(slockHome) {
107
+ return path.join(slockHome, "computer");
108
+ }
109
+ function userSessionPath(slockHome) {
110
+ return path.join(computerDir(slockHome), "user-session.json");
111
+ }
112
+ var SERVER_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
113
+ function isValidServerId(serverId) {
114
+ return SERVER_ID_RE.test(serverId);
115
+ }
116
+ function assertValidServerId(serverId) {
117
+ if (!isValidServerId(serverId)) {
118
+ throw new Error(`invalid server id: ${JSON.stringify(serverId)} (expected a UUID)`);
119
+ }
120
+ return serverId;
121
+ }
122
+ function serversDir(slockHome) {
123
+ return path.join(computerDir(slockHome), "servers");
124
+ }
125
+ function serverDir(slockHome, serverId) {
126
+ return path.join(serversDir(slockHome), assertValidServerId(serverId));
127
+ }
128
+ function serverAttachmentPath(slockHome, serverId) {
129
+ return path.join(serverDir(slockHome, serverId), "attachment.json");
130
+ }
131
+ function serverDaemonPidPath(slockHome, serverId) {
132
+ return path.join(serverDir(slockHome, serverId), "daemon.pid");
133
+ }
134
+ function serverDaemonLogPath(slockHome, serverId) {
135
+ return path.join(serverDir(slockHome, serverId), "daemon.log");
136
+ }
137
+ function serverHealthPath(slockHome, serverId) {
138
+ return path.join(serverDir(slockHome, serverId), "health.json");
139
+ }
140
+ function supervisorPidPath(slockHome) {
141
+ return path.join(computerDir(slockHome), "supervisor.pid");
142
+ }
143
+ function supervisorLogPath(slockHome) {
144
+ return path.join(computerDir(slockHome), "supervisor.log");
145
+ }
146
+
147
+ // src/serverState.ts
148
+ import { readFile as readFile2, readdir as readdir2, writeFile, mkdir, unlink, access, chmod } from "fs/promises";
149
+ import { dirname } from "path";
150
+ import { constants as fsConstants } from "fs";
151
+ function parseAttachment(raw) {
152
+ try {
153
+ const a = JSON.parse(raw);
154
+ if (a.kind === "computer-attachment" && typeof a.serverId === "string" && typeof a.serverMachineId === "string" && typeof a.apiKey === "string" && a.apiKey.length > 0 && typeof a.serverUrl === "string") {
155
+ return {
156
+ kind: "computer-attachment",
157
+ serverId: a.serverId,
158
+ serverSlug: typeof a.serverSlug === "string" && a.serverSlug.length > 0 ? a.serverSlug : void 0,
159
+ serverMachineId: a.serverMachineId,
160
+ apiKey: a.apiKey,
161
+ serverUrl: a.serverUrl,
162
+ attachedAt: typeof a.attachedAt === "string" ? a.attachedAt : void 0
163
+ };
164
+ }
165
+ } catch {
166
+ }
167
+ return null;
168
+ }
169
+ async function readServerAttachment(slockHome, serverId) {
170
+ if (!isValidServerId(serverId)) return null;
171
+ try {
172
+ return parseAttachment(await readFile2(serverAttachmentPath(slockHome, serverId), "utf8"));
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+ async function listAttachedServerIds(slockHome) {
178
+ let entries;
179
+ try {
180
+ entries = await readdir2(serversDir(slockHome));
181
+ } catch {
182
+ return [];
183
+ }
184
+ const ids = [];
185
+ for (const name of entries) {
186
+ if (!isValidServerId(name)) continue;
187
+ if (await readServerAttachment(slockHome, name)) ids.push(name);
188
+ }
189
+ return ids.sort();
190
+ }
191
+ async function listServerAttachments(slockHome) {
192
+ const ids = await listAttachedServerIds(slockHome);
193
+ const out = [];
194
+ for (const id of ids) {
195
+ const a = await readServerAttachment(slockHome, id);
196
+ if (a) out.push(a);
197
+ }
198
+ return out;
199
+ }
200
+
201
+ // src/internal/process-primitives.ts
202
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2, unlink as unlink2 } from "fs/promises";
203
+ import { dirname as dirname2 } from "path";
204
+ async function readPidfileAt(pidfilePath) {
205
+ try {
206
+ const raw = (await readFile3(pidfilePath, "utf8")).trim();
207
+ const pid = Number.parseInt(raw, 10);
208
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ function isProcessAlive(pid) {
214
+ if (!Number.isInteger(pid) || pid <= 0) return false;
215
+ try {
216
+ process.kill(pid, 0);
217
+ return true;
218
+ } catch (err) {
219
+ return err.code === "EPERM";
220
+ }
221
+ }
222
+
223
+ // src/health.ts
224
+ import { readFile as readFile4, writeFile as writeFile3, unlink as unlink3, mkdir as mkdir3, appendFile } from "fs/promises";
225
+ import { dirname as dirname3 } from "path";
226
+ var CRASH_WINDOW_MS = 6e4;
227
+ var DEGRADED_THRESHOLD = 3;
228
+ async function readHealthFile(slockHome, serverId) {
229
+ if (!isValidServerId(serverId)) return { crashes: [] };
230
+ try {
231
+ const raw = await readFile4(serverHealthPath(slockHome, serverId), "utf8");
232
+ const parsed = JSON.parse(raw);
233
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.crashes)) {
234
+ return parsed;
235
+ }
236
+ } catch {
237
+ }
238
+ return { crashes: [] };
239
+ }
240
+ async function isDegraded(slockHome, serverId, nowMs = Date.now()) {
241
+ const file = await readHealthFile(slockHome, serverId);
242
+ if (file.fatalConfig) return true;
243
+ const cutoffMs = nowMs - CRASH_WINDOW_MS;
244
+ const recent = file.crashes.filter((c) => {
245
+ const t = new Date(c.at).getTime();
246
+ return Number.isFinite(t) && t >= cutoffMs;
247
+ });
248
+ return recent.length >= DEGRADED_THRESHOLD;
249
+ }
250
+
251
+ // src/status.ts
252
+ async function readUserSession(path2) {
253
+ try {
254
+ const parsed = JSON.parse(await readFile5(path2, "utf8"));
255
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
256
+ return { state: "present", session: parsed, error: null };
257
+ }
258
+ return { state: "invalid", session: null, error: "not a JSON object" };
259
+ } catch (err) {
260
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
261
+ return { state: "missing", session: null, error: null };
262
+ }
263
+ return {
264
+ state: "invalid",
265
+ session: null,
266
+ error: err instanceof Error ? err.message : String(err)
267
+ };
268
+ }
269
+ }
270
+ function str(v) {
271
+ return typeof v === "string" && v.length > 0 ? v : null;
272
+ }
273
+ async function pidStatus(pidfile) {
274
+ const pid = await readPidfileAt(pidfile);
275
+ return pid !== null && isProcessAlive(pid) ? { running: true, pid } : { running: false };
276
+ }
277
+ async function deriveHealth(slockHome, serverId, daemon) {
278
+ if (!daemon.running) return "offline";
279
+ if (await isDegraded(slockHome, serverId)) return "degraded";
280
+ return "ok";
281
+ }
282
+ async function buildStatusReport(installRoot) {
283
+ const sessionRead = await readUserSession(userSessionPath(installRoot));
284
+ const session = sessionRead.session;
285
+ const attachments = await listServerAttachments(installRoot);
286
+ const supervisor = {
287
+ ...await pidStatus(supervisorPidPath(installRoot)),
288
+ logPath: supervisorLogPath(installRoot)
289
+ };
290
+ const servers = [];
291
+ for (const a of attachments) {
292
+ const daemon = await pidStatus(serverDaemonPidPath(installRoot, a.serverId));
293
+ servers.push({
294
+ serverId: a.serverId,
295
+ serverSlug: a.serverSlug ?? null,
296
+ serverMachineId: a.serverMachineId,
297
+ serverUrl: a.serverUrl,
298
+ attachedAt: a.attachedAt ?? null,
299
+ daemonLogPath: serverDaemonLogPath(installRoot, a.serverId),
300
+ daemon,
301
+ health: await deriveHealth(installRoot, a.serverId, daemon)
302
+ });
303
+ }
304
+ const loggedIn = session?.kind === "user-session" && typeof session.accessToken === "string" && session.accessToken.length > 0;
305
+ return {
306
+ slockHome: installRoot,
307
+ loggedIn,
308
+ userId: session ? str(session.userId) : null,
309
+ loginServerUrl: session ? str(session.serverUrl) : null,
310
+ userSessionError: sessionRead.state === "invalid" ? sessionRead.error : null,
311
+ supervisor,
312
+ servers
313
+ };
314
+ }
315
+
316
+ // src/apiClient.ts
317
+ import { fetch } from "undici";
318
+ var RunnersClient = class {
319
+ constructor(baseUrl, computerApiKey) {
320
+ this.baseUrl = baseUrl;
321
+ this.computerApiKey = computerApiKey;
322
+ }
323
+ url(p) {
324
+ return new URL(p, this.baseUrl).toString();
325
+ }
326
+ async list() {
327
+ const res = await fetch(this.url("/internal/computer/runners"), {
328
+ method: "GET",
329
+ headers: { Authorization: `Bearer ${this.computerApiKey}` }
330
+ });
331
+ const body = await res.json().catch(() => null);
332
+ if (res.status === 200 && body && Array.isArray(body.runners)) {
333
+ return {
334
+ status: "success",
335
+ whitelist: Array.isArray(body.whitelist) ? body.whitelist : [],
336
+ runners: body.runners
337
+ };
338
+ }
339
+ if (res.status === 401 || res.status === 403) return { status: "unauthorized" };
340
+ const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
341
+ return { status: "error", code };
342
+ }
343
+ async stop(agentId) {
344
+ const res = await fetch(this.url(`/internal/computer/runners/${encodeURIComponent(agentId)}/stop`), {
345
+ method: "POST",
346
+ headers: { Authorization: `Bearer ${this.computerApiKey}`, "Content-Type": "application/json" },
347
+ body: "{}"
348
+ });
349
+ if (res.status === 200) return { status: "success" };
350
+ if (res.status === 404) return { status: "not_found" };
351
+ if (res.status === 401 || res.status === 403) return { status: "unauthorized" };
352
+ const body = await res.json().catch(() => null);
353
+ const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
354
+ return { status: "error", code };
355
+ }
356
+ };
357
+
358
+ // src/lib/readers.ts
359
+ async function readServiceStatus(installRoot) {
360
+ return buildStatusReport(installRoot);
361
+ }
362
+ async function readRunnerStatus(installRoot, serverId) {
363
+ const report = await buildStatusReport(installRoot);
364
+ const server = report.servers.find((s) => s.serverId === serverId);
365
+ if (!server) {
366
+ throw new StateReaderError(
367
+ "NOT_ATTACHED",
368
+ `Server ${serverId} is not attached to this Computer.`
369
+ );
370
+ }
371
+ const attachment = await readServerAttachment(installRoot, serverId);
372
+ if (!attachment) {
373
+ throw new StateReaderError(
374
+ "INVALID_ATTACHMENT",
375
+ `Attachment for server ${serverId} is missing or invalid.`
376
+ );
377
+ }
378
+ const client = new RunnersClient(attachment.serverUrl, attachment.apiKey);
379
+ const result = await client.list();
380
+ if (result.status === "success") {
381
+ return { status: "ok", server, whitelist: result.whitelist, runners: result.runners };
382
+ }
383
+ if (result.status === "unauthorized") {
384
+ return { status: "unauthorized", server };
385
+ }
386
+ return { status: "error", server, code: result.code };
387
+ }
388
+ async function listRunners(installRoot, opts = {}) {
389
+ const all = await listServerAttachments(installRoot);
390
+ let subset = all;
391
+ if (opts.serverId !== void 0) {
392
+ const found = all.find((a) => a.serverId === opts.serverId);
393
+ if (!found) {
394
+ throw new StateReaderError(
395
+ "NOT_ATTACHED",
396
+ `Server ${opts.serverId} is not attached to this Computer.`
397
+ );
398
+ }
399
+ subset = [found];
400
+ }
401
+ const servers = [];
402
+ for (const a of subset) {
403
+ const client = new RunnersClient(a.serverUrl, a.apiKey);
404
+ const result = await client.list();
405
+ const idCols = { serverId: a.serverId, serverSlug: a.serverSlug ?? null };
406
+ if (result.status === "success") {
407
+ servers.push({
408
+ ...idCols,
409
+ status: "ok",
410
+ whitelist: result.whitelist,
411
+ runners: result.runners
412
+ });
413
+ } else if (result.status === "unauthorized") {
414
+ servers.push({ ...idCols, status: "unauthorized" });
415
+ } else {
416
+ servers.push({ ...idCols, status: "error", code: result.code });
417
+ }
418
+ }
419
+ return { servers };
420
+ }
24
421
 
25
422
  // src/lib/state.ts
26
423
  var RUNNER_STATE_VALUES = [
@@ -45,14 +442,7 @@ function isServiceState(value) {
45
442
  }
46
443
 
47
444
  // src/upgradeLog.ts
48
- import { chmod, mkdir, open } from "fs/promises";
49
-
50
- // src/paths.ts
51
- import { createHash } from "crypto";
52
- import os from "os";
53
- import path from "path";
54
-
55
- // src/upgradeLog.ts
445
+ import { chmod as chmod2, mkdir as mkdir4, open } from "fs/promises";
56
446
  var UPGRADE_ERROR_CODES = [
57
447
  "UPGRADE_DEPS_CHANGED",
58
448
  "UPGRADE_NETWORK_FAILED",
@@ -96,12 +486,20 @@ function assertUpgradeLogEntry(entry) {
96
486
  }
97
487
  export {
98
488
  IPC_ERROR_CODES,
489
+ MIGRATION_DETECTION_KINDS,
99
490
  RUNNER_STATE_VALUES,
100
491
  SERVICE_STATE_VALUES,
492
+ STATE_READER_ERROR_CODES,
101
493
  ServiceClientError,
494
+ StateReaderError,
102
495
  UPGRADE_ERROR_CODES,
103
496
  assertUpgradeLogEntry,
497
+ detectLegacyMigration,
104
498
  isIpcErrorCode,
499
+ isMigrationDetectionKind,
105
500
  isRunnerState,
106
- isServiceState
501
+ isServiceState,
502
+ listRunners,
503
+ readRunnerStatus,
504
+ readServiceStatus
107
505
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/computer",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
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": {
@@ -28,7 +28,7 @@
28
28
  "commander": "^12.1.0",
29
29
  "proper-lockfile": "^4.1.2",
30
30
  "undici": "^7.24.7",
31
- "@slock-ai/daemon": "0.55.0"
31
+ "@slock-ai/daemon": "0.55.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^25.5.0",