@slock-ai/computer 0.0.1-play.20260511091124
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/index.js +556 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/proxy.ts
|
|
7
|
+
import { ProxyAgent } from "undici";
|
|
8
|
+
var fetchDispatcherCache = /* @__PURE__ */ new Map();
|
|
9
|
+
function getDefaultPort(protocol) {
|
|
10
|
+
switch (protocol) {
|
|
11
|
+
case "https:":
|
|
12
|
+
return "443";
|
|
13
|
+
case "http:":
|
|
14
|
+
return "80";
|
|
15
|
+
default:
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function hostMatchesNoProxyEntry(hostname, ruleHost) {
|
|
20
|
+
if (!ruleHost) return false;
|
|
21
|
+
const normalizedRule = ruleHost.replace(/^\*\./, ".").replace(/^\./, "").toLowerCase();
|
|
22
|
+
const normalizedHost = hostname.toLowerCase();
|
|
23
|
+
return normalizedHost === normalizedRule || normalizedHost.endsWith(`.${normalizedRule}`);
|
|
24
|
+
}
|
|
25
|
+
function getProxyUrlForTarget(targetUrl, env) {
|
|
26
|
+
const protocol = new URL(targetUrl).protocol;
|
|
27
|
+
switch (protocol) {
|
|
28
|
+
case "https:":
|
|
29
|
+
return env.HTTPS_PROXY || env.https_proxy || env.ALL_PROXY || env.all_proxy;
|
|
30
|
+
case "http:":
|
|
31
|
+
return env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy;
|
|
32
|
+
default:
|
|
33
|
+
return env.ALL_PROXY || env.all_proxy;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function shouldBypassProxy(targetUrl, env) {
|
|
37
|
+
const rawNoProxy = env.NO_PROXY || env.no_proxy;
|
|
38
|
+
if (!rawNoProxy) return false;
|
|
39
|
+
const url = new URL(targetUrl);
|
|
40
|
+
const hostname = url.hostname.toLowerCase();
|
|
41
|
+
const port = url.port || getDefaultPort(url.protocol);
|
|
42
|
+
return rawNoProxy.split(",").map((entry) => entry.trim()).filter(Boolean).some((entry) => {
|
|
43
|
+
if (entry === "*") return true;
|
|
44
|
+
const [ruleHost, rulePort] = entry.split(":", 2);
|
|
45
|
+
if (rulePort && rulePort !== port) return false;
|
|
46
|
+
return hostMatchesNoProxyEntry(hostname, ruleHost);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function buildFetchDispatcher(targetUrl, env = process.env) {
|
|
50
|
+
const proxyUrl = getProxyUrlForTarget(targetUrl, env);
|
|
51
|
+
if (!proxyUrl) return void 0;
|
|
52
|
+
if (shouldBypassProxy(targetUrl, env)) return void 0;
|
|
53
|
+
const cached = fetchDispatcherCache.get(proxyUrl);
|
|
54
|
+
if (cached) return cached;
|
|
55
|
+
const dispatcher = new ProxyAgent(proxyUrl);
|
|
56
|
+
fetchDispatcherCache.set(proxyUrl, dispatcher);
|
|
57
|
+
return dispatcher;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/computor/apiClient.ts
|
|
61
|
+
var ComputorApiError = class extends Error {
|
|
62
|
+
constructor(status, code, message) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.status = status;
|
|
65
|
+
this.code = code;
|
|
66
|
+
this.name = "ComputorApiError";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var ComputorHttpClient = class {
|
|
70
|
+
constructor(serverUrl) {
|
|
71
|
+
this.serverUrl = serverUrl;
|
|
72
|
+
}
|
|
73
|
+
async startDeviceAuth(request) {
|
|
74
|
+
return this.requestJson("POST", "/api/auth/device/start", request);
|
|
75
|
+
}
|
|
76
|
+
async pollDeviceToken(deviceCode) {
|
|
77
|
+
const res = await this.fetchJson("POST", "/api/auth/device/token", { deviceCode });
|
|
78
|
+
if (res.ok) {
|
|
79
|
+
const parsed = res.data;
|
|
80
|
+
return { ...parsed, status: "approved" };
|
|
81
|
+
}
|
|
82
|
+
const status = res.data?.status ?? res.code;
|
|
83
|
+
if (res.status === 428 || status === "authorization_pending") return { status: "authorization_pending" };
|
|
84
|
+
if (res.status === 429 || status === "slow_down") {
|
|
85
|
+
const interval = res.data?.interval;
|
|
86
|
+
return { status: "slow_down", interval: typeof interval === "number" ? interval : void 0 };
|
|
87
|
+
}
|
|
88
|
+
if (res.status === 410 || status === "expired_token") return { status: "expired_token" };
|
|
89
|
+
if (res.status === 403 || status === "denied") return { status: "denied" };
|
|
90
|
+
throw new ComputorApiError(res.status, res.code, res.message);
|
|
91
|
+
}
|
|
92
|
+
async refreshSession(refreshToken, targetServerId) {
|
|
93
|
+
return this.requestJson(
|
|
94
|
+
"POST",
|
|
95
|
+
"/api/auth/refresh",
|
|
96
|
+
{ refreshToken },
|
|
97
|
+
void 0,
|
|
98
|
+
targetServerId
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
async getMe(accessToken, targetServerId) {
|
|
102
|
+
return this.requestJson("GET", "/api/auth/me", void 0, accessToken, targetServerId);
|
|
103
|
+
}
|
|
104
|
+
async attach(accessToken, request) {
|
|
105
|
+
return this.requestJson(
|
|
106
|
+
"POST",
|
|
107
|
+
"/api/computors/attach",
|
|
108
|
+
request,
|
|
109
|
+
accessToken,
|
|
110
|
+
request.targetServerId
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
async requestJson(method, pathname, body, accessToken, targetServerId) {
|
|
114
|
+
const res = await this.fetchJson(method, pathname, body, accessToken, targetServerId);
|
|
115
|
+
if (!res.ok) throw new ComputorApiError(res.status, res.code, res.message);
|
|
116
|
+
return res.data;
|
|
117
|
+
}
|
|
118
|
+
async fetchJson(method, pathname, body, accessToken, targetServerId) {
|
|
119
|
+
const url = new URL(pathname, this.serverUrl).toString();
|
|
120
|
+
const headers = { "Content-Type": "application/json" };
|
|
121
|
+
if (accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
122
|
+
if (targetServerId) headers["X-Server-Id"] = targetServerId;
|
|
123
|
+
const dispatcher = buildFetchDispatcher(url);
|
|
124
|
+
const init = {
|
|
125
|
+
method,
|
|
126
|
+
headers,
|
|
127
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
128
|
+
};
|
|
129
|
+
if (dispatcher) init.dispatcher = dispatcher;
|
|
130
|
+
const response = await fetch(url, init);
|
|
131
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
132
|
+
const data = contentType.includes("application/json") ? await response.json().catch(() => null) : null;
|
|
133
|
+
const error = data;
|
|
134
|
+
return {
|
|
135
|
+
ok: response.ok,
|
|
136
|
+
status: response.status,
|
|
137
|
+
data,
|
|
138
|
+
code: error?.errorCode ?? error?.code ?? error?.status ?? `HTTP_${response.status}`,
|
|
139
|
+
message: error?.message ?? error?.error ?? `HTTP ${response.status}`
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/computor/attachFlow.ts
|
|
145
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
146
|
+
|
|
147
|
+
// src/computor/localIdentity.ts
|
|
148
|
+
import { randomUUID } from "crypto";
|
|
149
|
+
import fs from "fs/promises";
|
|
150
|
+
import path2 from "path";
|
|
151
|
+
|
|
152
|
+
// src/computor/paths.ts
|
|
153
|
+
import os from "os";
|
|
154
|
+
import path from "path";
|
|
155
|
+
function resolveSlockHome(env = process.env, homeDir = os.homedir()) {
|
|
156
|
+
const configured = env.SLOCK_HOME?.trim();
|
|
157
|
+
const raw = configured && configured.length > 0 ? configured : path.join(homeDir, ".slock");
|
|
158
|
+
return path.resolve(expandHome(raw, homeDir));
|
|
159
|
+
}
|
|
160
|
+
function expandHome(input, homeDir) {
|
|
161
|
+
if (input === "~") return homeDir;
|
|
162
|
+
if (input.startsWith("~/")) return path.join(homeDir, input.slice(2));
|
|
163
|
+
return input;
|
|
164
|
+
}
|
|
165
|
+
function authDir(slockHome) {
|
|
166
|
+
return path.join(slockHome, "auth");
|
|
167
|
+
}
|
|
168
|
+
function activeSessionPath(slockHome) {
|
|
169
|
+
return path.join(authDir(slockHome), "active");
|
|
170
|
+
}
|
|
171
|
+
function sessionPath(slockHome, humanId) {
|
|
172
|
+
return path.join(authDir(slockHome), "sessions", humanId, "session.json");
|
|
173
|
+
}
|
|
174
|
+
function computersDir(slockHome) {
|
|
175
|
+
return path.join(slockHome, "computers");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/computor/localIdentity.ts
|
|
179
|
+
var LocalIdentityError = class extends Error {
|
|
180
|
+
constructor(code, message) {
|
|
181
|
+
super(message);
|
|
182
|
+
this.code = code;
|
|
183
|
+
this.name = "LocalIdentityError";
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
function getComputorDirectoryName(serverMachineId) {
|
|
187
|
+
if (!serverMachineId || serverMachineId.includes("/") || serverMachineId.includes("\\")) {
|
|
188
|
+
throw new LocalIdentityError("INVALID_SERVER_MACHINE_ID", "serverMachineId is not safe for a local computor path.");
|
|
189
|
+
}
|
|
190
|
+
return `computer-${serverMachineId}`;
|
|
191
|
+
}
|
|
192
|
+
function isLocalComputorState(value) {
|
|
193
|
+
const candidate = value;
|
|
194
|
+
return typeof candidate?.serverMachineId === "string" && typeof candidate.serverId === "string" && (candidate.humanId === void 0 || typeof candidate.humanId === "string");
|
|
195
|
+
}
|
|
196
|
+
async function readComputorState(statePath) {
|
|
197
|
+
let raw;
|
|
198
|
+
try {
|
|
199
|
+
raw = await fs.readFile(statePath, "utf8");
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (err.code === "ENOENT") return null;
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse(raw);
|
|
206
|
+
return isLocalComputorState(parsed) ? parsed : null;
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function writeComputorStateAfterAttach(slockHome, attach) {
|
|
212
|
+
const state = {
|
|
213
|
+
serverMachineId: attach.serverMachineId,
|
|
214
|
+
humanId: attach.humanId,
|
|
215
|
+
serverId: attach.serverId
|
|
216
|
+
};
|
|
217
|
+
const computorDir = path2.join(computersDir(slockHome), getComputorDirectoryName(attach.serverMachineId));
|
|
218
|
+
const statePath = path2.join(computorDir, "state.json");
|
|
219
|
+
const tempPath = path2.join(computorDir, `state.json.${process.pid}.${randomUUID()}.tmp`);
|
|
220
|
+
await fs.mkdir(computorDir, { recursive: true, mode: 448 });
|
|
221
|
+
await fs.writeFile(tempPath, `${JSON.stringify(state, null, 2)}
|
|
222
|
+
`, { mode: 384 });
|
|
223
|
+
await fs.rename(tempPath, statePath);
|
|
224
|
+
return { serverMachineId: state.serverMachineId, statePath, state };
|
|
225
|
+
}
|
|
226
|
+
async function probeExistingServerMachineIdentity(slockHome, targetServerId, authenticatedHumanId) {
|
|
227
|
+
let entries;
|
|
228
|
+
try {
|
|
229
|
+
const dirents = await fs.readdir(computersDir(slockHome), { withFileTypes: true });
|
|
230
|
+
entries = dirents.filter((entry) => entry.isDirectory() && entry.name.startsWith("computer-")).map((entry) => entry.name).sort();
|
|
231
|
+
} catch (err) {
|
|
232
|
+
if (err.code === "ENOENT") return null;
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
const matches = [];
|
|
236
|
+
for (const entry of entries) {
|
|
237
|
+
const statePath = path2.join(computersDir(slockHome), entry, "state.json");
|
|
238
|
+
const state = await readComputorState(statePath);
|
|
239
|
+
if (state?.serverId === targetServerId && state.humanId === authenticatedHumanId) {
|
|
240
|
+
matches.push({ serverMachineId: state.serverMachineId, statePath, state });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (matches.length > 1) {
|
|
244
|
+
throw new LocalIdentityError(
|
|
245
|
+
"MULTIPLE_LOCAL_COMPUTERS",
|
|
246
|
+
`Multiple local computor state files match server ${targetServerId}; refusing to guess which serverMachineId to resume.`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return matches[0] ?? null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/computor/sessionStore.ts
|
|
253
|
+
import fs2 from "fs/promises";
|
|
254
|
+
import path3 from "path";
|
|
255
|
+
async function readJsonFile(filePath) {
|
|
256
|
+
let raw;
|
|
257
|
+
try {
|
|
258
|
+
raw = await fs2.readFile(filePath, "utf8");
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (err.code === "ENOENT") return null;
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
return JSON.parse(raw);
|
|
265
|
+
} catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function writeJsonAtomic(filePath, value) {
|
|
270
|
+
await fs2.mkdir(path3.dirname(filePath), { recursive: true, mode: 448 });
|
|
271
|
+
const tmpPath = path3.join(
|
|
272
|
+
path3.dirname(filePath),
|
|
273
|
+
`.${path3.basename(filePath)}.${process.pid}.${Date.now()}.tmp`
|
|
274
|
+
);
|
|
275
|
+
await fs2.writeFile(tmpPath, `${JSON.stringify(value, null, 2)}
|
|
276
|
+
`, {
|
|
277
|
+
encoding: "utf8",
|
|
278
|
+
mode: 384
|
|
279
|
+
});
|
|
280
|
+
await fs2.rename(tmpPath, filePath);
|
|
281
|
+
}
|
|
282
|
+
function isActiveSessionPointer(value) {
|
|
283
|
+
return typeof value?.humanId === "string";
|
|
284
|
+
}
|
|
285
|
+
function isComputorSession(value) {
|
|
286
|
+
const candidate = value;
|
|
287
|
+
return typeof candidate?.accessToken === "string" && typeof candidate.refreshToken === "string" && typeof candidate.humanId === "string";
|
|
288
|
+
}
|
|
289
|
+
async function readActiveSessionPointer(slockHome) {
|
|
290
|
+
const value = await readJsonFile(activeSessionPath(slockHome));
|
|
291
|
+
return isActiveSessionPointer(value) ? value : null;
|
|
292
|
+
}
|
|
293
|
+
async function readSession(slockHome, humanId) {
|
|
294
|
+
const value = await readJsonFile(sessionPath(slockHome, humanId));
|
|
295
|
+
return isComputorSession(value) ? value : null;
|
|
296
|
+
}
|
|
297
|
+
async function writeSession(slockHome, session) {
|
|
298
|
+
await writeJsonAtomic(sessionPath(slockHome, session.humanId), {
|
|
299
|
+
...session,
|
|
300
|
+
updatedAt: session.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async function writeActiveSessionPointer(slockHome, pointer) {
|
|
304
|
+
await writeJsonAtomic(activeSessionPath(slockHome), pointer);
|
|
305
|
+
}
|
|
306
|
+
async function writeSessionAndActivePointer(slockHome, session, targetServerId) {
|
|
307
|
+
await writeSession(slockHome, session);
|
|
308
|
+
await writeActiveSessionPointer(slockHome, {
|
|
309
|
+
humanId: session.humanId,
|
|
310
|
+
serverId: targetServerId
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/computor/attachFlow.ts
|
|
315
|
+
var DEFAULT_CLIENT_VERSION = "0.0.1";
|
|
316
|
+
var ComputorAttachFlowError = class extends Error {
|
|
317
|
+
constructor(code, message) {
|
|
318
|
+
super(message);
|
|
319
|
+
this.code = code;
|
|
320
|
+
this.name = "ComputorAttachFlowError";
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
async function runComputorAttachFlow(options) {
|
|
324
|
+
const slockHome = options.slockHome ?? resolveSlockHome(options.env, options.homeDir);
|
|
325
|
+
const attachAttemptId = options.generateAttemptId?.() ?? randomUUID2();
|
|
326
|
+
const session = await ensureHumanSession({ ...options, slockHome });
|
|
327
|
+
const existingIdentity = await probeExistingServerMachineIdentity(
|
|
328
|
+
slockHome,
|
|
329
|
+
options.targetServerId,
|
|
330
|
+
session.humanId
|
|
331
|
+
);
|
|
332
|
+
const existingServerMachineId = existingIdentity?.serverMachineId ?? null;
|
|
333
|
+
const attachResult = await attachWithRetry({
|
|
334
|
+
client: options.client,
|
|
335
|
+
session,
|
|
336
|
+
slockHome,
|
|
337
|
+
targetServerId: options.targetServerId,
|
|
338
|
+
existingServerMachineId,
|
|
339
|
+
attachAttemptId,
|
|
340
|
+
clientVersion: options.clientVersion ?? DEFAULT_CLIENT_VERSION,
|
|
341
|
+
wait: options.wait,
|
|
342
|
+
maxAttempts: options.maxAttachAttempts ?? 3
|
|
343
|
+
});
|
|
344
|
+
if (attachResult.attach.serverId !== options.targetServerId) {
|
|
345
|
+
throw new ComputorAttachFlowError(
|
|
346
|
+
"ATTACH_SERVER_MISMATCH",
|
|
347
|
+
`Attach response serverId=${attachResult.attach.serverId} did not match targetServerId=${options.targetServerId}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (existingServerMachineId && attachResult.attach.serverMachineId !== existingServerMachineId) {
|
|
351
|
+
throw new ComputorAttachFlowError(
|
|
352
|
+
"ATTACH_IDENTITY_MISMATCH",
|
|
353
|
+
`Attach resumed serverMachineId=${attachResult.attach.serverMachineId}, expected local state identity ${existingServerMachineId}`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
await options.onPostAttachSuccess?.(attachResult.attach);
|
|
357
|
+
return {
|
|
358
|
+
attachAttemptId,
|
|
359
|
+
slockHome,
|
|
360
|
+
existingServerMachineId,
|
|
361
|
+
session: attachResult.session,
|
|
362
|
+
attach: attachResult.attach
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
async function ensureHumanSession(options) {
|
|
366
|
+
const active = await readActiveSessionPointer(options.slockHome);
|
|
367
|
+
if (!active) return runDeviceLogin(options);
|
|
368
|
+
const session = await readSession(options.slockHome, active.humanId);
|
|
369
|
+
if (!session) return runDeviceLogin(options);
|
|
370
|
+
try {
|
|
371
|
+
await options.client.getMe(session.accessToken, options.targetServerId);
|
|
372
|
+
return session;
|
|
373
|
+
} catch (err) {
|
|
374
|
+
if (!(err instanceof ComputorApiError) || err.status !== 401) {
|
|
375
|
+
throw err;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
const refreshed = await options.client.refreshSession(session.refreshToken, options.targetServerId);
|
|
380
|
+
const nextSession = mergeRefreshedSession(session, refreshed);
|
|
381
|
+
await writeSession(options.slockHome, nextSession);
|
|
382
|
+
return nextSession;
|
|
383
|
+
} catch (err) {
|
|
384
|
+
if (!(err instanceof ComputorApiError) || err.status !== 401) {
|
|
385
|
+
throw err;
|
|
386
|
+
}
|
|
387
|
+
return runDeviceLogin(options);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async function runDeviceLogin(options) {
|
|
391
|
+
const challenge = await options.client.startDeviceAuth({
|
|
392
|
+
targetServerId: options.targetServerId,
|
|
393
|
+
clientId: "slock-computer",
|
|
394
|
+
clientVersion: options.clientVersion ?? DEFAULT_CLIENT_VERSION
|
|
395
|
+
});
|
|
396
|
+
await options.onDeviceCode?.(challenge);
|
|
397
|
+
let intervalSeconds = Math.max(1, challenge.interval);
|
|
398
|
+
for (let poll = 0; poll < (options.maxDevicePolls ?? 120); poll += 1) {
|
|
399
|
+
const response = await options.client.pollDeviceToken(challenge.deviceCode);
|
|
400
|
+
if (response.status === "approved") {
|
|
401
|
+
if (response.serverId !== options.targetServerId) {
|
|
402
|
+
throw new ComputorAttachFlowError(
|
|
403
|
+
"DEVICE_TOKEN_SERVER_MISMATCH",
|
|
404
|
+
`Device token serverId=${response.serverId} did not match targetServerId=${options.targetServerId}`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
const session = {
|
|
408
|
+
accessToken: response.accessToken,
|
|
409
|
+
refreshToken: response.refreshToken,
|
|
410
|
+
humanId: response.humanId,
|
|
411
|
+
serverId: response.serverId,
|
|
412
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
413
|
+
};
|
|
414
|
+
await writeSessionAndActivePointer(options.slockHome, session, options.targetServerId);
|
|
415
|
+
return session;
|
|
416
|
+
}
|
|
417
|
+
if (response.status === "expired_token") {
|
|
418
|
+
throw new ComputorAttachFlowError("LOGIN_EXPIRED", "Device login expired; retry attach.");
|
|
419
|
+
}
|
|
420
|
+
if (response.status === "denied") {
|
|
421
|
+
throw new ComputorAttachFlowError("LOGIN_DENIED", "Device login was denied.");
|
|
422
|
+
}
|
|
423
|
+
if (response.status === "slow_down") {
|
|
424
|
+
intervalSeconds = Math.max(intervalSeconds * 2, response.interval ?? 0);
|
|
425
|
+
}
|
|
426
|
+
await (options.wait ?? sleep)(intervalSeconds * 1e3);
|
|
427
|
+
}
|
|
428
|
+
throw new ComputorAttachFlowError("LOGIN_POLL_EXHAUSTED", "Device login did not complete before polling budget expired.");
|
|
429
|
+
}
|
|
430
|
+
function mergeRefreshedSession(previous, refreshed) {
|
|
431
|
+
return {
|
|
432
|
+
...previous,
|
|
433
|
+
accessToken: refreshed.accessToken,
|
|
434
|
+
refreshToken: refreshed.refreshToken,
|
|
435
|
+
humanId: refreshed.humanId ?? previous.humanId,
|
|
436
|
+
serverId: refreshed.serverId ?? previous.serverId,
|
|
437
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
async function attachWithRetry(args) {
|
|
441
|
+
const request = buildAttachRequest(args);
|
|
442
|
+
let session = args.session;
|
|
443
|
+
let refreshedAfterAttach401 = false;
|
|
444
|
+
for (let attempt = 0; attempt < args.maxAttempts; attempt += 1) {
|
|
445
|
+
try {
|
|
446
|
+
return { attach: await args.client.attach(session.accessToken, request), session };
|
|
447
|
+
} catch (err) {
|
|
448
|
+
if (!(err instanceof ComputorApiError)) throw err;
|
|
449
|
+
if (err.status === 401 && !refreshedAfterAttach401) {
|
|
450
|
+
refreshedAfterAttach401 = true;
|
|
451
|
+
try {
|
|
452
|
+
const refreshed = await args.client.refreshSession(session.refreshToken, args.targetServerId);
|
|
453
|
+
session = mergeRefreshedSession(session, refreshed);
|
|
454
|
+
await writeSession(args.slockHome, session);
|
|
455
|
+
attempt -= 1;
|
|
456
|
+
continue;
|
|
457
|
+
} catch (refreshErr) {
|
|
458
|
+
if (refreshErr instanceof ComputorApiError && refreshErr.status === 401) {
|
|
459
|
+
throw new ComputorAttachFlowError("SESSION_INVALID", "Session expired; re-run attach to log in again.");
|
|
460
|
+
}
|
|
461
|
+
throw refreshErr;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
throwTerminalAttachError(err, args.existingServerMachineId !== null);
|
|
465
|
+
if (attempt >= args.maxAttempts - 1 || err.status < 500) throw err;
|
|
466
|
+
await (args.wait ?? sleep)(100 * 2 ** attempt);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
throw new ComputorAttachFlowError("ATTACH_RETRY_EXHAUSTED", "Attach did not complete before retry budget expired.");
|
|
470
|
+
}
|
|
471
|
+
function buildAttachRequest(args) {
|
|
472
|
+
return {
|
|
473
|
+
targetServerId: args.targetServerId,
|
|
474
|
+
attachAttemptId: args.attachAttemptId,
|
|
475
|
+
...args.existingServerMachineId ? { existingServerMachineId: args.existingServerMachineId } : {},
|
|
476
|
+
clientId: "slock-computer",
|
|
477
|
+
clientVersion: args.clientVersion
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function throwTerminalAttachError(err, hadExistingIdentity) {
|
|
481
|
+
if (err.status === 403 && err.code === "insufficient_role") {
|
|
482
|
+
throw new ComputorAttachFlowError("INSUFFICIENT_ROLE", "Owner/admin role is required to attach this server.");
|
|
483
|
+
}
|
|
484
|
+
if (err.status === 409) {
|
|
485
|
+
throw new ComputorAttachFlowError(
|
|
486
|
+
"ATTEMPT_CONFLICT",
|
|
487
|
+
"Attach attempt id is already bound to another human or target server."
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
if (hadExistingIdentity && (err.status === 403 || err.status === 404)) {
|
|
491
|
+
throw new ComputorAttachFlowError(
|
|
492
|
+
"STALE_IDENTITY",
|
|
493
|
+
"Local state.json references an unknown or foreign server machine; refusing to create a fallback machine."
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function sleep(ms) {
|
|
498
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/output.ts
|
|
502
|
+
var CliExit = class extends Error {
|
|
503
|
+
constructor(exitCode) {
|
|
504
|
+
super(`CliExit(${exitCode})`);
|
|
505
|
+
this.exitCode = exitCode;
|
|
506
|
+
this.name = "CliExit";
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
function fail(code, message, exitCode = 1) {
|
|
510
|
+
process.stderr.write(JSON.stringify({ ok: false, code, message }) + "\n");
|
|
511
|
+
throw new CliExit(exitCode);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/index.ts
|
|
515
|
+
var program = new Command();
|
|
516
|
+
program.name("slock-computer").description("Slock Computer login and attach entrypoint.").version("0.0.1");
|
|
517
|
+
program.command("attach").argument("<serverId>", "target Slock server id").option("--server-url <url>", "Slock API base URL; defaults to SLOCK_SERVER_URL").option("--client-version <version>", "slock-computer client version", "0.0.1").action(async (serverId, opts) => {
|
|
518
|
+
const slockHome = resolveSlockHome();
|
|
519
|
+
const serverUrl = opts.serverUrl ?? process.env.SLOCK_SERVER_URL;
|
|
520
|
+
if (!serverUrl) {
|
|
521
|
+
fail("MISSING_SERVER_URL", "Set SLOCK_SERVER_URL or pass --server-url.");
|
|
522
|
+
}
|
|
523
|
+
const result = await runComputorAttachFlow({
|
|
524
|
+
targetServerId: serverId,
|
|
525
|
+
slockHome,
|
|
526
|
+
clientVersion: opts.clientVersion,
|
|
527
|
+
client: new ComputorHttpClient(serverUrl),
|
|
528
|
+
onDeviceCode: (challenge) => {
|
|
529
|
+
const uri = challenge.verificationUriComplete ?? challenge.verificationUri;
|
|
530
|
+
process.stdout.write(`Open ${uri} and enter code ${challenge.userCode}
|
|
531
|
+
`);
|
|
532
|
+
},
|
|
533
|
+
onPostAttachSuccess: async (attach) => {
|
|
534
|
+
await writeComputorStateAfterAttach(slockHome, attach);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
process.stdout.write(
|
|
538
|
+
`Attach authorized for server ${result.attach.serverId} as computor ${result.attach.serverMachineId} (${result.attach.resumed ? "resumed" : "created"}).
|
|
539
|
+
`
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
program.parseAsync().catch((err) => {
|
|
543
|
+
if (err instanceof CliExit) {
|
|
544
|
+
process.exitCode = err.exitCode;
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (err instanceof ComputorAttachFlowError) {
|
|
548
|
+
fail(err.code, err.message);
|
|
549
|
+
}
|
|
550
|
+
if (err instanceof ComputorApiError) {
|
|
551
|
+
fail(err.code, err.message);
|
|
552
|
+
}
|
|
553
|
+
process.stderr.write(`Unexpected error: ${err?.message ?? err}
|
|
554
|
+
`);
|
|
555
|
+
process.exitCode = 1;
|
|
556
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@slock-ai/computer",
|
|
3
|
+
"version": "0.0.1-play.20260511091124",
|
|
4
|
+
"description": "Slock Computer attach and local runtime entrypoint.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"slock-computer": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/botiverse/slock.git",
|
|
15
|
+
"directory": "packages/computer"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"computer": "tsx src/index.ts",
|
|
22
|
+
"start": "tsx src/index.ts",
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"test": "node --import tsx --test --test-force-exit 'src/**/*.test.ts'",
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"commander": "^12.1.0",
|
|
29
|
+
"undici": "^7.24.7"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^25.5.0",
|
|
33
|
+
"tsup": "^8.5.1",
|
|
34
|
+
"tsx": "^4.21.0",
|
|
35
|
+
"typescript": "^5.9.3"
|
|
36
|
+
},
|
|
37
|
+
"private": false
|
|
38
|
+
}
|