@alook/cli 0.0.118 → 0.0.120

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,7 +14561,8 @@ 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() })
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() })
14565
14566
  ]);
14566
14567
  var RegisterResponseSchema = exports_external.object({
14567
14568
  runtimes: exports_external.array(exports_external.object({ id: exports_external.string() }))
@@ -14591,6 +14592,9 @@ var RegisterDaemonRequestSchema = exports_external.object({
14591
14592
  workspaces_root: exports_external.string().optional().default(""),
14592
14593
  runtimes: exports_external.array(DaemonRuntimeItemSchema).min(1)
14593
14594
  });
14595
+ var BindWorkspaceRequestSchema = exports_external.object({
14596
+ workspace_id: exports_external.string().min(1)
14597
+ });
14594
14598
  var DeregisterRequestSchema = exports_external.object({
14595
14599
  daemon_id: exports_external.string().min(1)
14596
14600
  });
@@ -14752,7 +14756,8 @@ var CreateConversationRequestSchema = exports_external.object({
14752
14756
  channel: exports_external.string().optional()
14753
14757
  });
14754
14758
  var CreateMessageRequestSchema = exports_external.object({
14755
- content: exports_external.string().min(1, "content is required")
14759
+ content: exports_external.string().min(1, "content is required"),
14760
+ metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
14756
14761
  });
14757
14762
  var AgentDmRequestSchema = exports_external.object({
14758
14763
  content: exports_external.string().min(1, "content is required"),
@@ -16735,6 +16740,8 @@ var machineToken = sqliteTable("machine_token", {
16735
16740
  token: text("token").unique().notNull(),
16736
16741
  name: text("name").notNull().default(""),
16737
16742
  status: text("status").notNull().default("active"),
16743
+ hostname: text("hostname"),
16744
+ runtimesJson: text("runtimes_json"),
16738
16745
  lastUsedAt: text("last_used_at"),
16739
16746
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
16740
16747
  }, (t) => [index("idx_machine_token").on(t.token)]);
@@ -16883,7 +16890,24 @@ function isLocalUrl(url2) {
16883
16890
  return false;
16884
16891
  }
16885
16892
  }
16893
+ function hasWindow() {
16894
+ return typeof globalThis !== "undefined" && "window" in globalThis;
16895
+ }
16896
+ function isTauri() {
16897
+ return hasWindow() && typeof window !== "undefined" && "__TAURI__" in window;
16898
+ }
16899
+ function isMobile() {
16900
+ if (!isTauri())
16901
+ return false;
16902
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
16903
+ return /(android|iphone|ipad|ipod)/i.test(ua);
16904
+ }
16886
16905
  function resolveMode(signals) {
16906
+ if (signals.tauri || isTauri()) {
16907
+ if (signals.tauriPlatform === "mobile" || isMobile())
16908
+ return "mobile";
16909
+ return "desktop";
16910
+ }
16887
16911
  if (signals.nodeEnv === "development" && !signals.cmdPrefix)
16888
16912
  return "dev";
16889
16913
  if (signals.serverUrl && !signals.cmdPrefix && signals.nodeEnv !== "production" && isLocalUrl(signals.serverUrl))
@@ -16900,6 +16924,8 @@ function cliCommand(mode) {
16900
16924
  return "pnpm dev:cli";
16901
16925
  case "app":
16902
16926
  return "npx @alook/app cli";
16927
+ case "desktop":
16928
+ case "mobile":
16903
16929
  case "production":
16904
16930
  return "npx @alook/cli";
16905
16931
  }
@@ -16958,10 +16984,19 @@ function loadCLIConfigForProfile(profile) {
16958
16984
  if (profileName && cfg.profiles?.[profileName]) {
16959
16985
  return cfg.profiles[profileName];
16960
16986
  }
16961
- return {
16987
+ const result = {
16962
16988
  server_url: cfg.server_url || "",
16963
16989
  watched_workspaces: cfg.watched_workspaces || []
16964
16990
  };
16991
+ const legacy = cfg.machine_token;
16992
+ if (legacy && !result.watched_workspaces.some((w) => w.token === legacy)) {
16993
+ result.watched_workspaces.push({ id: null, name: null, token: legacy, status: "registered", agent_ids: [] });
16994
+ }
16995
+ for (const ws of result.watched_workspaces) {
16996
+ if (!ws.status)
16997
+ ws.status = ws.id ? "active" : "registered";
16998
+ }
16999
+ return result;
16965
17000
  }
16966
17001
  function saveCLIConfig(cfg) {
16967
17002
  mkdirSync(configDir(), { recursive: true, mode: 448 });
@@ -16976,6 +17011,7 @@ function saveCLIConfigForProfile(profile, profileConfig) {
16976
17011
  } else {
16977
17012
  cfg.server_url = profileConfig.server_url;
16978
17013
  cfg.watched_workspaces = profileConfig.watched_workspaces;
17014
+ delete cfg.machine_token;
16979
17015
  }
16980
17016
  saveCLIConfig(cfg);
16981
17017
  }
@@ -17296,7 +17332,7 @@ async function activateAndSave(opts) {
17296
17332
  }
17297
17333
  console.log(`Found: ${runtimes.map((r) => r.type).join(", ")}`);
17298
17334
  const host = hostname4();
17299
- console.log("Registering runtime...");
17335
+ console.log("Registering machine...");
17300
17336
  let activateResp;
17301
17337
  try {
17302
17338
  const res = await fetch(`${serverUrl}/api/machine-tokens/activate`, {
@@ -17314,32 +17350,10 @@ async function activateAndSave(opts) {
17314
17350
  console.error(`Error: failed to activate: ${err instanceof Error ? err.message : err}`);
17315
17351
  process.exit(1);
17316
17352
  }
17317
- const client = new APIClient(serverUrl, token);
17318
- let workspaces;
17319
- try {
17320
- workspaces = await client.getJSON("/api/workspaces");
17321
- } catch (err) {
17322
- console.error(`Error: failed to fetch workspaces: ${err instanceof Error ? err.message : err}`);
17323
- process.exit(1);
17324
- }
17325
- if (!workspaces.length) {
17326
- console.error("Error: no workspaces found for this user");
17327
- process.exit(1);
17328
- }
17329
- const ws = workspaces.find((w) => w.id === activateResp.workspace_id) || workspaces[0];
17330
- const wsClient = new APIClient(serverUrl, token, ws.id);
17331
- let agentIds = [];
17332
- try {
17333
- const agents = await wsClient.getJSON(`/api/agents?workspace_id=${ws.id}`);
17334
- agentIds = agents.map((a) => a.id);
17335
- } catch {}
17336
17353
  const existing = loadCLIConfigForProfile(profile);
17337
17354
  const watched = existing.watched_workspaces || [];
17338
- const idx = watched.findIndex((w) => w.id === ws.id);
17339
- if (idx >= 0) {
17340
- watched[idx] = { id: ws.id, name: ws.name, token, agent_ids: agentIds };
17341
- } else {
17342
- watched.push({ id: ws.id, name: ws.name, token, agent_ids: agentIds });
17355
+ if (!watched.some((w) => w.token === token)) {
17356
+ watched.push({ id: null, name: null, token, status: "registered", agent_ids: [] });
17343
17357
  }
17344
17358
  saveCLIConfigForProfile(profile, {
17345
17359
  server_url: serverUrl,
@@ -17350,10 +17364,10 @@ async function activateAndSave(opts) {
17350
17364
  try {
17351
17365
  process.kill(daemonPid, "SIGHUP");
17352
17366
  console.log(`
17353
- Daemon (pid ${daemonPid}) notified — workspace will be active shortly.`);
17367
+ Daemon (pid ${daemonPid}) notified — machine registered, awaiting workspace binding.`);
17354
17368
  } catch {
17355
17369
  console.log(`
17356
- Daemon is running but could not be notified. Restart it to pick up the new workspace.`);
17370
+ Daemon is running but could not be notified. Restart it to pick up the new token.`);
17357
17371
  }
17358
17372
  } else {
17359
17373
  const startCmd = isDev() ? `${cmdPrefix()} daemon start --foreground` : `${cmdPrefix()} daemon start`;
@@ -17361,9 +17375,8 @@ Daemon is running but could not be notified. Restart it to pick up the new works
17361
17375
  console.log(`Run '${startCmd}' to start the daemon.`);
17362
17376
  }
17363
17377
  return {
17364
- workspaceId: ws.id,
17365
- workspaceName: ws.name,
17366
- runtimeProviders: activateResp.runtimes.map((r) => r.provider)
17378
+ daemonId: activateResp.daemon_id,
17379
+ tokenStatus: activateResp.token_status
17367
17380
  };
17368
17381
  }
17369
17382
 
@@ -17393,8 +17406,8 @@ Usage: ${cmdPrefix()} register --token <token>`);
17393
17406
  const result = await activateAndSave({ token, serverUrl, profile });
17394
17407
  console.log(`
17395
17408
  Registered as ${me.email}`);
17396
- console.log(`Workspace: ${result.workspaceName} (${result.workspaceId})`);
17397
- console.log(`Runtimes: ${result.runtimeProviders.join(", ")}`);
17409
+ console.log(`Machine: ${result.daemonId} (status: ${result.tokenStatus})`);
17410
+ console.log(`Workspace binding will happen when you launch a company.`);
17398
17411
  });
17399
17412
  return cmd;
17400
17413
  }
@@ -17486,8 +17499,8 @@ async function pollAndActivate(opts) {
17486
17499
  console.log(`
17487
17500
  Logged in as ${email3}`);
17488
17501
  }
17489
- console.log(`Workspace: ${result.workspaceName} (${result.workspaceId})`);
17490
- console.log(`Runtimes: ${result.runtimeProviders.join(", ")}`);
17502
+ console.log(`Machine: ${result.daemonId} (status: ${result.tokenStatus})`);
17503
+ console.log(`Workspace binding will happen when you launch a company.`);
17491
17504
  }
17492
17505
  if (process.argv.includes("--__login-poll")) {
17493
17506
  const idx = process.argv.indexOf("--__login-poll");
@@ -17527,7 +17540,7 @@ async function checkExistingAuth(serverUrl, profile) {
17527
17540
  email3 = me.email;
17528
17541
  }
17529
17542
  } catch {}
17530
- return { valid: true, email: email3, workspaceName: ws.name };
17543
+ return { valid: true, email: email3, workspaceName: ws.name ?? undefined };
17531
17544
  } catch {
17532
17545
  return { valid: false };
17533
17546
  }
@@ -17752,6 +17765,9 @@ class DaemonClient {
17752
17765
  syncSkills(token, body) {
17753
17766
  return this.request("POST", "/api/daemon/skills/sync", token, body);
17754
17767
  }
17768
+ async checkStandby(token, body) {
17769
+ return this.request("POST", "/api/daemon/register", token, body);
17770
+ }
17755
17771
  }
17756
17772
 
17757
17773
  // daemon/health.ts
@@ -19091,6 +19107,7 @@ The CLI auto-detects your identity from the environment. No need to pass \`--age
19091
19107
  ### Command quick reference
19092
19108
  | Capability | Command |
19093
19109
  |---|---|
19110
+ | Send a message to the user | \`${cmdPrefix()} sync send-dm\` |
19094
19111
  | Schedule / list / edit tasks | \`${cmdPrefix()} calendar set\` (also list, show, update, delete) |
19095
19112
  | Upload a file for your owner | \`${cmdPrefix()} sync upload-artifact\` |
19096
19113
  | Recruit a colleague agent | \`${cmdPrefix()} agent recruit\` |
@@ -19181,11 +19198,24 @@ Upload files for your owner to review in the app.
19181
19198
  ### Talking to the user
19182
19199
  You're texting a colleague, not filing a report. The only thing the user sees is what you send with \`${cmdPrefix()} sync send-dm\` — your task output, reasoning, and tool calls are all off-screen. If you finish without sending, they got silence.
19183
19200
 
19201
+ \`${cmdPrefix()} sync send-dm\` sends a message to **the user** (your owner), not to a colleague agent. This is how you communicate with the human who gave you the task. Use email to talk to colleague agents.
19202
+
19184
19203
  Message at milestones, the way a person would: acknowledge when you pick something up, share a real step forward or a fork in the road, and deliver the result. A quick task is often one message; a long one is a few well-spaced check-ins. Trust your read of the moment — don't narrate every small step, and don't go dark for a long stretch on something they're waiting on.
19185
19204
 
19186
19205
  Say what a colleague would say, not a transcript — the answer in your own voice. (Email- and calendar-triggered tasks have no one watching the chat; use email there.)
19187
19206
 
19188
- \`${cmdPrefix()} sync send-dm --message "…"\` (or \`--message-file <path>\` for long/markdown). The conversation is in $ALOOK_CONVERSATION_ID, so you usually need no flags. You can send several times in one task.
19207
+ **A real person is waiting on the other end.** Send updates at every milestone of your work not just the final result. For any task longer than a minute:
19208
+ 1. **Before you start**: tell them your plan ("I'll research X, then modify Y and Z")
19209
+ 2. **During work**: update when you find something important, change direction, or hit a blocker ("Found the issue — it's in the auth module, fixing now")
19210
+ 3. **When done**: deliver the clear result
19211
+
19212
+ Don't bundle everything into one giant message at the end. The user shouldn't have to sit in silence wondering what's happening. A one-line progress update costs nothing and keeps the human in the loop. But don't send repetitive or near-identical messages — each update should carry new information, not just restate what you already said.
19213
+
19214
+ **If the user sends you a message while you're working** — especially questions like "are you there?", "what's the status?", or unrelated requests — **respond to them immediately**. Don't finish your current task first and then reply. The user reached out because they need your attention NOW. Acknowledge them right away, then resume your work.
19215
+
19216
+ \`${cmdPrefix()} sync send-dm --message "…"\` for short messages. For longer or markdown-rich messages, write to a file first and use \`--message-file <path>\` — this preserves formatting and avoids shell escaping issues. The conversation is in $ALOOK_CONVERSATION_ID, so you usually need no flags. You can send several times in one task.
19217
+
19218
+ Your messages are rendered as **markdown** in the user's app. Use formatting to make your responses clear and scannable — headers, bullet lists, code blocks, bold for key points. Don't send a wall of plain text when structure would help the reader. For anything beyond a one-liner, prefer \`--message-file\` so you can write proper markdown without fighting shell escaping.
19189
19219
 
19190
19220
  ### Attachments
19191
19221
  When your task includes attachments, their local paths are listed in the prompt JSON under "attachments".
@@ -19652,7 +19682,7 @@ function releaseSteeringLock(baseDir, contextKey) {
19652
19682
  }
19653
19683
 
19654
19684
  // daemon/prompt.ts
19655
- var DM_RESPONSE_NOTICE = "Reply with `alook sync send-dm` — that's the only thing the user sees;" + " your task output and reasoning are not shown." + " Talk to them at milestones like a colleague would, and don't end your turn without sending what they need.";
19685
+ var DM_RESPONSE_NOTICE = "Reply with `alook sync send-dm` — that's the only thing the user sees; your task output and reasoning are not shown." + " Talk to them at milestones like a colleague would, and don't end your turn without sending what they need." + " If this task will take more than 30 seconds, send a quick ack first so the user knows you're on it.";
19656
19686
  var EMAIL_NOTICE = "This task was triggered automatically by an incoming email. There is no human in this session." + " If you need to communicate with a human, you MUST send an email using the email sending tool." + " If you need more information or confirmation from the human, send them an email asking for it and then exit." + " Do not wait — when the human replies, a new task will be triggered automatically and you will be woken up with their response.";
19657
19687
  var CALENDAR_NOTICE = "This task was triggered by a scheduled calendar event. There is no human in this session." + " If you need to communicate with a human, you MUST send an email using the email sending tool." + " If you need more information or confirmation, send an email asking for it and then exit." + " Do not wait — when the human replies, a new task will be triggered automatically and you will be woken up with their response.";
19658
19688
  var ISSUE_NOTICE = "This task was triggered by an assigned issue. The issue_id is provided in this message." + " Use `alook issue show --issue_id <issue_id>` to read full context." + " Use `alook issue update --issue_id <issue_id> --status <status>` to change status." + " Use `alook issue comment --issue_id <issue_id> --body <text>` to leave a comment." + " CRITICAL — You MUST manage the issue status correctly. This is NOT optional:" + " 1. Set status to 'in_progress' when you start working." + " 2. If you complete the work yourself: leave a summary comment, then set status to 'review' as your last action. 'review' means there is actual completed work (code, artifact, result) ready for the owner to look at." + " 3. If you delegated work to colleagues and are waiting for their response: KEEP status as 'in_progress' and exit. This is expected — you will be woken up when they reply. Set 'review' only after all delegated work is confirmed complete." + " 4. NEVER set 'review' unless there is concrete completed work for the owner to review. Sending a plan to a colleague is NOT completed work." + " NEVER exit without doing at least one of: updating the status, or leaving a comment explaining what you did and what you're waiting for.";
@@ -19669,6 +19699,13 @@ function buildTaskObject(task, attachments) {
19669
19699
  };
19670
19700
  if (task.type === "user_dm_message") {
19671
19701
  obj.notice = DM_RESPONSE_NOTICE;
19702
+ const ctx = task.context;
19703
+ if (ctx?.message_id) {
19704
+ obj.message_id = ctx.message_id;
19705
+ }
19706
+ if (ctx?.quoted_message) {
19707
+ obj.quoted_message = ctx.quoted_message;
19708
+ }
19672
19709
  }
19673
19710
  if (task.type === "email_notification") {
19674
19711
  const ctx = task.context;
@@ -21015,11 +21052,14 @@ async function startDaemon(profile, serverUrl) {
21015
21052
  }
21016
21053
  }
21017
21054
  const cliConfig = loadCLIConfigForProfile(profile);
21018
- const workspaces = cliConfig.watched_workspaces || [];
21019
- if (workspaces.length === 0) {
21020
- log10.error("No watched workspaces configured.");
21021
- process.exit(1);
21022
- return;
21055
+ const allEntries = cliConfig.watched_workspaces || [];
21056
+ const workspaces = allEntries.filter((ws) => ws.status !== "registered" && !!ws.id);
21057
+ const registeredEntries = allEntries.filter((ws) => ws.status === "registered" && !ws.id);
21058
+ const standbyToken = registeredEntries[0]?.token ?? null;
21059
+ if (workspaces.length === 0 && standbyToken) {
21060
+ log10.info("No workspaces configured — daemon starting in standby mode with machine token. Awaiting workspace binding.");
21061
+ } else if (workspaces.length === 0) {
21062
+ log10.info("No workspaces configured — daemon starting in standby mode. Register a workspace to begin.");
21023
21063
  }
21024
21064
  const hasPerWorkspaceTokens = workspaces.every((ws) => !!ws.token);
21025
21065
  if (!hasPerWorkspaceTokens) {
@@ -21050,6 +21090,7 @@ async function startDaemon(profile, serverUrl) {
21050
21090
  log10.info(`Detected providers: ${providers.map((p) => `${p.type}@${p.version}`).join(", ")}`);
21051
21091
  const workspaceStates = [];
21052
21092
  const runtimeIndex = new Map;
21093
+ let hadWorkspaces = workspaces.length > 0;
21053
21094
  for (const ws of workspaces) {
21054
21095
  const runtimes = providers.map((p) => ({
21055
21096
  type: p.type,
@@ -21085,7 +21126,7 @@ async function startDaemon(profile, serverUrl) {
21085
21126
  });
21086
21127
  }
21087
21128
  }
21088
- if (workspaceStates.length === 0) {
21129
+ if (workspaceStates.length === 0 && workspaces.length > 0) {
21089
21130
  log10.error("No workspaces registered successfully.");
21090
21131
  process.exit(1);
21091
21132
  return;
@@ -21130,6 +21171,50 @@ async function startDaemon(profile, serverUrl) {
21130
21171
  } catch {}
21131
21172
  log10.info(`Workspace ${workspaceId} deleted server-side — removed from config`);
21132
21173
  }
21174
+ async function handleWorkspaceAdded(workspaceId, workspaceName, token) {
21175
+ if (workspaceStates.some((ws) => ws.workspaceId === workspaceId)) {
21176
+ log10.info(`Workspace ${workspaceId} already registered — ignoring workspace_added`);
21177
+ return;
21178
+ }
21179
+ log10.info(`Workspace ${workspaceId} bound — registering...`);
21180
+ const runtimes = providers.map((p) => ({ type: p.type, version: p.version }));
21181
+ try {
21182
+ const resp = await client.register(token, {
21183
+ workspace_id: workspaceId,
21184
+ daemon_id: config2.daemonId,
21185
+ device_name: config2.deviceName,
21186
+ cli_version: config2.cliVersion,
21187
+ workspaces_root: config2.workspacesRoot,
21188
+ runtimes
21189
+ });
21190
+ const runtimeIds = resp.runtimes.map((r) => r.id);
21191
+ workspaceStates.push({ workspaceId, token, runtimeIds });
21192
+ for (let i = 0;i < runtimeIds.length; i++) {
21193
+ runtimeIndex.set(runtimeIds[i], {
21194
+ id: runtimeIds[i],
21195
+ workspaceId,
21196
+ provider: providers[i].type
21197
+ });
21198
+ }
21199
+ hadWorkspaces = true;
21200
+ health.setRuntimeCount(workspaceStates.reduce((sum, w) => sum + w.runtimeIds.length, 0));
21201
+ try {
21202
+ const cfg = loadCLIConfigForProfile(profile);
21203
+ const watched = cfg.watched_workspaces || [];
21204
+ const registeredIdx = watched.findIndex((w) => w.token === token && w.status === "registered" && !w.id);
21205
+ if (registeredIdx !== -1) {
21206
+ watched[registeredIdx] = { id: workspaceId, name: workspaceName, token, status: "active", agent_ids: [] };
21207
+ } else if (!watched.some((w) => w.id === workspaceId)) {
21208
+ watched.push({ id: workspaceId, name: workspaceName, token, status: "active" });
21209
+ }
21210
+ cfg.watched_workspaces = watched;
21211
+ saveCLIConfigForProfile(profile, cfg);
21212
+ } catch {}
21213
+ log10.info(`Workspace ${workspaceId} added via WS push — ${runtimeIds.length} runtime(s)`);
21214
+ } catch (e) {
21215
+ log10.error(`Failed to register workspace ${workspaceId} from WS push`, e);
21216
+ }
21217
+ }
21133
21218
  const pollCycle = async () => {
21134
21219
  let remaining = config2.maxConcurrentTasks - activeTasks.size;
21135
21220
  if (remaining <= 0)
@@ -21205,7 +21290,7 @@ async function startDaemon(profile, serverUrl) {
21205
21290
  for (const id of evictedIds) {
21206
21291
  evictWorkspace(id);
21207
21292
  }
21208
- if (workspaceStates.length === 0) {
21293
+ if (workspaceStates.length === 0 && hadWorkspaces) {
21209
21294
  log10.info("All workspaces evicted — shutting down");
21210
21295
  shutdown();
21211
21296
  }
@@ -21318,23 +21403,69 @@ async function startDaemon(profile, serverUrl) {
21318
21403
  }
21319
21404
  break;
21320
21405
  }
21406
+ case "daemon.workspace_added": {
21407
+ handleWorkspaceAdded(msg.workspaceId, msg.workspaceName, msg.token);
21408
+ break;
21409
+ }
21321
21410
  }
21322
21411
  }
21323
- const wsClient = firstToken ? new DaemonWsClient({
21412
+ const wsToken = firstToken || standbyToken;
21413
+ let wsClient = wsToken ? new DaemonWsClient({
21324
21414
  serverURL: config2.serverURL,
21325
21415
  daemonId: config2.daemonId,
21326
- machineToken: firstToken,
21416
+ machineToken: wsToken,
21327
21417
  onMessage: handleWsPush,
21328
21418
  onConnected: () => {
21329
- log10.info("WS connected switching to low-frequency poll");
21330
- updatePollInterval(config2.wsPollInterval);
21419
+ if (workspaceStates.length > 0) {
21420
+ log10.info("WS connected — switching to low-frequency poll");
21421
+ updatePollInterval(config2.wsPollInterval);
21422
+ } else {
21423
+ log10.info("WS connected in standby mode — awaiting workspace binding");
21424
+ }
21331
21425
  },
21332
21426
  onDisconnected: () => {
21333
- log10.info("WS disconnected reverting to high-frequency poll");
21334
- updatePollInterval(config2.pollInterval);
21427
+ if (workspaceStates.length > 0) {
21428
+ log10.info("WS disconnected — reverting to high-frequency poll");
21429
+ updatePollInterval(config2.pollInterval);
21430
+ }
21335
21431
  }
21336
21432
  }) : null;
21337
21433
  wsClient?.connect();
21434
+ const STANDBY_POLL_MS = 30000;
21435
+ let standbyPollTimer = null;
21436
+ if (standbyToken && workspaceStates.length === 0) {
21437
+ const standbyPollTick = async () => {
21438
+ if (workspaceStates.length > 0) {
21439
+ if (standbyPollTimer) {
21440
+ clearInterval(standbyPollTimer);
21441
+ standbyPollTimer = null;
21442
+ }
21443
+ return;
21444
+ }
21445
+ try {
21446
+ const runtimes = providers.map((p) => ({ type: p.type, version: p.version }));
21447
+ const resp = await client.checkStandby(standbyToken, {
21448
+ daemon_id: config2.daemonId,
21449
+ device_name: config2.deviceName,
21450
+ cli_version: config2.cliVersion,
21451
+ workspaces_root: config2.workspacesRoot,
21452
+ runtimes
21453
+ });
21454
+ if (!resp.standby && resp.runtimes.length > 0 && resp.workspaceId) {
21455
+ log10.info(`Standby poll: workspace ${resp.workspaceId} discovered via fallback`);
21456
+ await handleWorkspaceAdded(resp.workspaceId, "", standbyToken);
21457
+ if (standbyPollTimer) {
21458
+ clearInterval(standbyPollTimer);
21459
+ standbyPollTimer = null;
21460
+ }
21461
+ }
21462
+ } catch (e) {
21463
+ log10.debug("standby poll failed", { err: e instanceof Error ? e.message : String(e) });
21464
+ }
21465
+ };
21466
+ standbyPollTick();
21467
+ standbyPollTimer = setInterval(standbyPollTick, STANDBY_POLL_MS);
21468
+ }
21338
21469
  const sweepTick = async () => {
21339
21470
  for (const ws of workspaceStates) {
21340
21471
  client.sweep(ws.token, config2.daemonId).catch((e) => {
@@ -21372,6 +21503,8 @@ async function startDaemon(profile, serverUrl) {
21372
21503
  clearInterval(pollTimer);
21373
21504
  clearInterval(heartbeatTimer);
21374
21505
  clearInterval(sweepTimer);
21506
+ if (standbyPollTimer)
21507
+ clearInterval(standbyPollTimer);
21375
21508
  stopSkillScanner();
21376
21509
  wsClient?.close();
21377
21510
  const shutdownMs = restartRequested ? 30000 : Number(process.env.ALOOK_SHUTDOWN_TIMEOUT_MS) || 5000;
@@ -21420,7 +21553,7 @@ async function startDaemon(profile, serverUrl) {
21420
21553
  log10.info("SIGHUP received — reloading config...");
21421
21554
  try {
21422
21555
  const freshConfig = loadCLIConfigForProfile(profile);
21423
- const freshWorkspaces = freshConfig.watched_workspaces || [];
21556
+ const freshWorkspaces = (freshConfig.watched_workspaces || []).filter((ws) => ws.status !== "registered" && !!ws.id);
21424
21557
  const existingIds = new Set(workspaceStates.map((ws) => ws.workspaceId));
21425
21558
  const newWorkspaces = freshWorkspaces.filter((ws) => ws.token && !existingIds.has(ws.id));
21426
21559
  for (const ws of newWorkspaces) {
@@ -21450,7 +21583,27 @@ async function startDaemon(profile, serverUrl) {
21450
21583
  }
21451
21584
  }
21452
21585
  if (newWorkspaces.length > 0) {
21586
+ hadWorkspaces = true;
21453
21587
  health.setRuntimeCount(workspaceStates.reduce((sum, w) => sum + w.runtimeIds.length, 0));
21588
+ if (!wsClient && workspaceStates.length > 0) {
21589
+ const token = workspaceStates[0].token;
21590
+ wsClient = new DaemonWsClient({
21591
+ serverURL: config2.serverURL,
21592
+ daemonId: config2.daemonId,
21593
+ machineToken: token,
21594
+ onMessage: handleWsPush,
21595
+ onConnected: () => {
21596
+ log10.info("WS connected — switching to low-frequency poll");
21597
+ updatePollInterval(config2.wsPollInterval);
21598
+ },
21599
+ onDisconnected: () => {
21600
+ log10.info("WS disconnected — reverting to high-frequency poll");
21601
+ updatePollInterval(config2.pollInterval);
21602
+ }
21603
+ });
21604
+ wsClient.connect();
21605
+ log10.info("WS push client initialized after SIGHUP reload");
21606
+ }
21454
21607
  log10.info(`Reload complete — now polling ${workspaceStates.length} workspace(s)`);
21455
21608
  } else {
21456
21609
  log10.info("Reload complete — no new workspaces found");
@@ -22967,7 +23120,7 @@ function versionCommand() {
22967
23120
 
22968
23121
  // commands/update.ts
22969
23122
  import { Command as Command11 } from "commander";
22970
- function updateCommand() {
23123
+ function updateCommand2() {
22971
23124
  const cmd = new Command11("update").description("Update CLI to the latest version").action(async () => {
22972
23125
  const current = getCurrentVersion();
22973
23126
  console.log(`Current version: ${current}`);
@@ -23046,7 +23199,8 @@ function syncCommand() {
23046
23199
  process.exit(1);
23047
23200
  }
23048
23201
  } else {
23049
- content = opts.message ?? "";
23202
+ content = (opts.message ?? "").replace(/\\n/g, `
23203
+ `).replace(/\\t/g, "\t");
23050
23204
  }
23051
23205
  if (!content.trim()) {
23052
23206
  console.error("Error: --message or --message-file is required (and must not be empty)");
@@ -23194,7 +23348,7 @@ program.addCommand(issueCommand());
23194
23348
  program.addCommand(agentCommand());
23195
23349
  program.addCommand(configCommand());
23196
23350
  program.addCommand(versionCommand());
23197
- program.addCommand(updateCommand());
23351
+ program.addCommand(updateCommand2());
23198
23352
  program.addCommand(syncCommand());
23199
23353
  program.addCommand(workspaceCommand());
23200
23354
  program.parse();
@@ -14472,7 +14472,8 @@ var DaemonPushMessageSchema = exports_external.discriminatedUnion("type", [
14472
14472
  exports_external.object({ type: exports_external.literal("daemon.evict"), workspaceId: exports_external.string() }),
14473
14473
  exports_external.object({ type: exports_external.literal("daemon.update"), version: exports_external.string() }),
14474
14474
  exports_external.object({ type: exports_external.literal("daemon.rescan") }),
14475
- 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() })
14475
+ 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() }),
14476
+ exports_external.object({ type: exports_external.literal("daemon.workspace_added"), workspaceId: exports_external.string(), workspaceName: exports_external.string(), token: exports_external.string() })
14476
14477
  ]);
14477
14478
  var RegisterResponseSchema = exports_external.object({
14478
14479
  runtimes: exports_external.array(exports_external.object({ id: exports_external.string() }))
@@ -14502,6 +14503,9 @@ var RegisterDaemonRequestSchema = exports_external.object({
14502
14503
  workspaces_root: exports_external.string().optional().default(""),
14503
14504
  runtimes: exports_external.array(DaemonRuntimeItemSchema).min(1)
14504
14505
  });
14506
+ var BindWorkspaceRequestSchema = exports_external.object({
14507
+ workspace_id: exports_external.string().min(1)
14508
+ });
14505
14509
  var DeregisterRequestSchema = exports_external.object({
14506
14510
  daemon_id: exports_external.string().min(1)
14507
14511
  });
@@ -14663,7 +14667,8 @@ var CreateConversationRequestSchema = exports_external.object({
14663
14667
  channel: exports_external.string().optional()
14664
14668
  });
14665
14669
  var CreateMessageRequestSchema = exports_external.object({
14666
- content: exports_external.string().min(1, "content is required")
14670
+ content: exports_external.string().min(1, "content is required"),
14671
+ metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
14667
14672
  });
14668
14673
  var AgentDmRequestSchema = exports_external.object({
14669
14674
  content: exports_external.string().min(1, "content is required"),
@@ -16646,6 +16651,8 @@ var machineToken = sqliteTable("machine_token", {
16646
16651
  token: text("token").unique().notNull(),
16647
16652
  name: text("name").notNull().default(""),
16648
16653
  status: text("status").notNull().default("active"),
16654
+ hostname: text("hostname"),
16655
+ runtimesJson: text("runtimes_json"),
16649
16656
  lastUsedAt: text("last_used_at"),
16650
16657
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
16651
16658
  }, (t) => [index("idx_machine_token").on(t.token)]);
@@ -16780,7 +16787,24 @@ function isLocalUrl(url2) {
16780
16787
  return false;
16781
16788
  }
16782
16789
  }
16790
+ function hasWindow() {
16791
+ return typeof globalThis !== "undefined" && "window" in globalThis;
16792
+ }
16793
+ function isTauri() {
16794
+ return hasWindow() && typeof window !== "undefined" && "__TAURI__" in window;
16795
+ }
16796
+ function isMobile() {
16797
+ if (!isTauri())
16798
+ return false;
16799
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
16800
+ return /(android|iphone|ipad|ipod)/i.test(ua);
16801
+ }
16783
16802
  function resolveMode(signals) {
16803
+ if (signals.tauri || isTauri()) {
16804
+ if (signals.tauriPlatform === "mobile" || isMobile())
16805
+ return "mobile";
16806
+ return "desktop";
16807
+ }
16784
16808
  if (signals.nodeEnv === "development" && !signals.cmdPrefix)
16785
16809
  return "dev";
16786
16810
  if (signals.serverUrl && !signals.cmdPrefix && signals.nodeEnv !== "production" && isLocalUrl(signals.serverUrl))
@@ -16797,6 +16821,8 @@ function cliCommand(mode) {
16797
16821
  return "pnpm dev:cli";
16798
16822
  case "app":
16799
16823
  return "npx @alook/app cli";
16824
+ case "desktop":
16825
+ case "mobile":
16800
16826
  case "production":
16801
16827
  return "npx @alook/cli";
16802
16828
  }
@@ -16921,6 +16947,9 @@ class DaemonClient {
16921
16947
  syncSkills(token, body) {
16922
16948
  return this.request("POST", "/api/daemon/skills/sync", token, body);
16923
16949
  }
16950
+ async checkStandby(token, body) {
16951
+ return this.request("POST", "/api/daemon/register", token, body);
16952
+ }
16924
16953
  }
16925
16954
 
16926
16955
  // daemon/agent/claude.ts
@@ -18310,6 +18339,7 @@ The CLI auto-detects your identity from the environment. No need to pass \`--age
18310
18339
  ### Command quick reference
18311
18340
  | Capability | Command |
18312
18341
  |---|---|
18342
+ | Send a message to the user | \`${cmdPrefix()} sync send-dm\` |
18313
18343
  | Schedule / list / edit tasks | \`${cmdPrefix()} calendar set\` (also list, show, update, delete) |
18314
18344
  | Upload a file for your owner | \`${cmdPrefix()} sync upload-artifact\` |
18315
18345
  | Recruit a colleague agent | \`${cmdPrefix()} agent recruit\` |
@@ -18400,11 +18430,24 @@ Upload files for your owner to review in the app.
18400
18430
  ### Talking to the user
18401
18431
  You're texting a colleague, not filing a report. The only thing the user sees is what you send with \`${cmdPrefix()} sync send-dm\` — your task output, reasoning, and tool calls are all off-screen. If you finish without sending, they got silence.
18402
18432
 
18433
+ \`${cmdPrefix()} sync send-dm\` sends a message to **the user** (your owner), not to a colleague agent. This is how you communicate with the human who gave you the task. Use email to talk to colleague agents.
18434
+
18403
18435
  Message at milestones, the way a person would: acknowledge when you pick something up, share a real step forward or a fork in the road, and deliver the result. A quick task is often one message; a long one is a few well-spaced check-ins. Trust your read of the moment — don't narrate every small step, and don't go dark for a long stretch on something they're waiting on.
18404
18436
 
18405
18437
  Say what a colleague would say, not a transcript — the answer in your own voice. (Email- and calendar-triggered tasks have no one watching the chat; use email there.)
18406
18438
 
18407
- \`${cmdPrefix()} sync send-dm --message "…"\` (or \`--message-file <path>\` for long/markdown). The conversation is in $ALOOK_CONVERSATION_ID, so you usually need no flags. You can send several times in one task.
18439
+ **A real person is waiting on the other end.** Send updates at every milestone of your work not just the final result. For any task longer than a minute:
18440
+ 1. **Before you start**: tell them your plan ("I'll research X, then modify Y and Z")
18441
+ 2. **During work**: update when you find something important, change direction, or hit a blocker ("Found the issue — it's in the auth module, fixing now")
18442
+ 3. **When done**: deliver the clear result
18443
+
18444
+ Don't bundle everything into one giant message at the end. The user shouldn't have to sit in silence wondering what's happening. A one-line progress update costs nothing and keeps the human in the loop. But don't send repetitive or near-identical messages — each update should carry new information, not just restate what you already said.
18445
+
18446
+ **If the user sends you a message while you're working** — especially questions like "are you there?", "what's the status?", or unrelated requests — **respond to them immediately**. Don't finish your current task first and then reply. The user reached out because they need your attention NOW. Acknowledge them right away, then resume your work.
18447
+
18448
+ \`${cmdPrefix()} sync send-dm --message "…"\` for short messages. For longer or markdown-rich messages, write to a file first and use \`--message-file <path>\` — this preserves formatting and avoids shell escaping issues. The conversation is in $ALOOK_CONVERSATION_ID, so you usually need no flags. You can send several times in one task.
18449
+
18450
+ Your messages are rendered as **markdown** in the user's app. Use formatting to make your responses clear and scannable — headers, bullet lists, code blocks, bold for key points. Don't send a wall of plain text when structure would help the reader. For anything beyond a one-liner, prefer \`--message-file\` so you can write proper markdown without fighting shell escaping.
18408
18451
 
18409
18452
  ### Attachments
18410
18453
  When your task includes attachments, their local paths are listed in the prompt JSON under "attachments".
@@ -18788,7 +18831,7 @@ function clearKillIntent(baseDir, taskId) {
18788
18831
  }
18789
18832
 
18790
18833
  // daemon/prompt.ts
18791
- var DM_RESPONSE_NOTICE = "Reply with `alook sync send-dm` — that's the only thing the user sees;" + " your task output and reasoning are not shown." + " Talk to them at milestones like a colleague would, and don't end your turn without sending what they need.";
18834
+ var DM_RESPONSE_NOTICE = "Reply with `alook sync send-dm` — that's the only thing the user sees; your task output and reasoning are not shown." + " Talk to them at milestones like a colleague would, and don't end your turn without sending what they need." + " If this task will take more than 30 seconds, send a quick ack first so the user knows you're on it.";
18792
18835
  var EMAIL_NOTICE = "This task was triggered automatically by an incoming email. There is no human in this session." + " If you need to communicate with a human, you MUST send an email using the email sending tool." + " If you need more information or confirmation from the human, send them an email asking for it and then exit." + " Do not wait — when the human replies, a new task will be triggered automatically and you will be woken up with their response.";
18793
18836
  var CALENDAR_NOTICE = "This task was triggered by a scheduled calendar event. There is no human in this session." + " If you need to communicate with a human, you MUST send an email using the email sending tool." + " If you need more information or confirmation, send an email asking for it and then exit." + " Do not wait — when the human replies, a new task will be triggered automatically and you will be woken up with their response.";
18794
18837
  var ISSUE_NOTICE = "This task was triggered by an assigned issue. The issue_id is provided in this message." + " Use `alook issue show --issue_id <issue_id>` to read full context." + " Use `alook issue update --issue_id <issue_id> --status <status>` to change status." + " Use `alook issue comment --issue_id <issue_id> --body <text>` to leave a comment." + " CRITICAL — You MUST manage the issue status correctly. This is NOT optional:" + " 1. Set status to 'in_progress' when you start working." + " 2. If you complete the work yourself: leave a summary comment, then set status to 'review' as your last action. 'review' means there is actual completed work (code, artifact, result) ready for the owner to look at." + " 3. If you delegated work to colleagues and are waiting for their response: KEEP status as 'in_progress' and exit. This is expected — you will be woken up when they reply. Set 'review' only after all delegated work is confirmed complete." + " 4. NEVER set 'review' unless there is concrete completed work for the owner to review. Sending a plan to a colleague is NOT completed work." + " NEVER exit without doing at least one of: updating the status, or leaving a comment explaining what you did and what you're waiting for.";
@@ -18805,6 +18848,13 @@ function buildTaskObject(task, attachments) {
18805
18848
  };
18806
18849
  if (task.type === "user_dm_message") {
18807
18850
  obj.notice = DM_RESPONSE_NOTICE;
18851
+ const ctx = task.context;
18852
+ if (ctx?.message_id) {
18853
+ obj.message_id = ctx.message_id;
18854
+ }
18855
+ if (ctx?.quoted_message) {
18856
+ obj.quoted_message = ctx.quoted_message;
18857
+ }
18808
18858
  }
18809
18859
  if (task.type === "email_notification") {
18810
18860
  const ctx = task.context;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/cli",
3
- "version": "0.0.118",
3
+ "version": "0.0.120",
4
4
  "description": "Alook CLI — Enable Your Person Colleague",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/alookai/alook#readme",