@alook/cli 0.0.135 → 0.0.136

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 CHANGED
@@ -14561,8 +14561,7 @@ var DaemonPushMessageSchema = exports_external.discriminatedUnion("type", [
14561
14561
  exports_external.object({ type: exports_external.literal("daemon.evict"), workspaceId: exports_external.string() }),
14562
14562
  exports_external.object({ type: exports_external.literal("daemon.update"), version: exports_external.string() }),
14563
14563
  exports_external.object({ type: exports_external.literal("daemon.rescan") }),
14564
- exports_external.object({ type: exports_external.literal("daemon.kill"), workspaceId: exports_external.string(), agentId: exports_external.string().min(1), taskId: exports_external.string(), targetTaskId: exports_external.string() }),
14565
- exports_external.object({ type: exports_external.literal("daemon.workspace_added"), workspaceId: exports_external.string(), workspaceName: exports_external.string(), token: exports_external.string() })
14564
+ exports_external.object({ type: exports_external.literal("daemon.kill"), workspaceId: exports_external.string(), agentId: exports_external.string().min(1), taskId: exports_external.string(), targetTaskId: exports_external.string() })
14566
14565
  ]);
14567
14566
  var RegisterResponseSchema = exports_external.object({
14568
14567
  runtimes: exports_external.array(exports_external.object({ id: exports_external.string() }))
@@ -14592,9 +14591,6 @@ var RegisterDaemonRequestSchema = exports_external.object({
14592
14591
  workspaces_root: exports_external.string().optional().default(""),
14593
14592
  runtimes: exports_external.array(DaemonRuntimeItemSchema).min(1)
14594
14593
  });
14595
- var BindWorkspaceRequestSchema = exports_external.object({
14596
- workspace_id: exports_external.string().min(1)
14597
- });
14598
14594
  var DeregisterRequestSchema = exports_external.object({
14599
14595
  daemon_id: exports_external.string().min(1)
14600
14596
  });
@@ -14860,7 +14856,7 @@ var UpdateMemberRequestSchema = exports_external.object({
14860
14856
  });
14861
14857
  var CreateWorkspaceRequestSchema = exports_external.object({
14862
14858
  name: exports_external.string().min(1, "name is required"),
14863
- slug: exports_external.string().min(1, "slug is required")
14859
+ slug: exports_external.string().optional().default("")
14864
14860
  });
14865
14861
  var UpdateWorkspaceRequestSchema = exports_external.object({
14866
14862
  name: exports_external.string().min(1, "name is required").max(100).trim().optional(),
@@ -16358,6 +16354,7 @@ var workspace = sqliteTable("workspace", {
16358
16354
  id: text("id").primaryKey().$defaultFn(() => "sp_" + nanoid3()),
16359
16355
  name: text("name").notNull(),
16360
16356
  slug: text("slug").unique().notNull(),
16357
+ onboarded: integer2("onboarded").notNull().default(0),
16361
16358
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString()),
16362
16359
  updatedAt: text("updated_at").notNull().$defaultFn(() => new Date().toISOString())
16363
16360
  });
@@ -16971,6 +16968,9 @@ function cmdPrefix() {
16971
16968
 
16972
16969
  // lib/activate.ts
16973
16970
  import { hostname as hostname4 } from "os";
16971
+ import { spawn as spawn6 } from "child_process";
16972
+ import { openSync as openSync2, closeSync as closeSync2, mkdirSync as mkdirSync9 } from "fs";
16973
+ import { dirname as dirname4 } from "path";
16974
16974
 
16975
16975
  // lib/config.ts
16976
16976
  import { readFileSync, writeFileSync, mkdirSync } from "fs";
@@ -17000,13 +17000,9 @@ function loadCLIConfigForProfile(profile) {
17000
17000
  session_token: cfg.session_token,
17001
17001
  watched_workspaces: cfg.watched_workspaces || []
17002
17002
  };
17003
- const legacy = cfg.machine_token;
17004
- if (legacy && !result.watched_workspaces.some((w) => w.token === legacy)) {
17005
- result.watched_workspaces.push({ id: null, name: null, token: legacy, status: "registered", agent_ids: [] });
17006
- }
17007
17003
  for (const ws of result.watched_workspaces) {
17008
17004
  if (!ws.status)
17009
- ws.status = ws.id ? "active" : "registered";
17005
+ ws.status = ws.id ? "active" : "deleted";
17010
17006
  }
17011
17007
  return result;
17012
17008
  }
@@ -17309,556 +17305,176 @@ function releaseDaemonPid(profile) {
17309
17305
  removePidFileIfMatches(process.pid, profile);
17310
17306
  }
17311
17307
 
17312
- // lib/runtimes.ts
17313
- import { execSync } from "child_process";
17314
- function isCommandAvailable(cmd) {
17315
- try {
17316
- const check2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
17317
- execSync(check2, { stdio: "ignore" });
17318
- return true;
17319
- } catch {
17320
- return false;
17308
+ // daemon/client.ts
17309
+ class DaemonClient {
17310
+ baseURL;
17311
+ constructor(baseURL) {
17312
+ this.baseURL = baseURL;
17321
17313
  }
17322
- }
17323
- function detectRuntimes() {
17324
- const found = [];
17325
- for (const type of ["claude", "codex", "opencode"]) {
17326
- if (isCommandAvailable(type)) {
17327
- let version3 = "";
17314
+ async request(method, path, token, body) {
17315
+ const headers = {
17316
+ "Content-Type": "application/json",
17317
+ Authorization: `Bearer ${token}`
17318
+ };
17319
+ const MAX_RETRIES = 3;
17320
+ const BASE_DELAY_MS = 500;
17321
+ let lastError;
17322
+ for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
17328
17323
  try {
17329
- version3 = execSync(`${type} --version`, { encoding: "utf-8" }).trim();
17330
- } catch {}
17331
- found.push({ type, version: version3 });
17324
+ const res = await fetch(this.baseURL + path, {
17325
+ method,
17326
+ headers,
17327
+ body: body ? JSON.stringify(body) : undefined
17328
+ });
17329
+ if (!res.ok)
17330
+ throw new Error(`HTTP ${res.status}: ${await res.text()}`);
17331
+ if (res.status === 204)
17332
+ return;
17333
+ return res.json();
17334
+ } catch (e) {
17335
+ if (e instanceof TypeError) {
17336
+ lastError = e;
17337
+ if (attempt < MAX_RETRIES) {
17338
+ await new Promise((r) => setTimeout(r, BASE_DELAY_MS * 2 ** attempt));
17339
+ continue;
17340
+ }
17341
+ }
17342
+ throw e;
17343
+ }
17332
17344
  }
17345
+ throw lastError;
17333
17346
  }
17334
- return found;
17335
- }
17336
-
17337
- // lib/activate.ts
17338
- async function activateAndSave(opts) {
17339
- const { token, serverUrl, profile } = opts;
17340
- console.log("Scanning for AI runtimes...");
17341
- const runtimes = detectRuntimes();
17342
- if (runtimes.length === 0) {
17343
- console.error("Error: no runtimes found. Install claude, codex, or opencode first.");
17344
- process.exit(1);
17347
+ async register(token, body) {
17348
+ const raw = await this.request("POST", "/api/daemon/register", token, body);
17349
+ return RegisterResponseSchema.parse(raw);
17345
17350
  }
17346
- console.log(`Found: ${runtimes.map((r) => r.type).join(", ")}`);
17347
- const host = hostname4();
17348
- console.log("Registering machine...");
17349
- let activateResp;
17350
- try {
17351
- const res = await fetch(`${serverUrl}/api/machine-tokens/activate`, {
17352
- method: "POST",
17353
- headers: { "Content-Type": "application/json" },
17354
- body: JSON.stringify({ token, hostname: host, runtimes })
17351
+ heartbeat(token, daemonId) {
17352
+ return this.request("POST", "/api/daemon/heartbeat", token, {
17353
+ daemon_id: daemonId
17355
17354
  });
17356
- if (!res.ok) {
17357
- const text2 = await res.text();
17358
- console.error(`Error: registration failed (${res.status}): ${text2}`);
17359
- process.exit(1);
17360
- }
17361
- activateResp = await res.json();
17362
- } catch (err) {
17363
- console.error(`Error: failed to activate: ${err instanceof Error ? err.message : err}`);
17364
- process.exit(1);
17365
17355
  }
17366
- const existing = loadCLIConfigForProfile(profile);
17367
- const watched = existing.watched_workspaces || [];
17368
- if (!watched.some((w) => w.token === token)) {
17369
- watched.push({ id: null, name: null, token, status: "registered", agent_ids: [] });
17356
+ sweep(token, daemonId) {
17357
+ return this.request("POST", "/api/daemon/sweep", token, {
17358
+ daemon_id: daemonId
17359
+ });
17370
17360
  }
17371
- saveCLIConfigForProfile(profile, {
17372
- server_url: serverUrl,
17373
- watched_workspaces: watched
17374
- });
17375
- const daemonPid = readDaemonPid(profile);
17376
- if (daemonPid && isProcessAlive(daemonPid)) {
17377
- try {
17378
- process.kill(daemonPid, "SIGHUP");
17379
- console.log(`
17380
- Daemon (pid ${daemonPid}) notified — machine registered, awaiting workspace binding.`);
17381
- } catch {
17382
- console.log(`
17383
- Daemon is running but could not be notified. Restart it to pick up the new token.`);
17361
+ deregister(token, daemonId) {
17362
+ return this.request("POST", "/api/daemon/deregister", token, {
17363
+ daemon_id: daemonId
17364
+ });
17365
+ }
17366
+ async poll(token, daemonId, maxTasks, cliVersion) {
17367
+ const raw = await this.request("POST", "/api/daemon/tasks/poll", token, { daemon_id: daemonId, max_tasks: maxTasks, ...cliVersion && { cli_version: cliVersion } });
17368
+ const resp = PollResponseSchema.parse(raw);
17369
+ return {
17370
+ tasks: resp.tasks,
17371
+ evicted: resp.evicted ?? false,
17372
+ pending_update: resp.pending_update,
17373
+ pending_rescan: resp.pending_rescan,
17374
+ file_requests: resp.file_requests,
17375
+ meetings: resp.meetings
17376
+ };
17377
+ }
17378
+ startTask(token, taskId) {
17379
+ return this.request("POST", `/api/daemon/tasks/${taskId}/start`, token);
17380
+ }
17381
+ completeTask(token, taskId, body) {
17382
+ return this.request("POST", `/api/daemon/tasks/${taskId}/complete`, token, body);
17383
+ }
17384
+ failTask(token, taskId, error51) {
17385
+ return this.request("POST", `/api/daemon/tasks/${taskId}/fail`, token, {
17386
+ error: error51
17387
+ });
17388
+ }
17389
+ supersedeTask(token, taskId) {
17390
+ return this.request("POST", `/api/daemon/tasks/${taskId}/supersede`, token);
17391
+ }
17392
+ async getArtifactMeta(token, artifactId, workspaceId) {
17393
+ return this.request("GET", `/api/artifacts/${artifactId}?workspace_id=${encodeURIComponent(workspaceId)}`, token);
17394
+ }
17395
+ async downloadArtifact(token, artifactId, workspaceId) {
17396
+ const MAX_RETRIES = 3;
17397
+ const BASE_DELAY_MS = 500;
17398
+ let lastError;
17399
+ for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
17400
+ try {
17401
+ const res = await fetch(`${this.baseURL}/api/artifacts/${artifactId}/content?workspace_id=${encodeURIComponent(workspaceId)}`, { headers: { Authorization: `Bearer ${token}` } });
17402
+ if (!res.ok) {
17403
+ throw new Error(`artifact download failed: HTTP ${res.status}`);
17404
+ }
17405
+ return res.arrayBuffer();
17406
+ } catch (e) {
17407
+ if (e instanceof TypeError) {
17408
+ lastError = e;
17409
+ if (attempt < MAX_RETRIES) {
17410
+ await new Promise((r) => setTimeout(r, BASE_DELAY_MS * 2 ** attempt));
17411
+ continue;
17412
+ }
17413
+ }
17414
+ throw e;
17415
+ }
17384
17416
  }
17385
- } else {
17386
- const startCmd = isDev() ? `${cmdPrefix()} daemon start --foreground` : `${cmdPrefix()} daemon start`;
17387
- console.log();
17388
- console.log(`Run '${startCmd}' to start the daemon.`);
17417
+ throw lastError;
17418
+ }
17419
+ reportMessages(token, taskId, messages) {
17420
+ return this.request("POST", `/api/daemon/tasks/${taskId}/messages`, token, { messages });
17421
+ }
17422
+ reportFileData(token, body) {
17423
+ return this.request("POST", "/api/daemon/workspace/report", token, body);
17424
+ }
17425
+ syncSkills(token, body) {
17426
+ return this.request("POST", "/api/daemon/skills/sync", token, body);
17389
17427
  }
17390
- return {
17391
- daemonId: activateResp.daemon_id,
17392
- tokenStatus: activateResp.token_status
17393
- };
17394
17428
  }
17395
17429
 
17396
- // commands/register.ts
17397
- function registerCommand() {
17398
- const cmd = new Command("register").description("Register CLI with your Alook account").requiredOption("--token <token>", "API token (starts with al_)").option("--server <url>", "Server URL").option("--profile <name>", "Profile name").action(async (opts, command) => {
17399
- const token = opts.token;
17400
- const profile = opts.profile || command.parent?.opts().profile;
17401
- const serverUrl = opts.server || command.parent?.opts().server || getServerUrl();
17402
- if (!token) {
17403
- console.error(`Error: --token is required
17404
- Usage: ${cmdPrefix()} register --token <token>`);
17405
- process.exit(1);
17406
- }
17407
- if (!token.startsWith("al_")) {
17408
- console.error("Error: invalid token format: must start with 'al_'");
17409
- process.exit(1);
17410
- }
17411
- const client = new APIClient(serverUrl, token);
17412
- let me;
17413
- try {
17414
- me = await client.getJSON("/api/me");
17415
- } catch (err) {
17416
- console.error(`Error: failed to verify token: ${err instanceof Error ? err.message : err}`);
17417
- process.exit(1);
17430
+ // daemon/health.ts
17431
+ import { createServer } from "http";
17432
+ var DEFAULT_HEALTH_PORT = Number(process.env.ALOOK_HEALTH_PORT) || 19514;
17433
+ function createHealthServer(port = DEFAULT_HEALTH_PORT) {
17434
+ let runtimeCount = 0;
17435
+ const startTime = Date.now();
17436
+ const server = createServer((req, res) => {
17437
+ if (req.url === "/health") {
17438
+ const uptimeSec = Math.floor((Date.now() - startTime) / 1000);
17439
+ res.writeHead(200, { "Content-Type": "application/json" });
17440
+ res.end(JSON.stringify({
17441
+ status: "ok",
17442
+ uptime: `${uptimeSec}s`,
17443
+ runtimes: runtimeCount
17444
+ }));
17445
+ } else {
17446
+ res.writeHead(404);
17447
+ res.end();
17418
17448
  }
17419
- const result = await activateAndSave({ token, serverUrl, profile });
17420
- console.log(`
17421
- Registered as ${me.email}`);
17422
- console.log(`Machine: ${result.daemonId} (status: ${result.tokenStatus})`);
17423
- console.log(`Workspace binding will happen when you launch a company.`);
17424
17449
  });
17425
- return cmd;
17450
+ server.listen(port, "127.0.0.1");
17451
+ return {
17452
+ server,
17453
+ setRuntimeCount(n) {
17454
+ runtimeCount = n;
17455
+ }
17456
+ };
17426
17457
  }
17427
17458
 
17428
- // commands/login.ts
17429
- import { Command as Command2 } from "commander";
17430
- import { fork, spawn } from "child_process";
17431
- import { fileURLToPath as fileURLToPath2 } from "url";
17432
- var DEVICE_CLIENT_ID = process.env.ALOOK_DEVICE_CLIENT_ID || "alook-cli";
17433
- function openBrowser(url2) {
17434
- try {
17435
- const cmd = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : process.platform === "win32" ? "start" : null;
17436
- if (cmd) {
17437
- const args = process.platform === "win32" ? ["", url2] : [url2];
17438
- spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
17439
- }
17440
- } catch {}
17459
+ // daemon/agent/claude.ts
17460
+ import { spawn } from "child_process";
17461
+ import { createInterface } from "readline";
17462
+
17463
+ // daemon/kill-tree.ts
17464
+ import { execSync } from "child_process";
17465
+ var log3 = createLogger2({ module: "kill-tree" });
17466
+ function killGraceMs() {
17467
+ return Number(process.env.ALOOK_KILL_GRACE_MS) || 2000;
17441
17468
  }
17442
- function sleep(ms) {
17443
- return new Promise((resolve) => setTimeout(resolve, ms));
17444
- }
17445
- function syncWorkspacesToConfig(serverWorkspaces, profile, sessionToken) {
17446
- const cfg = loadCLIConfigForProfile(profile);
17447
- const watched = cfg.watched_workspaces || [];
17448
- const serverIds = new Set(serverWorkspaces.map((w) => w.id));
17449
- for (const sw of serverWorkspaces) {
17450
- const existing = watched.find((w) => w.id === sw.id);
17451
- if (existing) {
17452
- existing.status = "active";
17453
- existing.name = sw.name;
17454
- } else {
17455
- watched.push({ id: sw.id, name: sw.name, token: "", status: "active", agent_ids: [] });
17456
- }
17457
- }
17458
- for (const w of watched) {
17459
- if (w.id && !serverIds.has(w.id) && w.status !== "registered") {
17460
- w.status = "deleted";
17461
- }
17462
- }
17463
- saveCLIConfigForProfile(profile, {
17464
- server_url: cfg.server_url,
17465
- session_token: sessionToken ?? cfg.session_token,
17466
- watched_workspaces: watched
17467
- });
17468
- }
17469
- async function pollAndActivate(opts) {
17470
- const { deviceCode, expiresIn, serverUrl, profile } = opts;
17471
- let interval = opts.interval;
17472
- const expiresAt = Date.now() + expiresIn * 1000;
17473
- let tokenResp;
17474
- while (Date.now() < expiresAt) {
17475
- await sleep(interval);
17476
- try {
17477
- const res = await fetch(`${serverUrl}/api/auth/device/token`, {
17478
- method: "POST",
17479
- headers: { "Content-Type": "application/json" },
17480
- body: JSON.stringify({
17481
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
17482
- device_code: deviceCode,
17483
- client_id: DEVICE_CLIENT_ID
17484
- })
17485
- });
17486
- if (res.ok) {
17487
- tokenResp = await res.json();
17488
- break;
17489
- }
17490
- const errBody = await res.json();
17491
- if (errBody.error === "slow_down") {
17492
- interval += 5000;
17493
- } else if (errBody.error === "authorization_pending") {} else if (errBody.error === "expired_token") {
17494
- console.error("Error: device code expired. Please run login again.");
17495
- process.exit(1);
17496
- } else if (errBody.error === "access_denied") {
17497
- console.error("Error: authorization was denied.");
17498
- process.exit(1);
17499
- } else {
17500
- console.error(`Error: unexpected error: ${errBody.error_description || errBody.error}`);
17501
- process.exit(1);
17502
- }
17503
- } catch {
17504
- console.error("Error: network request failed during polling.");
17505
- process.exit(1);
17506
- }
17507
- }
17508
- if (!tokenResp) {
17509
- console.error("Error: device code expired (timed out). Please run login again.");
17510
- process.exit(1);
17511
- }
17512
- const sessionToken = tokenResp.access_token;
17513
- const client = new APIClient(serverUrl, sessionToken);
17514
- let email3 = "";
17515
- try {
17516
- const me = await client.getJSON("/api/me");
17517
- email3 = me.email;
17518
- } catch {}
17519
- let serverWorkspaces = [];
17520
- try {
17521
- serverWorkspaces = await client.getJSON("/api/workspaces");
17522
- } catch {}
17523
- syncWorkspacesToConfig(serverWorkspaces, profile, sessionToken);
17524
- const existingWorkspaceId = serverWorkspaces.length > 0 ? serverWorkspaces[0].id : "";
17525
- const mtUrl = existingWorkspaceId ? `/api/machine-tokens?workspace_id=${existingWorkspaceId}` : "/api/machine-tokens";
17526
- let machineToken2;
17527
- try {
17528
- const mtResp = await client.postJSON(mtUrl);
17529
- machineToken2 = mtResp.token;
17530
- } catch {
17531
- process.exit(1);
17532
- }
17533
- const result = await activateAndSave({ token: machineToken2, serverUrl, profile });
17534
- if (email3) {
17535
- console.log(`
17536
- Logged in as ${email3}`);
17537
- }
17538
- console.log(`Machine: ${result.daemonId} (status: ${result.tokenStatus})`);
17539
- console.log(`Workspace binding will happen when you launch a company.`);
17540
- }
17541
- if (process.argv.includes("--__login-poll")) {
17542
- const idx = process.argv.indexOf("--__login-poll");
17543
- let data;
17544
- try {
17545
- data = JSON.parse(process.argv[idx + 1]);
17546
- } catch {
17547
- console.error("Error: invalid poll data");
17548
- process.exit(1);
17549
- }
17550
- pollAndActivate(data).catch(() => process.exit(1));
17551
- }
17552
- async function checkExistingAuth(serverUrl, profile) {
17553
- const config2 = loadCLIConfigForProfile(profile);
17554
- const sessionToken = config2.session_token;
17555
- const workspaces = config2.watched_workspaces || [];
17556
- const ws = workspaces[0];
17557
- const authToken = sessionToken || ws?.token;
17558
- if (!authToken) {
17559
- return { valid: false };
17560
- }
17561
- try {
17562
- const res = await fetch(`${serverUrl}/api/workspaces`, {
17563
- headers: { Authorization: `Bearer ${authToken}` }
17564
- });
17565
- if (!res.ok) {
17566
- return { valid: false };
17567
- }
17568
- const serverWorkspaces = await res.json();
17569
- const hasValidWorkspace = workspaces.some((w) => w.id && w.status !== "deleted");
17570
- if (!hasValidWorkspace && serverWorkspaces.length > 0) {
17571
- syncWorkspacesToConfig(serverWorkspaces, profile);
17572
- }
17573
- let email3;
17574
- try {
17575
- const meRes = await fetch(`${serverUrl}/api/me`, {
17576
- headers: { Authorization: `Bearer ${authToken}` }
17577
- });
17578
- if (meRes.ok) {
17579
- const me = await meRes.json();
17580
- email3 = me.email;
17581
- }
17582
- } catch {}
17583
- const workspaceName = (serverWorkspaces.length > 0 ? serverWorkspaces[0].name : undefined) || ws?.name || undefined;
17584
- return { valid: true, email: email3, workspaceName };
17585
- } catch {
17586
- return { valid: false };
17587
- }
17588
- }
17589
- function loginCommand() {
17590
- const cmd = new Command2("login").description("Log in to Alook via browser (device code flow)").option("--server <url>", "Server URL").option("--profile <name>", "Profile name").option("--force", "Re-authenticate even if already logged in").action(async (opts, command) => {
17591
- const profile = opts.profile || command.parent?.opts().profile;
17592
- const serverUrl = opts.server || command.parent?.opts().server || getServerUrl();
17593
- if (!opts.force) {
17594
- const existing = await checkExistingAuth(serverUrl, profile);
17595
- if (existing.valid) {
17596
- const parts = ["Already logged in"];
17597
- if (existing.email)
17598
- parts[0] += ` as ${existing.email}`;
17599
- if (existing.workspaceName)
17600
- parts[0] += ` (workspace: ${existing.workspaceName})`;
17601
- parts[0] += ".";
17602
- console.log(parts[0]);
17603
- return;
17604
- }
17605
- }
17606
- console.log("Requesting device code...");
17607
- let deviceResp;
17608
- try {
17609
- const res = await fetch(`${serverUrl}/api/auth/device/code`, {
17610
- method: "POST",
17611
- headers: { "Content-Type": "application/json" },
17612
- body: JSON.stringify({ client_id: DEVICE_CLIENT_ID })
17613
- });
17614
- if (!res.ok) {
17615
- const text2 = await res.text();
17616
- console.error(`Error: failed to get device code (${res.status}): ${text2}`);
17617
- process.exit(1);
17618
- }
17619
- deviceResp = await res.json();
17620
- } catch (err) {
17621
- console.error(`Error: failed to request device code: ${err instanceof Error ? err.message : err}`);
17622
- process.exit(1);
17623
- }
17624
- const verificationUrl = deviceResp.verification_uri_complete || deviceResp.verification_uri;
17625
- console.log();
17626
- console.log(` Open this URL in your browser:`);
17627
- console.log(` ${verificationUrl}`);
17628
- console.log();
17629
- console.log(` Enter code: ${deviceResp.user_code}`);
17630
- console.log();
17631
- if (!process.stdout.isTTY) {
17632
- const pollData = JSON.stringify({
17633
- deviceCode: deviceResp.device_code,
17634
- interval: (deviceResp.interval || 5) * 1000,
17635
- expiresIn: deviceResp.expires_in,
17636
- serverUrl,
17637
- profile
17638
- });
17639
- const thisFile = fileURLToPath2(import.meta.url);
17640
- const child = fork(thisFile, ["--__login-poll", pollData], {
17641
- detached: true,
17642
- stdio: "ignore"
17643
- });
17644
- child.unref();
17645
- console.log(" Polling for authorization in the background (timeout: 5min).");
17646
- console.log(` Once approved, run \`${cmdPrefix()} status\` to verify.`);
17647
- return;
17648
- }
17649
- openBrowser(verificationUrl);
17650
- console.log(" (Browser opened automatically)");
17651
- console.log();
17652
- console.log("Waiting for authorization...");
17653
- await pollAndActivate({
17654
- deviceCode: deviceResp.device_code,
17655
- interval: (deviceResp.interval || 5) * 1000,
17656
- expiresIn: deviceResp.expires_in,
17657
- serverUrl,
17658
- profile
17659
- });
17660
- });
17661
- return cmd;
17662
- }
17663
-
17664
- // commands/status.ts
17665
- import { Command as Command3 } from "commander";
17666
- function statusCommand() {
17667
- const cmd = new Command3("status").description("Show registration status").action((_opts, command) => {
17668
- const profile = command.parent?.opts().profile;
17669
- const cfg = loadCLIConfigForProfile(profile);
17670
- const ws = cfg.watched_workspaces?.[0];
17671
- if (!ws?.token) {
17672
- console.log("Not registered");
17673
- console.log(`Run '${cmdPrefix()} register --token <token>' to register.`);
17674
- return;
17675
- }
17676
- console.log("Status: Registered");
17677
- console.log(`Server: ${cfg.server_url}`);
17678
- console.log(`Workspace: ${ws.name} (${ws.id})`);
17679
- });
17680
- return cmd;
17681
- }
17682
-
17683
- // commands/daemon.ts
17684
- import { Command as Command4 } from "commander";
17685
- import { spawn as spawn7 } from "child_process";
17686
- import { openSync as openSync2, closeSync as closeSync2, mkdirSync as mkdirSync9 } from "fs";
17687
- import { dirname as dirname4 } from "path";
17688
-
17689
- // daemon/client.ts
17690
- class DaemonClient {
17691
- baseURL;
17692
- constructor(baseURL) {
17693
- this.baseURL = baseURL;
17694
- }
17695
- async request(method, path, token, body) {
17696
- const headers = {
17697
- "Content-Type": "application/json",
17698
- Authorization: `Bearer ${token}`
17699
- };
17700
- const MAX_RETRIES = 3;
17701
- const BASE_DELAY_MS = 500;
17702
- let lastError;
17703
- for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
17704
- try {
17705
- const res = await fetch(this.baseURL + path, {
17706
- method,
17707
- headers,
17708
- body: body ? JSON.stringify(body) : undefined
17709
- });
17710
- if (!res.ok)
17711
- throw new Error(`HTTP ${res.status}: ${await res.text()}`);
17712
- if (res.status === 204)
17713
- return;
17714
- return res.json();
17715
- } catch (e) {
17716
- if (e instanceof TypeError) {
17717
- lastError = e;
17718
- if (attempt < MAX_RETRIES) {
17719
- await new Promise((r) => setTimeout(r, BASE_DELAY_MS * 2 ** attempt));
17720
- continue;
17721
- }
17722
- }
17723
- throw e;
17724
- }
17725
- }
17726
- throw lastError;
17727
- }
17728
- async register(token, body) {
17729
- const raw = await this.request("POST", "/api/daemon/register", token, body);
17730
- return RegisterResponseSchema.parse(raw);
17731
- }
17732
- heartbeat(token, daemonId) {
17733
- return this.request("POST", "/api/daemon/heartbeat", token, {
17734
- daemon_id: daemonId
17735
- });
17736
- }
17737
- sweep(token, daemonId) {
17738
- return this.request("POST", "/api/daemon/sweep", token, {
17739
- daemon_id: daemonId
17740
- });
17741
- }
17742
- deregister(token, daemonId) {
17743
- return this.request("POST", "/api/daemon/deregister", token, {
17744
- daemon_id: daemonId
17745
- });
17746
- }
17747
- async poll(token, daemonId, maxTasks, cliVersion) {
17748
- const raw = await this.request("POST", "/api/daemon/tasks/poll", token, { daemon_id: daemonId, max_tasks: maxTasks, ...cliVersion && { cli_version: cliVersion } });
17749
- const resp = PollResponseSchema.parse(raw);
17750
- return {
17751
- tasks: resp.tasks,
17752
- evicted: resp.evicted ?? false,
17753
- pending_update: resp.pending_update,
17754
- pending_rescan: resp.pending_rescan,
17755
- file_requests: resp.file_requests,
17756
- meetings: resp.meetings
17757
- };
17758
- }
17759
- startTask(token, taskId) {
17760
- return this.request("POST", `/api/daemon/tasks/${taskId}/start`, token);
17761
- }
17762
- completeTask(token, taskId, body) {
17763
- return this.request("POST", `/api/daemon/tasks/${taskId}/complete`, token, body);
17764
- }
17765
- failTask(token, taskId, error51) {
17766
- return this.request("POST", `/api/daemon/tasks/${taskId}/fail`, token, {
17767
- error: error51
17768
- });
17769
- }
17770
- supersedeTask(token, taskId) {
17771
- return this.request("POST", `/api/daemon/tasks/${taskId}/supersede`, token);
17772
- }
17773
- async getArtifactMeta(token, artifactId, workspaceId) {
17774
- return this.request("GET", `/api/artifacts/${artifactId}?workspace_id=${encodeURIComponent(workspaceId)}`, token);
17775
- }
17776
- async downloadArtifact(token, artifactId, workspaceId) {
17777
- const MAX_RETRIES = 3;
17778
- const BASE_DELAY_MS = 500;
17779
- let lastError;
17780
- for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
17781
- try {
17782
- const res = await fetch(`${this.baseURL}/api/artifacts/${artifactId}/content?workspace_id=${encodeURIComponent(workspaceId)}`, { headers: { Authorization: `Bearer ${token}` } });
17783
- if (!res.ok) {
17784
- throw new Error(`artifact download failed: HTTP ${res.status}`);
17785
- }
17786
- return res.arrayBuffer();
17787
- } catch (e) {
17788
- if (e instanceof TypeError) {
17789
- lastError = e;
17790
- if (attempt < MAX_RETRIES) {
17791
- await new Promise((r) => setTimeout(r, BASE_DELAY_MS * 2 ** attempt));
17792
- continue;
17793
- }
17794
- }
17795
- throw e;
17796
- }
17797
- }
17798
- throw lastError;
17799
- }
17800
- reportMessages(token, taskId, messages) {
17801
- return this.request("POST", `/api/daemon/tasks/${taskId}/messages`, token, { messages });
17802
- }
17803
- reportFileData(token, body) {
17804
- return this.request("POST", "/api/daemon/workspace/report", token, body);
17805
- }
17806
- syncSkills(token, body) {
17807
- return this.request("POST", "/api/daemon/skills/sync", token, body);
17808
- }
17809
- async checkStandby(token, body) {
17810
- return this.request("POST", "/api/daemon/register", token, body);
17811
- }
17812
- }
17813
-
17814
- // daemon/health.ts
17815
- import { createServer } from "http";
17816
- var DEFAULT_HEALTH_PORT = Number(process.env.ALOOK_HEALTH_PORT) || 19514;
17817
- function createHealthServer(port = DEFAULT_HEALTH_PORT) {
17818
- let runtimeCount = 0;
17819
- const startTime = Date.now();
17820
- const server = createServer((req, res) => {
17821
- if (req.url === "/health") {
17822
- const uptimeSec = Math.floor((Date.now() - startTime) / 1000);
17823
- res.writeHead(200, { "Content-Type": "application/json" });
17824
- res.end(JSON.stringify({
17825
- status: "ok",
17826
- uptime: `${uptimeSec}s`,
17827
- runtimes: runtimeCount
17828
- }));
17829
- } else {
17830
- res.writeHead(404);
17831
- res.end();
17832
- }
17833
- });
17834
- server.listen(port, "127.0.0.1");
17835
- return {
17836
- server,
17837
- setRuntimeCount(n) {
17838
- runtimeCount = n;
17839
- }
17840
- };
17841
- }
17842
-
17843
- // daemon/agent/claude.ts
17844
- import { spawn as spawn2 } from "child_process";
17845
- import { createInterface } from "readline";
17846
-
17847
- // daemon/kill-tree.ts
17848
- import { execSync as execSync2 } from "child_process";
17849
- var log3 = createLogger2({ module: "kill-tree" });
17850
- function killGraceMs() {
17851
- return Number(process.env.ALOOK_KILL_GRACE_MS) || 2000;
17852
- }
17853
- var POLL_MS = 100;
17854
- var isPosix = process.platform !== "win32";
17855
- function isAlive(pid) {
17856
- try {
17857
- process.kill(pid, 0);
17858
- return true;
17859
- } catch (e) {
17860
- return e?.code === "EPERM";
17861
- }
17469
+ var POLL_MS = 100;
17470
+ var isPosix = process.platform !== "win32";
17471
+ function isAlive(pid) {
17472
+ try {
17473
+ process.kill(pid, 0);
17474
+ return true;
17475
+ } catch (e) {
17476
+ return e?.code === "EPERM";
17477
+ }
17862
17478
  }
17863
17479
  function signalTree(pid, signal) {
17864
17480
  if (isPosix) {
@@ -17873,7 +17489,7 @@ function signalTree(pid, signal) {
17873
17489
  }
17874
17490
  if (!isPosix) {
17875
17491
  try {
17876
- execSync2(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
17492
+ execSync(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
17877
17493
  return;
17878
17494
  } catch {}
17879
17495
  return;
@@ -17931,7 +17547,7 @@ class ClaudeBackend {
17931
17547
  if (options.resumeSessionId) {
17932
17548
  args.push("--resume", options.resumeSessionId);
17933
17549
  }
17934
- const proc = spawn2(this.cliPath, args, {
17550
+ const proc = spawn(this.cliPath, args, {
17935
17551
  cwd: options.cwd,
17936
17552
  stdio: ["pipe", "pipe", "pipe"],
17937
17553
  env: { ...process.env, ...options.env },
@@ -18170,7 +17786,7 @@ function handleControlRequest(proc, event) {
18170
17786
  }
18171
17787
 
18172
17788
  // daemon/agent/codex.ts
18173
- import { spawn as spawn3 } from "child_process";
17789
+ import { spawn as spawn2 } from "child_process";
18174
17790
  import { createInterface as createInterface2 } from "readline";
18175
17791
  var RAW_DETECTION_METHODS = new Set([
18176
17792
  "turn/started",
@@ -18202,7 +17818,7 @@ class CodexBackend {
18202
17818
  this.cliPath = cliPath;
18203
17819
  }
18204
17820
  execute(prompt, options) {
18205
- const proc = spawn3(this.cliPath, ["app-server", "--listen", "stdio://", "--config", "sandbox_mode=danger-full-access"], {
17821
+ const proc = spawn2(this.cliPath, ["app-server", "--listen", "stdio://", "--config", "sandbox_mode=danger-full-access"], {
18206
17822
  cwd: options.cwd,
18207
17823
  stdio: ["pipe", "pipe", "pipe"],
18208
17824
  env: { ...process.env, ...options.env },
@@ -18676,7 +18292,7 @@ class CodexBackend {
18676
18292
  }
18677
18293
 
18678
18294
  // daemon/agent/opencode.ts
18679
- import { spawn as spawn4 } from "child_process";
18295
+ import { spawn as spawn3 } from "child_process";
18680
18296
  import { createInterface as createInterface3 } from "readline";
18681
18297
  class OpenCodeBackend {
18682
18298
  cliPath;
@@ -18693,7 +18309,7 @@ class OpenCodeBackend {
18693
18309
  args.push("--session", options.resumeSessionId);
18694
18310
  }
18695
18311
  args.push(prompt);
18696
- const proc = spawn4(this.cliPath, args, {
18312
+ const proc = spawn3(this.cliPath, args, {
18697
18313
  cwd: options.cwd,
18698
18314
  stdio: ["ignore", "pipe", "pipe"],
18699
18315
  env: { ...process.env, ...options.env, OPENCODE_PERMISSION: '{"*":"allow"}' },
@@ -18934,7 +18550,7 @@ class OpenCodeBackend {
18934
18550
  }
18935
18551
 
18936
18552
  // daemon/agent/index.ts
18937
- import { execSync as execSync3 } from "child_process";
18553
+ import { execSync as execSync2 } from "child_process";
18938
18554
  function createBackend(provider, cliPath) {
18939
18555
  switch (provider) {
18940
18556
  case "claude":
@@ -18949,7 +18565,7 @@ function createBackend(provider, cliPath) {
18949
18565
  }
18950
18566
  async function detectVersion2(cliPath) {
18951
18567
  try {
18952
- return execSync3(`${cliPath} --version`, { encoding: "utf-8" }).trim();
18568
+ return execSync2(`${cliPath} --version`, { encoding: "utf-8" }).trim();
18953
18569
  } catch {
18954
18570
  return "unknown";
18955
18571
  }
@@ -19509,7 +19125,7 @@ function localISOString(now = new Date) {
19509
19125
  function lockPathFor(timelineDir, filename) {
19510
19126
  return join7(timelineDir, `.${filename}.lock`);
19511
19127
  }
19512
- function sleep2(ms) {
19128
+ function sleep(ms) {
19513
19129
  return new Promise((resolve) => setTimeout(resolve, ms));
19514
19130
  }
19515
19131
  async function initEntryAsync(timelineDir, entry) {
@@ -19519,7 +19135,7 @@ async function initEntryAsync(timelineDir, entry) {
19519
19135
  try {
19520
19136
  let acquired = acquireLock(lockPath);
19521
19137
  if (!acquired) {
19522
- await sleep2(200);
19138
+ await sleep(200);
19523
19139
  acquired = acquireLock(lockPath);
19524
19140
  }
19525
19141
  if (!acquired) {
@@ -20329,7 +19945,7 @@ class DaemonWsClient {
20329
19945
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, unlinkSync as unlinkSync4 } from "fs";
20330
19946
 
20331
19947
  // lib/update.ts
20332
- import { spawn as spawn5 } from "child_process";
19948
+ import { spawn as spawn4 } from "child_process";
20333
19949
  function fetchLatestVersion() {
20334
19950
  return fetch("https://registry.npmjs.org/@alook/cli/latest").then((res) => {
20335
19951
  if (!res.ok)
@@ -20340,7 +19956,7 @@ function fetchLatestVersion() {
20340
19956
  function runNpmUpdate(targetVersion) {
20341
19957
  return new Promise((resolve) => {
20342
19958
  const chunks = [];
20343
- const child = spawn5("npm", ["install", "-g", `@alook/cli@${targetVersion}`], {
19959
+ const child = spawn4("npm", ["install", "-g", `@alook/cli@${targetVersion}`], {
20344
19960
  stdio: ["ignore", "pipe", "pipe"]
20345
19961
  });
20346
19962
  child.stdout?.on("data", (d) => chunks.push(d));
@@ -20878,7 +20494,7 @@ function stopSkillScanner() {
20878
20494
  }
20879
20495
 
20880
20496
  // lib/shell-env.ts
20881
- import { execSync as execSync4 } from "child_process";
20497
+ import { execSync as execSync3 } from "child_process";
20882
20498
  var PASSTHROUGH_VARS = ["ALOOK_PROJECT_ROOT", "ALOOK_SERVER_URL", "ALOOK_CMD_PREFIX", "ALOOK_HEALTH_PORT"];
20883
20499
  function resolveLoginShellEnv() {
20884
20500
  if (isWindows) {
@@ -20886,7 +20502,7 @@ function resolveLoginShellEnv() {
20886
20502
  }
20887
20503
  const shell = process.env.SHELL || "/bin/zsh";
20888
20504
  try {
20889
- const output = execSync4(`${shell} -ilc 'env'`, {
20505
+ const output = execSync3(`${shell} -ilc 'env'`, {
20890
20506
  encoding: "utf-8",
20891
20507
  timeout: 5000,
20892
20508
  stdio: ["ignore", "pipe", "ignore"]
@@ -20913,17 +20529,17 @@ function resolveLoginShellEnv() {
20913
20529
  // daemon/daemon.ts
20914
20530
  import { existsSync as existsSync3, mkdirSync as mkdirSync8, openSync, closeSync, readdirSync as readdirSync3, statSync as statSync4, unlinkSync as unlinkSync5 } from "fs";
20915
20531
  import { readdir as readdir2, readFile as readFile2, unlink, stat as fsStat } from "fs/promises";
20916
- import { execSync as execSync5, spawn as spawn6 } from "child_process";
20917
- import { fileURLToPath as fileURLToPath3 } from "url";
20532
+ import { execSync as execSync4, spawn as spawn5 } from "child_process";
20533
+ import { fileURLToPath as fileURLToPath2 } from "url";
20918
20534
  import { dirname as dirname3, join as join11 } from "path";
20919
20535
  var log10 = createLogger2({ module: "daemon" });
20920
- var _dir = dirname3(fileURLToPath3(import.meta.url));
20536
+ var _dir = dirname3(fileURLToPath2(import.meta.url));
20921
20537
  var sessionRunnerPath = existsSync3(join11(_dir, "session-runner.js")) ? join11(_dir, "session-runner.js") : join11(_dir, "session-runner.ts");
20922
20538
  var meetingRunnerPath = existsSync3(join11(_dir, "meeting-runner.js")) ? join11(_dir, "meeting-runner.js") : join11(_dir, "meeting-runner.ts");
20923
- function isCommandAvailable2(cmd) {
20539
+ function isCommandAvailable(cmd) {
20924
20540
  try {
20925
20541
  const check2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
20926
- execSync5(check2, { stdio: "ignore" });
20542
+ execSync4(check2, { stdio: "ignore" });
20927
20543
  return true;
20928
20544
  } catch {
20929
20545
  return false;
@@ -21098,13 +20714,11 @@ async function startDaemon(profile, serverUrl) {
21098
20714
  }
21099
20715
  const cliConfig = loadCLIConfigForProfile(profile);
21100
20716
  const allEntries = cliConfig.watched_workspaces || [];
21101
- const workspaces = allEntries.filter((ws) => ws.status !== "registered" && !!ws.id);
21102
- const registeredEntries = allEntries.filter((ws) => ws.status === "registered" && !ws.id);
21103
- const standbyToken = registeredEntries[0]?.token ?? null;
21104
- if (workspaces.length === 0 && standbyToken) {
21105
- log10.info("No workspaces configured — daemon starting in standby mode with machine token. Awaiting workspace binding.");
21106
- } else if (workspaces.length === 0) {
21107
- log10.info("No workspaces configured — daemon starting in standby mode. Register a workspace to begin.");
20717
+ const workspaces = allEntries.filter((ws) => ws.status !== "deleted" && !!ws.id);
20718
+ if (workspaces.length === 0) {
20719
+ log10.error("No workspaces configured. Register a workspace first.");
20720
+ process.exit(1);
20721
+ return;
21108
20722
  }
21109
20723
  const hasPerWorkspaceTokens = workspaces.every((ws) => !!ws.token);
21110
20724
  if (!hasPerWorkspaceTokens) {
@@ -21122,7 +20736,7 @@ async function startDaemon(profile, serverUrl) {
21122
20736
  ["codex", config2.codexPath],
21123
20737
  ["opencode", config2.opencodePath]
21124
20738
  ]) {
21125
- if (isCommandAvailable2(path2)) {
20739
+ if (isCommandAvailable(path2)) {
21126
20740
  const version3 = await detectVersion2(path2);
21127
20741
  providers.push({ type, path: path2, version: version3 });
21128
20742
  }
@@ -21135,7 +20749,7 @@ async function startDaemon(profile, serverUrl) {
21135
20749
  log10.info(`Detected providers: ${providers.map((p) => `${p.type}@${p.version}`).join(", ")}`);
21136
20750
  const workspaceStates = [];
21137
20751
  const runtimeIndex = new Map;
21138
- let hadWorkspaces = workspaces.length > 0;
20752
+ const hadWorkspaces = true;
21139
20753
  for (const ws of workspaces) {
21140
20754
  const runtimes = providers.map((p) => ({
21141
20755
  type: p.type,
@@ -21202,63 +20816,19 @@ async function startDaemon(profile, serverUrl) {
21202
20816
  function evictWorkspace(workspaceId) {
21203
20817
  const idx = workspaceStates.findIndex((ws2) => ws2.workspaceId === workspaceId);
21204
20818
  if (idx === -1)
21205
- return;
21206
- const ws = workspaceStates[idx];
21207
- for (const rid of ws.runtimeIds) {
21208
- runtimeIndex.delete(rid);
21209
- }
21210
- workspaceStates.splice(idx, 1);
21211
- health.setRuntimeCount(workspaceStates.reduce((sum, w) => sum + w.runtimeIds.length, 0));
21212
- try {
21213
- const cfg = loadCLIConfigForProfile(profile);
21214
- cfg.watched_workspaces = (cfg.watched_workspaces || []).filter((w) => w.id !== workspaceId);
21215
- saveCLIConfigForProfile(profile, cfg);
21216
- } catch {}
21217
- log10.info(`Workspace ${workspaceId} deleted server-side — removed from config`);
21218
- }
21219
- async function handleWorkspaceAdded(workspaceId, workspaceName, token) {
21220
- if (workspaceStates.some((ws) => ws.workspaceId === workspaceId)) {
21221
- log10.info(`Workspace ${workspaceId} already registered — ignoring workspace_added`);
21222
- return;
21223
- }
21224
- log10.info(`Workspace ${workspaceId} bound — registering...`);
21225
- const runtimes = providers.map((p) => ({ type: p.type, version: p.version }));
21226
- try {
21227
- const resp = await client.register(token, {
21228
- workspace_id: workspaceId,
21229
- daemon_id: config2.daemonId,
21230
- device_name: config2.deviceName,
21231
- cli_version: config2.cliVersion,
21232
- workspaces_root: config2.workspacesRoot,
21233
- runtimes
21234
- });
21235
- const runtimeIds = resp.runtimes.map((r) => r.id);
21236
- workspaceStates.push({ workspaceId, token, runtimeIds });
21237
- for (let i = 0;i < runtimeIds.length; i++) {
21238
- runtimeIndex.set(runtimeIds[i], {
21239
- id: runtimeIds[i],
21240
- workspaceId,
21241
- provider: providers[i].type
21242
- });
21243
- }
21244
- hadWorkspaces = true;
21245
- health.setRuntimeCount(workspaceStates.reduce((sum, w) => sum + w.runtimeIds.length, 0));
21246
- try {
21247
- const cfg = loadCLIConfigForProfile(profile);
21248
- const watched = cfg.watched_workspaces || [];
21249
- const registeredIdx = watched.findIndex((w) => w.token === token && w.status === "registered" && !w.id);
21250
- if (registeredIdx !== -1) {
21251
- watched[registeredIdx] = { id: workspaceId, name: workspaceName, token, status: "active", agent_ids: [] };
21252
- } else if (!watched.some((w) => w.id === workspaceId)) {
21253
- watched.push({ id: workspaceId, name: workspaceName, token, status: "active" });
21254
- }
21255
- cfg.watched_workspaces = watched;
21256
- saveCLIConfigForProfile(profile, cfg);
21257
- } catch {}
21258
- log10.info(`Workspace ${workspaceId} added via WS push — ${runtimeIds.length} runtime(s)`);
21259
- } catch (e) {
21260
- log10.error(`Failed to register workspace ${workspaceId} from WS push`, e);
20819
+ return;
20820
+ const ws = workspaceStates[idx];
20821
+ for (const rid of ws.runtimeIds) {
20822
+ runtimeIndex.delete(rid);
21261
20823
  }
20824
+ workspaceStates.splice(idx, 1);
20825
+ health.setRuntimeCount(workspaceStates.reduce((sum, w) => sum + w.runtimeIds.length, 0));
20826
+ try {
20827
+ const cfg = loadCLIConfigForProfile(profile);
20828
+ cfg.watched_workspaces = (cfg.watched_workspaces || []).filter((w) => w.id !== workspaceId);
20829
+ saveCLIConfigForProfile(profile, cfg);
20830
+ } catch {}
20831
+ log10.info(`Workspace ${workspaceId} deleted server-side — removed from config`);
21262
20832
  }
21263
20833
  const pollCycle = async () => {
21264
20834
  let remaining = config2.maxConcurrentTasks - activeTasks.size;
@@ -21448,69 +21018,24 @@ async function startDaemon(profile, serverUrl) {
21448
21018
  }
21449
21019
  break;
21450
21020
  }
21451
- case "daemon.workspace_added": {
21452
- handleWorkspaceAdded(msg.workspaceId, msg.workspaceName, msg.token);
21453
- break;
21454
- }
21455
21021
  }
21456
21022
  }
21457
- const wsToken = firstToken || standbyToken;
21023
+ const wsToken = firstToken;
21458
21024
  let wsClient = wsToken ? new DaemonWsClient({
21459
21025
  serverURL: config2.serverURL,
21460
21026
  daemonId: config2.daemonId,
21461
21027
  machineToken: wsToken,
21462
21028
  onMessage: handleWsPush,
21463
21029
  onConnected: () => {
21464
- if (workspaceStates.length > 0) {
21465
- log10.info("WS connected — switching to low-frequency poll");
21466
- updatePollInterval(config2.wsPollInterval);
21467
- } else {
21468
- log10.info("WS connected in standby mode — awaiting workspace binding");
21469
- }
21030
+ log10.info("WS connected — switching to low-frequency poll");
21031
+ updatePollInterval(config2.wsPollInterval);
21470
21032
  },
21471
21033
  onDisconnected: () => {
21472
- if (workspaceStates.length > 0) {
21473
- log10.info("WS disconnected — reverting to high-frequency poll");
21474
- updatePollInterval(config2.pollInterval);
21475
- }
21034
+ log10.info("WS disconnected — reverting to high-frequency poll");
21035
+ updatePollInterval(config2.pollInterval);
21476
21036
  }
21477
21037
  }) : null;
21478
21038
  wsClient?.connect();
21479
- const STANDBY_POLL_MS = 30000;
21480
- let standbyPollTimer = null;
21481
- if (standbyToken && workspaceStates.length === 0) {
21482
- const standbyPollTick = async () => {
21483
- if (workspaceStates.length > 0) {
21484
- if (standbyPollTimer) {
21485
- clearInterval(standbyPollTimer);
21486
- standbyPollTimer = null;
21487
- }
21488
- return;
21489
- }
21490
- try {
21491
- const runtimes = providers.map((p) => ({ type: p.type, version: p.version }));
21492
- const resp = await client.checkStandby(standbyToken, {
21493
- daemon_id: config2.daemonId,
21494
- device_name: config2.deviceName,
21495
- cli_version: config2.cliVersion,
21496
- workspaces_root: config2.workspacesRoot,
21497
- runtimes
21498
- });
21499
- if (!resp.standby && resp.runtimes.length > 0 && resp.workspaceId) {
21500
- log10.info(`Standby poll: workspace ${resp.workspaceId} discovered via fallback`);
21501
- await handleWorkspaceAdded(resp.workspaceId, "", standbyToken);
21502
- if (standbyPollTimer) {
21503
- clearInterval(standbyPollTimer);
21504
- standbyPollTimer = null;
21505
- }
21506
- }
21507
- } catch (e) {
21508
- log10.debug("standby poll failed", { err: e instanceof Error ? e.message : String(e) });
21509
- }
21510
- };
21511
- standbyPollTick();
21512
- standbyPollTimer = setInterval(standbyPollTick, STANDBY_POLL_MS);
21513
- }
21514
21039
  const sweepTick = async () => {
21515
21040
  for (const ws of workspaceStates) {
21516
21041
  client.sweep(ws.token, config2.daemonId).catch((e) => {
@@ -21548,8 +21073,6 @@ async function startDaemon(profile, serverUrl) {
21548
21073
  clearInterval(pollTimer);
21549
21074
  clearInterval(heartbeatTimer);
21550
21075
  clearInterval(sweepTimer);
21551
- if (standbyPollTimer)
21552
- clearInterval(standbyPollTimer);
21553
21076
  stopSkillScanner();
21554
21077
  wsClient?.close();
21555
21078
  const shutdownMs = restartRequested ? 30000 : Number(process.env.ALOOK_SHUTDOWN_TIMEOUT_MS) || 5000;
@@ -21576,7 +21099,7 @@ async function startDaemon(profile, serverUrl) {
21576
21099
  } catch (e) {
21577
21100
  log10.error(`Failed to open daemon log file ${logPath}`, e);
21578
21101
  }
21579
- const child = spawn6(process.execPath, args, {
21102
+ const child = spawn5(process.execPath, args, {
21580
21103
  detached: true,
21581
21104
  stdio: logFd != null ? ["ignore", logFd, logFd] : ["ignore", "ignore", "ignore"],
21582
21105
  env: resolveLoginShellEnv()
@@ -21598,7 +21121,7 @@ async function startDaemon(profile, serverUrl) {
21598
21121
  log10.info("SIGHUP received — reloading config...");
21599
21122
  try {
21600
21123
  const freshConfig = loadCLIConfigForProfile(profile);
21601
- const freshWorkspaces = (freshConfig.watched_workspaces || []).filter((ws) => ws.status !== "registered" && !!ws.id);
21124
+ const freshWorkspaces = (freshConfig.watched_workspaces || []).filter((ws) => ws.status !== "deleted" && !!ws.id);
21602
21125
  const existingIds = new Set(workspaceStates.map((ws) => ws.workspaceId));
21603
21126
  const newWorkspaces = freshWorkspaces.filter((ws) => ws.token && !existingIds.has(ws.id));
21604
21127
  for (const ws of newWorkspaces) {
@@ -21628,7 +21151,6 @@ async function startDaemon(profile, serverUrl) {
21628
21151
  }
21629
21152
  }
21630
21153
  if (newWorkspaces.length > 0) {
21631
- hadWorkspaces = true;
21632
21154
  health.setRuntimeCount(workspaceStates.reduce((sum, w) => sum + w.runtimeIds.length, 0));
21633
21155
  if (!wsClient && workspaceStates.length > 0) {
21634
21156
  const token = workspaceStates[0].token;
@@ -21671,7 +21193,7 @@ function spawnSessionRunner(input) {
21671
21193
  } catch (e) {
21672
21194
  log10.error(`Failed to open log file ${logFilePath}`, e);
21673
21195
  }
21674
- const child = spawn6(process.execPath, [sessionRunnerPath, encoded], {
21196
+ const child = spawn5(process.execPath, [sessionRunnerPath, encoded], {
21675
21197
  detached: true,
21676
21198
  stdio: fd != null ? ["ignore", fd, fd] : ["ignore", "ignore", "ignore"]
21677
21199
  });
@@ -21691,7 +21213,7 @@ function spawnMeetingRunner(input) {
21691
21213
  } catch (e) {
21692
21214
  log10.error(`Failed to open meeting log file ${logFilePath}`, e);
21693
21215
  }
21694
- const child = spawn6(process.execPath, [meetingRunnerPath, encoded], {
21216
+ const child = spawn5(process.execPath, [meetingRunnerPath, encoded], {
21695
21217
  detached: true,
21696
21218
  stdio: fd != null ? ["ignore", fd, fd] : ["ignore", "ignore", "ignore"]
21697
21219
  });
@@ -21968,66 +21490,499 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
21968
21490
  }
21969
21491
  }
21970
21492
  }
21971
- const cliPath = provider === "claude" ? config2.claudePath : provider === "codex" ? config2.codexPath : config2.opencodePath;
21972
- const configModel = provider === "claude" ? config2.claudeModel : provider === "codex" ? config2.codexModel : config2.opencodeModel;
21973
- const agentModel = task.agent?.runtimeConfig?.model;
21974
- const model = typeof agentModel === "string" && agentModel ? agentModel : configModel;
21975
- const input = {
21976
- task,
21977
- provider,
21978
- cliPath,
21979
- model,
21980
- serverURL: config2.serverURL,
21981
- token,
21982
- workspacesRoot: config2.workspacesRoot,
21983
- agentTimeout: config2.agentTimeout,
21984
- messageInactivityTimeout: config2.messageInactivityTimeout,
21985
- ...promptOverride && { promptOverride }
21986
- };
21987
- const child = spawnSessionRunner(input);
21988
- child.on("close", async (code) => {
21989
- activeTasks.delete(task.id);
21990
- if (code !== 0) {
21991
- const agentBaseDir = join11(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
21992
- const killIntent = readKillIntent(agentBaseDir, task.id);
21993
- if (killIntent) {
21994
- log10.info(`Task ${task.id} exited (${killIntent.reason}) — expected, skipping failTask`);
21995
- clearKillIntent(agentBaseDir, task.id);
21493
+ const cliPath = provider === "claude" ? config2.claudePath : provider === "codex" ? config2.codexPath : config2.opencodePath;
21494
+ const configModel = provider === "claude" ? config2.claudeModel : provider === "codex" ? config2.codexModel : config2.opencodeModel;
21495
+ const agentModel = task.agent?.runtimeConfig?.model;
21496
+ const model = typeof agentModel === "string" && agentModel ? agentModel : configModel;
21497
+ const input = {
21498
+ task,
21499
+ provider,
21500
+ cliPath,
21501
+ model,
21502
+ serverURL: config2.serverURL,
21503
+ token,
21504
+ workspacesRoot: config2.workspacesRoot,
21505
+ agentTimeout: config2.agentTimeout,
21506
+ messageInactivityTimeout: config2.messageInactivityTimeout,
21507
+ ...promptOverride && { promptOverride }
21508
+ };
21509
+ const child = spawnSessionRunner(input);
21510
+ child.on("close", async (code) => {
21511
+ activeTasks.delete(task.id);
21512
+ if (code !== 0) {
21513
+ const agentBaseDir = join11(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
21514
+ const killIntent = readKillIntent(agentBaseDir, task.id);
21515
+ if (killIntent) {
21516
+ log10.info(`Task ${task.id} exited (${killIntent.reason}) — expected, skipping failTask`);
21517
+ clearKillIntent(agentBaseDir, task.id);
21518
+ return;
21519
+ }
21520
+ const errorMsg = code === null ? "killed by signal" : `session-runner exited with code ${code}`;
21521
+ try {
21522
+ await client.failTask(token, task.id, errorMsg);
21523
+ log10.warn(`session-runner crashed (${errorMsg}, task ${task.id})`);
21524
+ const timelineDir = join11(agentBaseDir, ".context_timeline");
21525
+ updateEntry(timelineDir, task.id, (entry) => {
21526
+ entry.pid = null;
21527
+ entry.status = "failed";
21528
+ entry.errmsg = errorMsg;
21529
+ });
21530
+ } catch (e) {
21531
+ if (isClientError3(e)) {
21532
+ log10.info(`Task ${task.id} exited (already terminal) — session-runner handled cleanup`);
21533
+ return;
21534
+ }
21535
+ log10.error(`Failed to report crash for task ${task.id}`, e);
21536
+ try {
21537
+ await writeMarkerFile(config2.workspacesRoot, {
21538
+ taskId: task.id,
21539
+ type: "fail",
21540
+ payload: { error: errorMsg },
21541
+ token,
21542
+ serverURL: config2.serverURL,
21543
+ createdAt: new Date().toISOString()
21544
+ });
21545
+ } catch {}
21546
+ }
21547
+ }
21548
+ });
21549
+ log10.info(`Task ${task.id} dispatched to session-runner (pid=${child.pid})`);
21550
+ }
21551
+
21552
+ // lib/runtimes.ts
21553
+ import { execSync as execSync5 } from "child_process";
21554
+ function isCommandAvailable2(cmd) {
21555
+ try {
21556
+ const check2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
21557
+ execSync5(check2, { stdio: "ignore" });
21558
+ return true;
21559
+ } catch {
21560
+ return false;
21561
+ }
21562
+ }
21563
+ function detectRuntimes() {
21564
+ const found = [];
21565
+ for (const type of ["claude", "codex", "opencode"]) {
21566
+ if (isCommandAvailable2(type)) {
21567
+ let version3 = "";
21568
+ try {
21569
+ version3 = execSync5(`${type} --version`, { encoding: "utf-8" }).trim();
21570
+ } catch {}
21571
+ found.push({ type, version: version3 });
21572
+ }
21573
+ }
21574
+ return found;
21575
+ }
21576
+
21577
+ // lib/activate.ts
21578
+ async function activateAndSave(opts) {
21579
+ const { token, serverUrl, profile } = opts;
21580
+ console.log("Scanning for AI runtimes...");
21581
+ const runtimes = detectRuntimes();
21582
+ if (runtimes.length === 0) {
21583
+ console.error("Error: no runtimes found. Install claude, codex, or opencode first.");
21584
+ process.exit(1);
21585
+ }
21586
+ console.log(`Found: ${runtimes.map((r) => r.type).join(", ")}`);
21587
+ const host = hostname4();
21588
+ console.log("Registering machine...");
21589
+ let activateResp;
21590
+ try {
21591
+ const res = await fetch(`${serverUrl}/api/machine-tokens/activate`, {
21592
+ method: "POST",
21593
+ headers: { "Content-Type": "application/json" },
21594
+ body: JSON.stringify({ token, hostname: host, runtimes })
21595
+ });
21596
+ if (!res.ok) {
21597
+ const text2 = await res.text();
21598
+ console.error(`Error: registration failed (${res.status}): ${text2}`);
21599
+ process.exit(1);
21600
+ }
21601
+ activateResp = await res.json();
21602
+ } catch (err) {
21603
+ console.error(`Error: failed to activate: ${err instanceof Error ? err.message : err}`);
21604
+ process.exit(1);
21605
+ }
21606
+ const client = new APIClient(serverUrl, token);
21607
+ let workspaces;
21608
+ try {
21609
+ workspaces = await client.getJSON("/api/workspaces");
21610
+ } catch (err) {
21611
+ console.error(`Error: failed to fetch workspaces: ${err instanceof Error ? err.message : err}`);
21612
+ process.exit(1);
21613
+ }
21614
+ if (!workspaces.length) {
21615
+ console.error("Error: no workspaces found for this user");
21616
+ process.exit(1);
21617
+ }
21618
+ const ws = workspaces.find((w) => w.id === activateResp.workspace_id) || workspaces[0];
21619
+ const wsClient = new APIClient(serverUrl, token, ws.id);
21620
+ let agentIds = [];
21621
+ try {
21622
+ const agents = await wsClient.getJSON(`/api/agents?workspace_id=${ws.id}`);
21623
+ agentIds = agents.map((a) => a.id);
21624
+ } catch {}
21625
+ const existing = loadCLIConfigForProfile(profile);
21626
+ const watched = existing.watched_workspaces || [];
21627
+ const idx = watched.findIndex((w) => w.id === ws.id);
21628
+ if (idx >= 0) {
21629
+ watched[idx] = { id: ws.id, name: ws.name, token, status: "active", agent_ids: agentIds };
21630
+ } else {
21631
+ watched.push({ id: ws.id, name: ws.name, token, status: "active", agent_ids: agentIds });
21632
+ }
21633
+ saveCLIConfigForProfile(profile, {
21634
+ server_url: serverUrl,
21635
+ watched_workspaces: watched
21636
+ });
21637
+ const daemonPid = readDaemonPid(profile);
21638
+ if (daemonPid && isProcessAlive(daemonPid)) {
21639
+ try {
21640
+ process.kill(daemonPid, "SIGHUP");
21641
+ console.log(`
21642
+ Daemon (pid ${daemonPid}) notified — workspace will be active shortly.`);
21643
+ } catch {
21644
+ console.log(`
21645
+ Daemon is running but could not be notified. Restart it to pick up the new workspace.`);
21646
+ }
21647
+ } else if (isDev() && process.stdout.isTTY) {
21648
+ console.log(`
21649
+ Starting daemon in foreground...`);
21650
+ await startDaemon(profile, serverUrl);
21651
+ } else {
21652
+ console.log(`
21653
+ Starting daemon...`);
21654
+ try {
21655
+ const entry = process.argv[1];
21656
+ const args = [entry];
21657
+ if (profile)
21658
+ args.push("--profile", profile);
21659
+ args.push("daemon", "start", "--foreground");
21660
+ const logPath = daemonLogFilePath();
21661
+ mkdirSync9(dirname4(logPath), { recursive: true, mode: 448 });
21662
+ const logFd = openSync2(logPath, "a", 384);
21663
+ const child = spawn6(process.execPath, args, {
21664
+ detached: true,
21665
+ stdio: ["ignore", logFd, logFd],
21666
+ env: resolveLoginShellEnv()
21667
+ });
21668
+ child.unref();
21669
+ closeSync2(logFd);
21670
+ console.log("Daemon started in background.");
21671
+ console.log(`Logs: ${logPath}`);
21672
+ } catch {
21673
+ console.log(`Failed to auto-start daemon. Run '${cmdPrefix()} daemon start' manually.`);
21674
+ }
21675
+ }
21676
+ return {
21677
+ workspaceId: ws.id,
21678
+ workspaceName: ws.name,
21679
+ runtimeProviders: activateResp.runtimes.map((r) => r.provider)
21680
+ };
21681
+ }
21682
+
21683
+ // commands/register.ts
21684
+ function registerCommand() {
21685
+ const cmd = new Command("register").description("Register CLI with your Alook account").requiredOption("--token <token>", "API token (starts with al_)").option("--server <url>", "Server URL").option("--profile <name>", "Profile name").action(async (opts, command) => {
21686
+ const token = opts.token;
21687
+ const profile = opts.profile || command.parent?.opts().profile;
21688
+ const serverUrl = opts.server || command.parent?.opts().server || getServerUrl();
21689
+ if (!token) {
21690
+ console.error(`Error: --token is required
21691
+ Usage: ${cmdPrefix()} register --token <token>`);
21692
+ process.exit(1);
21693
+ }
21694
+ if (!token.startsWith("al_")) {
21695
+ console.error("Error: invalid token format: must start with 'al_'");
21696
+ process.exit(1);
21697
+ }
21698
+ const client = new APIClient(serverUrl, token);
21699
+ let me;
21700
+ try {
21701
+ me = await client.getJSON("/api/me");
21702
+ } catch (err) {
21703
+ console.error(`Error: failed to verify token: ${err instanceof Error ? err.message : err}`);
21704
+ process.exit(1);
21705
+ }
21706
+ const result = await activateAndSave({ token, serverUrl, profile });
21707
+ console.log(`
21708
+ Registered as ${me.email}`);
21709
+ console.log(`Workspace: ${result.workspaceName} (${result.workspaceId})`);
21710
+ console.log(`Runtimes: ${result.runtimeProviders.join(", ")}`);
21711
+ });
21712
+ return cmd;
21713
+ }
21714
+
21715
+ // commands/login.ts
21716
+ import { Command as Command2 } from "commander";
21717
+ import { fork, spawn as spawn7 } from "child_process";
21718
+ import { fileURLToPath as fileURLToPath3 } from "url";
21719
+ var DEVICE_CLIENT_ID = process.env.ALOOK_DEVICE_CLIENT_ID || "alook-cli";
21720
+ function openBrowser(url2) {
21721
+ try {
21722
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : process.platform === "win32" ? "start" : null;
21723
+ if (cmd) {
21724
+ const args = process.platform === "win32" ? ["", url2] : [url2];
21725
+ spawn7(cmd, args, { stdio: "ignore", detached: true }).unref();
21726
+ }
21727
+ } catch {}
21728
+ }
21729
+ function sleep2(ms) {
21730
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
21731
+ }
21732
+ function syncWorkspacesToConfig(serverWorkspaces, profile, sessionToken) {
21733
+ const cfg = loadCLIConfigForProfile(profile);
21734
+ const watched = cfg.watched_workspaces || [];
21735
+ const serverIds = new Set(serverWorkspaces.map((w) => w.id));
21736
+ for (const sw of serverWorkspaces) {
21737
+ const existing = watched.find((w) => w.id === sw.id);
21738
+ if (existing) {
21739
+ existing.status = "active";
21740
+ existing.name = sw.name;
21741
+ } else {
21742
+ watched.push({ id: sw.id, name: sw.name, token: "", status: "active", agent_ids: [] });
21743
+ }
21744
+ }
21745
+ for (const w of watched) {
21746
+ if (w.id && !serverIds.has(w.id)) {
21747
+ w.status = "deleted";
21748
+ }
21749
+ }
21750
+ saveCLIConfigForProfile(profile, {
21751
+ server_url: cfg.server_url,
21752
+ session_token: sessionToken ?? cfg.session_token,
21753
+ watched_workspaces: watched
21754
+ });
21755
+ }
21756
+ async function pollAndActivate(opts) {
21757
+ const { deviceCode, expiresIn, serverUrl, profile } = opts;
21758
+ let interval = opts.interval;
21759
+ const expiresAt = Date.now() + expiresIn * 1000;
21760
+ let tokenResp;
21761
+ while (Date.now() < expiresAt) {
21762
+ await sleep2(interval);
21763
+ try {
21764
+ const res = await fetch(`${serverUrl}/api/auth/device/token`, {
21765
+ method: "POST",
21766
+ headers: { "Content-Type": "application/json" },
21767
+ body: JSON.stringify({
21768
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
21769
+ device_code: deviceCode,
21770
+ client_id: DEVICE_CLIENT_ID
21771
+ })
21772
+ });
21773
+ if (res.ok) {
21774
+ tokenResp = await res.json();
21775
+ break;
21776
+ }
21777
+ const errBody = await res.json();
21778
+ if (errBody.error === "slow_down") {
21779
+ interval += 5000;
21780
+ } else if (errBody.error === "authorization_pending") {} else if (errBody.error === "expired_token") {
21781
+ console.error("Error: device code expired. Please run login again.");
21782
+ process.exit(1);
21783
+ } else if (errBody.error === "access_denied") {
21784
+ console.error("Error: authorization was denied.");
21785
+ process.exit(1);
21786
+ } else {
21787
+ console.error(`Error: unexpected error: ${errBody.error_description || errBody.error}`);
21788
+ process.exit(1);
21789
+ }
21790
+ } catch {
21791
+ console.error("Error: network request failed during polling.");
21792
+ process.exit(1);
21793
+ }
21794
+ }
21795
+ if (!tokenResp) {
21796
+ console.error("Error: device code expired (timed out). Please run login again.");
21797
+ process.exit(1);
21798
+ }
21799
+ const sessionToken = tokenResp.access_token;
21800
+ const client = new APIClient(serverUrl, sessionToken);
21801
+ let email3 = "";
21802
+ try {
21803
+ const me = await client.getJSON("/api/me");
21804
+ email3 = me.email;
21805
+ } catch {}
21806
+ let serverWorkspaces = [];
21807
+ try {
21808
+ serverWorkspaces = await client.getJSON("/api/workspaces");
21809
+ } catch {}
21810
+ syncWorkspacesToConfig(serverWorkspaces, profile, sessionToken);
21811
+ let workspaceId = serverWorkspaces.length > 0 ? serverWorkspaces[0].id : "";
21812
+ if (!workspaceId) {
21813
+ try {
21814
+ const newWs = await client.postJSON("/api/workspaces", {
21815
+ name: "Personal",
21816
+ slug: email3.split("@")[0]?.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 48) || "personal"
21817
+ });
21818
+ workspaceId = newWs.id;
21819
+ } catch (err) {
21820
+ console.error(`Error: failed to create workspace: ${err instanceof Error ? err.message : err}`);
21821
+ process.exit(1);
21822
+ }
21823
+ }
21824
+ let machineToken2;
21825
+ try {
21826
+ const mtResp = await client.postJSON(`/api/machine-tokens?workspace_id=${workspaceId}`);
21827
+ machineToken2 = mtResp.token;
21828
+ } catch {
21829
+ process.exit(1);
21830
+ }
21831
+ const result = await activateAndSave({ token: machineToken2, serverUrl, profile });
21832
+ if (email3) {
21833
+ console.log(`
21834
+ Logged in as ${email3}`);
21835
+ }
21836
+ console.log(`Workspace: ${result.workspaceName} (${result.workspaceId})`);
21837
+ console.log(`Runtimes: ${result.runtimeProviders.join(", ")}`);
21838
+ }
21839
+ if (process.argv.includes("--__login-poll")) {
21840
+ const idx = process.argv.indexOf("--__login-poll");
21841
+ let data;
21842
+ try {
21843
+ data = JSON.parse(process.argv[idx + 1]);
21844
+ } catch {
21845
+ console.error("Error: invalid poll data");
21846
+ process.exit(1);
21847
+ }
21848
+ pollAndActivate(data).catch(() => process.exit(1));
21849
+ }
21850
+ async function checkExistingAuth(serverUrl, profile) {
21851
+ const config2 = loadCLIConfigForProfile(profile);
21852
+ const sessionToken = config2.session_token;
21853
+ const workspaces = config2.watched_workspaces || [];
21854
+ const ws = workspaces[0];
21855
+ const authToken = sessionToken || ws?.token;
21856
+ if (!authToken) {
21857
+ return { valid: false };
21858
+ }
21859
+ try {
21860
+ const res = await fetch(`${serverUrl}/api/workspaces`, {
21861
+ headers: { Authorization: `Bearer ${authToken}` }
21862
+ });
21863
+ if (!res.ok) {
21864
+ return { valid: false };
21865
+ }
21866
+ const serverWorkspaces = await res.json();
21867
+ const hasValidWorkspace = workspaces.some((w) => w.id && w.status !== "deleted");
21868
+ if (!hasValidWorkspace && serverWorkspaces.length > 0) {
21869
+ syncWorkspacesToConfig(serverWorkspaces, profile);
21870
+ }
21871
+ let email3;
21872
+ try {
21873
+ const meRes = await fetch(`${serverUrl}/api/me`, {
21874
+ headers: { Authorization: `Bearer ${authToken}` }
21875
+ });
21876
+ if (meRes.ok) {
21877
+ const me = await meRes.json();
21878
+ email3 = me.email;
21879
+ }
21880
+ } catch {}
21881
+ const workspaceName = (serverWorkspaces.length > 0 ? serverWorkspaces[0].name : undefined) || ws?.name || undefined;
21882
+ return { valid: true, email: email3, workspaceName };
21883
+ } catch {
21884
+ return { valid: false };
21885
+ }
21886
+ }
21887
+ function loginCommand() {
21888
+ const cmd = new Command2("login").description("Log in to Alook via browser (device code flow)").option("--server <url>", "Server URL").option("--profile <name>", "Profile name").option("--force", "Re-authenticate even if already logged in").action(async (opts, command) => {
21889
+ const profile = opts.profile || command.parent?.opts().profile;
21890
+ const serverUrl = opts.server || command.parent?.opts().server || getServerUrl();
21891
+ if (!opts.force) {
21892
+ const existing = await checkExistingAuth(serverUrl, profile);
21893
+ if (existing.valid) {
21894
+ const parts = ["Already logged in"];
21895
+ if (existing.email)
21896
+ parts[0] += ` as ${existing.email}`;
21897
+ if (existing.workspaceName)
21898
+ parts[0] += ` (workspace: ${existing.workspaceName})`;
21899
+ parts[0] += ".";
21900
+ console.log(parts[0]);
21996
21901
  return;
21997
21902
  }
21998
- const errorMsg = code === null ? "killed by signal" : `session-runner exited with code ${code}`;
21999
- try {
22000
- await client.failTask(token, task.id, errorMsg);
22001
- log10.warn(`session-runner crashed (${errorMsg}, task ${task.id})`);
22002
- const timelineDir = join11(agentBaseDir, ".context_timeline");
22003
- updateEntry(timelineDir, task.id, (entry) => {
22004
- entry.pid = null;
22005
- entry.status = "failed";
22006
- entry.errmsg = errorMsg;
22007
- });
22008
- } catch (e) {
22009
- if (isClientError3(e)) {
22010
- log10.info(`Task ${task.id} exited (already terminal) — session-runner handled cleanup`);
22011
- return;
22012
- }
22013
- log10.error(`Failed to report crash for task ${task.id}`, e);
22014
- try {
22015
- await writeMarkerFile(config2.workspacesRoot, {
22016
- taskId: task.id,
22017
- type: "fail",
22018
- payload: { error: errorMsg },
22019
- token,
22020
- serverURL: config2.serverURL,
22021
- createdAt: new Date().toISOString()
22022
- });
22023
- } catch {}
21903
+ }
21904
+ console.log("Requesting device code...");
21905
+ let deviceResp;
21906
+ try {
21907
+ const res = await fetch(`${serverUrl}/api/auth/device/code`, {
21908
+ method: "POST",
21909
+ headers: { "Content-Type": "application/json" },
21910
+ body: JSON.stringify({ client_id: DEVICE_CLIENT_ID })
21911
+ });
21912
+ if (!res.ok) {
21913
+ const text2 = await res.text();
21914
+ console.error(`Error: failed to get device code (${res.status}): ${text2}`);
21915
+ process.exit(1);
22024
21916
  }
21917
+ deviceResp = await res.json();
21918
+ } catch (err) {
21919
+ console.error(`Error: failed to request device code: ${err instanceof Error ? err.message : err}`);
21920
+ process.exit(1);
21921
+ }
21922
+ const verificationUrl = deviceResp.verification_uri_complete || deviceResp.verification_uri;
21923
+ console.log();
21924
+ console.log(` Open this URL in your browser:`);
21925
+ console.log(` ${verificationUrl}`);
21926
+ console.log();
21927
+ console.log(` Enter code: ${deviceResp.user_code}`);
21928
+ console.log();
21929
+ if (!process.stdout.isTTY) {
21930
+ const pollData = JSON.stringify({
21931
+ deviceCode: deviceResp.device_code,
21932
+ interval: (deviceResp.interval || 5) * 1000,
21933
+ expiresIn: deviceResp.expires_in,
21934
+ serverUrl,
21935
+ profile
21936
+ });
21937
+ const thisFile = fileURLToPath3(import.meta.url);
21938
+ const child = fork(thisFile, ["--__login-poll", pollData], {
21939
+ detached: true,
21940
+ stdio: "ignore"
21941
+ });
21942
+ child.unref();
21943
+ console.log(" Polling for authorization in the background (timeout: 5min).");
21944
+ console.log(` Once approved, run \`${cmdPrefix()} status\` to verify.`);
21945
+ return;
21946
+ }
21947
+ openBrowser(verificationUrl);
21948
+ console.log(" (Browser opened automatically)");
21949
+ console.log();
21950
+ console.log("Waiting for authorization...");
21951
+ await pollAndActivate({
21952
+ deviceCode: deviceResp.device_code,
21953
+ interval: (deviceResp.interval || 5) * 1000,
21954
+ expiresIn: deviceResp.expires_in,
21955
+ serverUrl,
21956
+ profile
21957
+ });
21958
+ });
21959
+ return cmd;
21960
+ }
21961
+
21962
+ // commands/status.ts
21963
+ import { Command as Command3 } from "commander";
21964
+ function statusCommand() {
21965
+ const cmd = new Command3("status").description("Show registration status").action((_opts, command) => {
21966
+ const profile = command.parent?.opts().profile;
21967
+ const cfg = loadCLIConfigForProfile(profile);
21968
+ const ws = cfg.watched_workspaces?.[0];
21969
+ if (!ws?.token) {
21970
+ console.log("Not registered");
21971
+ console.log(`Run '${cmdPrefix()} register --token <token>' to register.`);
21972
+ return;
22025
21973
  }
21974
+ console.log("Status: Registered");
21975
+ console.log(`Server: ${cfg.server_url}`);
21976
+ console.log(`Workspace: ${ws.name} (${ws.id})`);
22026
21977
  });
22027
- log10.info(`Task ${task.id} dispatched to session-runner (pid=${child.pid})`);
21978
+ return cmd;
22028
21979
  }
22029
21980
 
22030
21981
  // commands/daemon.ts
21982
+ import { Command as Command4 } from "commander";
21983
+ import { spawn as spawn8 } from "child_process";
21984
+ import { openSync as openSync3, closeSync as closeSync3, mkdirSync as mkdirSync10 } from "fs";
21985
+ import { dirname as dirname5 } from "path";
22031
21986
  var PID_POLL_INTERVAL_MS = 200;
22032
21987
  var PID_POLL_TIMEOUT_MS = 2000;
22033
21988
  var STOP_POLL_INTERVAL_MS = 200;
@@ -22062,15 +22017,15 @@ async function startInBackground(profile, serverUrl) {
22062
22017
  return;
22063
22018
  }
22064
22019
  const logPath = daemonLogFilePath();
22065
- mkdirSync9(dirname4(logPath), { recursive: true, mode: 448 });
22066
- const logFd = openSync2(logPath, "a", 384);
22067
- const child = spawn7(process.execPath, buildChildArgs(profile, serverUrl), {
22020
+ mkdirSync10(dirname5(logPath), { recursive: true, mode: 448 });
22021
+ const logFd = openSync3(logPath, "a", 384);
22022
+ const child = spawn8(process.execPath, buildChildArgs(profile, serverUrl), {
22068
22023
  detached: true,
22069
22024
  stdio: ["ignore", logFd, logFd],
22070
22025
  env: resolveLoginShellEnv()
22071
22026
  });
22072
22027
  child.unref();
22073
- closeSync2(logFd);
22028
+ closeSync3(logFd);
22074
22029
  const pid = await waitForPidFile(profile);
22075
22030
  if (pid != null) {
22076
22031
  console.log(`Daemon started (pid=${pid})`);
@@ -22183,7 +22138,7 @@ function configCommand() {
22183
22138
 
22184
22139
  // commands/email.ts
22185
22140
  import { Command as Command6 } from "commander";
22186
- import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readFileSync as readFileSync11 } from "fs";
22141
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync11, readFileSync as readFileSync11 } from "fs";
22187
22142
  import { join as join12 } from "path";
22188
22143
  import PostalMime from "postal-mime";
22189
22144
 
@@ -22400,11 +22355,11 @@ function emailCommand() {
22400
22355
  printJSON(emails2);
22401
22356
  return;
22402
22357
  }
22403
- mkdirSync10(emailDir_base, { recursive: true });
22358
+ mkdirSync11(emailDir_base, { recursive: true });
22404
22359
  const downloadedPaths = [];
22405
22360
  for (const email3 of emails2) {
22406
22361
  const emailDir = join12(emailDir_base, email3.id);
22407
- mkdirSync10(emailDir, { recursive: true });
22362
+ mkdirSync11(emailDir, { recursive: true });
22408
22363
  const metadata = {
22409
22364
  id: email3.id,
22410
22365
  from: email3.from_email,
@@ -22443,7 +22398,7 @@ function emailCommand() {
22443
22398
  }
22444
22399
  if (parsed.attachments && parsed.attachments.length > 0) {
22445
22400
  const attDir = join12(emailDir, "attachments");
22446
- mkdirSync10(attDir, { recursive: true });
22401
+ mkdirSync11(attDir, { recursive: true });
22447
22402
  const usedFilenames = new Set;
22448
22403
  for (let i = 0;i < parsed.attachments.length; i++) {
22449
22404
  const att = parsed.attachments[i];
@@ -23332,21 +23287,11 @@ function workspaceCommand() {
23332
23287
  console.error("Error: JSON must contain a 'members' array with at least one member");
23333
23288
  process.exit(1);
23334
23289
  }
23290
+ const parentOpts = getRootOpts(command);
23335
23291
  let targetWorkspaceId = resolvedWorkspaceId;
23336
23292
  if (!targetWorkspaceId) {
23337
23293
  const resolved = await resolveWorkspaceId(client, opts.name || config2.name);
23338
23294
  targetWorkspaceId = resolved.workspaceId;
23339
- const parentOpts = getRootOpts(command);
23340
- const cfg = loadCLIConfigForProfile(parentOpts.profile);
23341
- const registeredWs = cfg.watched_workspaces?.find((w) => w.status === "registered" && !w.id);
23342
- if (registeredWs) {
23343
- try {
23344
- await client.postJSON("/api/machine-tokens/bind-workspace", { workspace_id: targetWorkspaceId });
23345
- console.log("Machine token bound to workspace. Waiting for runtime registration...");
23346
- } catch (err) {
23347
- console.warn(`Warning: bind-workspace failed: ${err instanceof Error ? err.message : err}`);
23348
- }
23349
- }
23350
23295
  let runtimes2 = [];
23351
23296
  const wsClient = new APIClient(serverUrl, token, targetWorkspaceId);
23352
23297
  for (let attempt = 0;attempt < 15; attempt++) {