@alook/cli 0.0.7 → 0.0.9

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
@@ -15,7 +15,7 @@ var __export = (target, all) => {
15
15
  };
16
16
 
17
17
  // src/index.ts
18
- import { Command as Command8 } from "commander";
18
+ import { Command as Command9 } from "commander";
19
19
 
20
20
  // commands/register.ts
21
21
  import { Command } from "commander";
@@ -167,7 +167,8 @@ function saveCLIConfigForProfile(profile, profileConfig) {
167
167
  // commands/register.ts
168
168
  function isCommandAvailable(cmd) {
169
169
  try {
170
- execSync(`which ${cmd}`, { stdio: "ignore" });
170
+ const check = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
171
+ execSync(check, { stdio: "ignore" });
171
172
  return true;
172
173
  } catch {
173
174
  return false;
@@ -295,9 +296,9 @@ function statusCommand() {
295
296
 
296
297
  // commands/daemon.ts
297
298
  import { Command as Command3 } from "commander";
298
- import { spawn as spawn2 } from "child_process";
299
+ import { spawn as spawn3 } from "child_process";
299
300
  import { openSync as openSync2, closeSync as closeSync2, mkdirSync as mkdirSync4 } from "fs";
300
- import { dirname as dirname3 } from "path";
301
+ import { dirname as dirname4 } from "path";
301
302
 
302
303
  // ../shared/src/constants.ts
303
304
  var TASK_TYPES = {
@@ -13864,6 +13865,7 @@ var ClaimedTaskRowSchema = exports_external.object({
13864
13865
  result: exports_external.unknown().nullable(),
13865
13866
  context: exports_external.unknown().nullable(),
13866
13867
  type: exports_external.string().default(TASK_TYPES.USER_DM_MESSAGE),
13868
+ contextKey: exports_external.string().nullable().optional(),
13867
13869
  sessionId: exports_external.string().nullable(),
13868
13870
  createdAt: exports_external.coerce.date(),
13869
13871
  dispatchedAt: exports_external.coerce.date().nullable(),
@@ -13893,18 +13895,21 @@ var TaskApiBaseSchema = exports_external.object({
13893
13895
  result: exports_external.unknown().nullable(),
13894
13896
  error: exports_external.string().nullable(),
13895
13897
  created_at: exports_external.string(),
13896
- type: exports_external.string()
13898
+ type: exports_external.string(),
13899
+ context_key: exports_external.string().nullable().optional()
13897
13900
  });
13898
13901
  var TaskApiSchema = TaskApiBaseSchema.extend({
13899
13902
  agent: TaskAgentDataApiSchema.nullable().optional()
13900
13903
  });
13901
13904
  var PollRequestSchema = exports_external.object({
13902
13905
  daemon_id: exports_external.string().min(1),
13903
- max_tasks: exports_external.number().int().min(1).default(1)
13906
+ max_tasks: exports_external.number().int().min(1).default(1),
13907
+ cli_version: exports_external.string().optional()
13904
13908
  });
13905
13909
  var PollResponseSchema = exports_external.object({
13906
13910
  tasks: exports_external.array(TaskApiSchema),
13907
- evicted: exports_external.boolean().optional()
13911
+ evicted: exports_external.boolean().optional(),
13912
+ pending_update: exports_external.object({ version: exports_external.string() }).optional()
13908
13913
  });
13909
13914
  var RegisterResponseSchema = exports_external.object({
13910
13915
  runtimes: exports_external.array(exports_external.object({ id: exports_external.string() }))
@@ -14007,6 +14012,9 @@ var CalendarEventApiSchema = exports_external.object({
14007
14012
  created_at: exports_external.string(),
14008
14013
  updated_at: exports_external.string()
14009
14014
  });
14015
+ var AddWhitelistRequestSchema = exports_external.object({
14016
+ email: exports_external.string().email()
14017
+ });
14010
14018
  // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@4.20260418.1_@opentelemetry+api@1.9.1_bun-types@1.3.12_kysely@0.28.16/node_modules/drizzle-orm/entity.js
14011
14019
  var entityKind = Symbol.for("drizzle:entityKind");
14012
14020
  var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
@@ -15443,6 +15451,7 @@ var machine = sqliteTable("machine", {
15443
15451
  deviceInfo: text("device_info").notNull().default(""),
15444
15452
  lastSeenAt: text("last_seen_at"),
15445
15453
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString()),
15454
+ pendingUpdateVersion: text("pending_update_version"),
15446
15455
  updatedAt: text("updated_at").notNull().$defaultFn(() => new Date().toISOString())
15447
15456
  }, (t) => [primaryKey({ columns: [t.workspaceId, t.daemonId] })]);
15448
15457
  var agentRuntime = sqliteTable("agent_runtime", {
@@ -15522,6 +15531,7 @@ var agentTaskQueue = sqliteTable("agent_task_queue", {
15522
15531
  conversationId: text("conversation_id").notNull().references(() => conversation.id),
15523
15532
  prompt: text("prompt").notNull(),
15524
15533
  type: text("type").notNull().default(TASK_TYPES.USER_DM_MESSAGE),
15534
+ contextKey: text("context_key"),
15525
15535
  status: text("status").notNull().default("queued"),
15526
15536
  priority: integer2("priority").notNull().default(0),
15527
15537
  result: text("result", { mode: "json" }),
@@ -15626,6 +15636,20 @@ var RESERVED_HANDLES = new Set([
15626
15636
  "system",
15627
15637
  "alook"
15628
15638
  ]);
15639
+ // ../shared/src/semver.ts
15640
+ function semverGte(a, b) {
15641
+ const pa = a.split(".").map(Number);
15642
+ const pb = b.split(".").map(Number);
15643
+ for (let i = 0;i < Math.max(pa.length, pb.length); i++) {
15644
+ const sa = pa[i] ?? 0;
15645
+ const sb = pb[i] ?? 0;
15646
+ if (sa > sb)
15647
+ return true;
15648
+ if (sa < sb)
15649
+ return false;
15650
+ }
15651
+ return true;
15652
+ }
15629
15653
  // daemon/client.ts
15630
15654
  class DaemonClient {
15631
15655
  baseURL;
@@ -15657,10 +15681,14 @@ class DaemonClient {
15657
15681
  daemon_id: daemonId
15658
15682
  });
15659
15683
  }
15660
- async poll(token, daemonId, maxTasks) {
15661
- const raw = await this.request("POST", "/api/daemon/tasks/poll", token, { daemon_id: daemonId, max_tasks: maxTasks });
15684
+ async poll(token, daemonId, maxTasks, cliVersion) {
15685
+ const raw = await this.request("POST", "/api/daemon/tasks/poll", token, { daemon_id: daemonId, max_tasks: maxTasks, ...cliVersion && { cli_version: cliVersion } });
15662
15686
  const resp = PollResponseSchema.parse(raw);
15663
- return { tasks: resp.tasks, evicted: resp.evicted ?? false };
15687
+ return {
15688
+ tasks: resp.tasks,
15689
+ evicted: resp.evicted ?? false,
15690
+ pending_update: resp.pending_update
15691
+ };
15664
15692
  }
15665
15693
  startTask(token, taskId) {
15666
15694
  return this.request("POST", `/api/daemon/tasks/${taskId}/start`, token);
@@ -15680,22 +15708,44 @@ class DaemonClient {
15680
15708
 
15681
15709
  // daemon/config.ts
15682
15710
  import { hostname as hostname4 } from "os";
15683
- import { join as join2 } from "path";
15711
+ import { join as join3 } from "path";
15712
+
15713
+ // lib/version.ts
15714
+ import { readFileSync as readFileSync2 } from "fs";
15715
+ import { join as join2, dirname } from "path";
15716
+ import { fileURLToPath } from "url";
15717
+ function getCurrentVersion() {
15718
+ const __dirname2 = dirname(fileURLToPath(import.meta.url));
15719
+ const candidates = [
15720
+ join2(__dirname2, "..", "package.json"),
15721
+ join2(__dirname2, "..", "..", "package.json")
15722
+ ];
15723
+ for (const candidate of candidates) {
15724
+ try {
15725
+ const pkg = JSON.parse(readFileSync2(candidate, "utf-8"));
15726
+ if (typeof pkg.version === "string")
15727
+ return pkg.version;
15728
+ } catch {}
15729
+ }
15730
+ return "unknown";
15731
+ }
15732
+
15733
+ // daemon/config.ts
15684
15734
  function pidFilePath(profile) {
15685
15735
  const name = profile ? `daemon_${profile}.pid` : "daemon.pid";
15686
- return join2(configDir(), name);
15736
+ return join3(configDir(), name);
15687
15737
  }
15688
15738
  function daemonLogDir() {
15689
- return join2(configDir(), "daemon", "logs");
15739
+ return join3(configDir(), "daemon", "logs");
15690
15740
  }
15691
15741
  function sessionRunnerLogDir() {
15692
- return join2(configDir(), "daemon", "session-runners");
15742
+ return join3(configDir(), "daemon", "session-runners");
15693
15743
  }
15694
15744
  function daemonLogFilePath(date5 = new Date) {
15695
15745
  const y = date5.getFullYear();
15696
15746
  const m = String(date5.getMonth() + 1).padStart(2, "0");
15697
15747
  const d = String(date5.getDate()).padStart(2, "0");
15698
- return join2(daemonLogDir(), `${y}-${m}-${d}.log`);
15748
+ return join3(daemonLogDir(), `${y}-${m}-${d}.log`);
15699
15749
  }
15700
15750
  function parseDuration(s) {
15701
15751
  if (!s)
@@ -15735,7 +15785,7 @@ function loadDaemonConfig(profile) {
15735
15785
  if (profile && !daemonId.endsWith(`-${profile}`)) {
15736
15786
  daemonId = `${daemonId}-${profile}`;
15737
15787
  }
15738
- const defaultRoot = join2(configDir(), profile ? `workspaces_${profile}` : "workspaces");
15788
+ const defaultRoot = join3(configDir(), profile ? `workspaces_${profile}` : "workspaces");
15739
15789
  const workspacesRoot = process.env.ALOOK_WORKSPACES_ROOT || defaultRoot;
15740
15790
  return {
15741
15791
  serverURL: normalizeServerBaseURL(process.env.ALOOK_SERVER_URL || "https://alook.ai"),
@@ -15752,7 +15802,7 @@ function loadDaemonConfig(profile) {
15752
15802
  deviceName: process.env.ALOOK_DAEMON_DEVICE_NAME || h,
15753
15803
  runtimeName: process.env.ALOOK_AGENT_RUNTIME_NAME || "Local Agent",
15754
15804
  workspacesRoot,
15755
- cliVersion: "0.1.0"
15805
+ cliVersion: getCurrentVersion()
15756
15806
  };
15757
15807
  }
15758
15808
  function normalizeServerBaseURL(url2) {
@@ -15820,6 +15870,7 @@ function fromApiTask(api2) {
15820
15870
  status: api2.status,
15821
15871
  priority: api2.priority,
15822
15872
  type: api2.type,
15873
+ contextKey: api2.context_key ?? null,
15823
15874
  agent: api2.agent ? { name: api2.agent.name, instructions: api2.agent.instructions, emailHandle: api2.agent.email_handle ?? undefined, userEmail: api2.agent.user_email ?? undefined, runtimeConfig: api2.agent.runtime_config ?? undefined } : undefined,
15824
15875
  repos: undefined,
15825
15876
  createdAt: api2.created_at
@@ -15918,8 +15969,8 @@ function createLogger2(level) {
15918
15969
  var log = createLogger2();
15919
15970
 
15920
15971
  // daemon/pidfile.ts
15921
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
15922
- import { dirname } from "path";
15972
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
15973
+ import { dirname as dirname2 } from "path";
15923
15974
  function isProcessAlive(pid) {
15924
15975
  try {
15925
15976
  process.kill(pid, 0);
@@ -15930,7 +15981,7 @@ function isProcessAlive(pid) {
15930
15981
  }
15931
15982
  function readDaemonPid(profile) {
15932
15983
  try {
15933
- const content = readFileSync2(pidFilePath(profile), "utf-8").trim();
15984
+ const content = readFileSync3(pidFilePath(profile), "utf-8").trim();
15934
15985
  const pid = parseInt(content, 10);
15935
15986
  return Number.isNaN(pid) ? null : pid;
15936
15987
  } catch {
@@ -15940,14 +15991,14 @@ function readDaemonPid(profile) {
15940
15991
  function acquireDaemonPid(profile) {
15941
15992
  const pidPath = pidFilePath(profile);
15942
15993
  try {
15943
- const content = readFileSync2(pidPath, "utf-8").trim();
15994
+ const content = readFileSync3(pidPath, "utf-8").trim();
15944
15995
  const existingPid = parseInt(content, 10);
15945
15996
  if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
15946
15997
  log.error(`Another daemon is already running (PID ${existingPid}). ` + `Remove ${pidPath} if this is stale.`);
15947
15998
  return false;
15948
15999
  }
15949
16000
  } catch {}
15950
- mkdirSync2(dirname(pidPath), { recursive: true, mode: 448 });
16001
+ mkdirSync2(dirname2(pidPath), { recursive: true, mode: 448 });
15951
16002
  writeFileSync2(pidPath, String(process.pid), { mode: 384 });
15952
16003
  return true;
15953
16004
  }
@@ -15964,16 +16015,75 @@ function releaseDaemonPid(profile) {
15964
16015
  removePidFileIfMatches(process.pid, profile);
15965
16016
  }
15966
16017
 
16018
+ // lib/update.ts
16019
+ import { spawn } from "child_process";
16020
+ function fetchLatestVersion() {
16021
+ return fetch("https://registry.npmjs.org/@alook/cli/latest").then((res) => {
16022
+ if (!res.ok)
16023
+ return null;
16024
+ return res.json();
16025
+ }).then((data) => data?.version ?? null).catch(() => null);
16026
+ }
16027
+ function runNpmUpdate(targetVersion) {
16028
+ return new Promise((resolve) => {
16029
+ const chunks = [];
16030
+ const child = spawn("npm", ["install", "-g", `@alook/cli@${targetVersion}`], {
16031
+ stdio: ["ignore", "pipe", "pipe"]
16032
+ });
16033
+ child.stdout?.on("data", (d) => chunks.push(d));
16034
+ child.stderr?.on("data", (d) => chunks.push(d));
16035
+ child.on("error", (err) => {
16036
+ resolve({ success: false, output: err.message });
16037
+ });
16038
+ child.on("close", (code) => {
16039
+ const output = Buffer.concat(chunks).toString();
16040
+ resolve({ success: code === 0, output });
16041
+ });
16042
+ });
16043
+ }
16044
+
16045
+ // daemon/update-handler.ts
16046
+ var updating = false;
16047
+ var retryCount = 0;
16048
+ var MAX_RETRIES = 3;
16049
+ function isUpdating() {
16050
+ return updating;
16051
+ }
16052
+ async function handleCliUpdate(version3, onSuccess) {
16053
+ if (updating)
16054
+ return;
16055
+ if (retryCount >= MAX_RETRIES)
16056
+ return;
16057
+ updating = true;
16058
+ try {
16059
+ log.info(`Updating CLI to v${version3}...`);
16060
+ const result = await runNpmUpdate(version3);
16061
+ if (result.success) {
16062
+ log.info(`CLI updated to v${version3} — restarting`);
16063
+ onSuccess();
16064
+ } else {
16065
+ retryCount++;
16066
+ log.error(`CLI update failed (attempt ${retryCount}/${MAX_RETRIES}): ${result.output}`);
16067
+ }
16068
+ } catch (e) {
16069
+ retryCount++;
16070
+ log.error(`CLI update error (attempt ${retryCount}/${MAX_RETRIES})`, e);
16071
+ } finally {
16072
+ updating = false;
16073
+ }
16074
+ }
16075
+
15967
16076
  // daemon/daemon.ts
15968
16077
  import { existsSync, mkdirSync as mkdirSync3, openSync, closeSync, renameSync, readdirSync, statSync, unlinkSync as unlinkSync2 } from "fs";
15969
- import { execSync as execSync3, spawn } from "child_process";
15970
- import { fileURLToPath } from "url";
15971
- import { dirname as dirname2, join as join3 } from "path";
15972
- var _dir = dirname2(fileURLToPath(import.meta.url));
15973
- var sessionRunnerPath = existsSync(join3(_dir, "session-runner.js")) ? join3(_dir, "session-runner.js") : join3(_dir, "session-runner.ts");
16078
+ import { execSync as execSync3, spawn as spawn2 } from "child_process";
16079
+ import { fileURLToPath as fileURLToPath2 } from "url";
16080
+ import { dirname as dirname3, join as join4 } from "path";
16081
+ var _dir = dirname3(fileURLToPath2(import.meta.url));
16082
+ var sessionRunnerPath = existsSync(join4(_dir, "session-runner.js")) ? join4(_dir, "session-runner.js") : join4(_dir, "session-runner.ts");
15974
16083
  function isCommandAvailable2(cmd) {
15975
16084
  try {
15976
- execSync3(`which ${cmd}`, { stdio: "ignore" });
16085
+ const check2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
16086
+ execSync3(check2, { stdio: "ignore" });
15977
16087
  return true;
15978
16088
  } catch {
15979
16089
  return false;
@@ -15991,7 +16101,7 @@ function pruneSessionRunnerLogs() {
15991
16101
  if (entries.length <= MAX_SESSION_RUNNER_LOGS)
15992
16102
  return;
15993
16103
  const withMtime = entries.map((name) => {
15994
- const full = join3(logDir, name);
16104
+ const full = join4(logDir, name);
15995
16105
  try {
15996
16106
  return { name, mtime: statSync(full).mtimeMs };
15997
16107
  } catch {
@@ -16001,7 +16111,7 @@ function pruneSessionRunnerLogs() {
16001
16111
  withMtime.sort((a, b) => b.mtime - a.mtime);
16002
16112
  for (const entry of withMtime.slice(MAX_SESSION_RUNNER_LOGS)) {
16003
16113
  try {
16004
- unlinkSync2(join3(logDir, entry.name));
16114
+ unlinkSync2(join4(logDir, entry.name));
16005
16115
  } catch {}
16006
16116
  }
16007
16117
  }
@@ -16074,7 +16184,16 @@ async function startDaemon(profile, serverUrl) {
16074
16184
  runtimes
16075
16185
  });
16076
16186
  } catch (e) {
16077
- log.error(`Failed to register workspace ${ws.id}, skipping`, e);
16187
+ if (e instanceof Error && e.message.startsWith("HTTP 401")) {
16188
+ log.warn(`Workspace ${ws.id} token invalid — removing from config`);
16189
+ try {
16190
+ const cfg = loadCLIConfigForProfile(profile);
16191
+ cfg.watched_workspaces = (cfg.watched_workspaces || []).filter((w) => w.id !== ws.id);
16192
+ saveCLIConfigForProfile(profile, cfg);
16193
+ } catch {}
16194
+ } else {
16195
+ log.error(`Failed to register workspace ${ws.id}, skipping`, e);
16196
+ }
16078
16197
  continue;
16079
16198
  }
16080
16199
  log.info(`Workspace ${ws.id} registered — ${resp.runtimes.length} runtime(s)`);
@@ -16147,11 +16266,14 @@ async function startDaemon(profile, serverUrl) {
16147
16266
  await new Promise((r) => setTimeout(r, staggerMs));
16148
16267
  }
16149
16268
  try {
16150
- const { tasks: apiTasks, evicted } = await client.poll(ws.token, config2.daemonId, remaining);
16269
+ const { tasks: apiTasks, evicted, pending_update } = await client.poll(ws.token, config2.daemonId, remaining, config2.cliVersion);
16151
16270
  if (evicted) {
16152
16271
  evictedIds.push(ws.workspaceId);
16153
16272
  continue;
16154
16273
  }
16274
+ if (pending_update && !isUpdating()) {
16275
+ handleCliUpdate(pending_update.version, () => requestRestart());
16276
+ }
16155
16277
  for (const apiTask of apiTasks) {
16156
16278
  const task = fromApiTask(apiTask);
16157
16279
  syncAgentId(task.agentId, ws.workspaceId);
@@ -16163,7 +16285,11 @@ async function startDaemon(profile, serverUrl) {
16163
16285
  });
16164
16286
  }
16165
16287
  } catch (e) {
16166
- log.debug("Poll error", e);
16288
+ if (e instanceof Error && e.message.startsWith("HTTP 401")) {
16289
+ evictedIds.push(ws.workspaceId);
16290
+ } else {
16291
+ log.debug("Poll error", e);
16292
+ }
16167
16293
  }
16168
16294
  }
16169
16295
  for (const id of evictedIds) {
@@ -16176,23 +16302,42 @@ async function startDaemon(profile, serverUrl) {
16176
16302
  };
16177
16303
  const pollTimer = setInterval(pollCycle, config2.pollInterval);
16178
16304
  let shuttingDown = false;
16305
+ let restartRequested = false;
16306
+ const requestRestart = () => {
16307
+ restartRequested = true;
16308
+ shutdown();
16309
+ };
16179
16310
  const shutdown = async () => {
16180
16311
  if (shuttingDown)
16181
16312
  return;
16182
16313
  shuttingDown = true;
16183
- log.info("Shutting down...");
16314
+ log.info(restartRequested ? "Restarting..." : "Shutting down...");
16184
16315
  clearInterval(pollTimer);
16185
- const shutdownMs = Number(process.env.ALOOK_SHUTDOWN_TIMEOUT_MS) || 5000;
16316
+ const shutdownMs = restartRequested ? 30000 : Number(process.env.ALOOK_SHUTDOWN_TIMEOUT_MS) || 5000;
16186
16317
  const timeout = setTimeout(() => process.exit(1), shutdownMs);
16187
16318
  try {
16188
16319
  for (const ws of workspaceStates) {
16189
16320
  await client.deregister(ws.token, config2.daemonId);
16190
16321
  }
16191
16322
  } catch {}
16192
- clearTimeout(timeout);
16193
16323
  releaseDaemonPid(profile);
16194
- health.server.close();
16195
- process.exit(0);
16324
+ health.server.close(() => {
16325
+ if (restartRequested) {
16326
+ const args = ["daemon", "start", "--foreground"];
16327
+ if (profile)
16328
+ args.push("--profile", profile);
16329
+ if (serverUrl)
16330
+ args.push("--server", serverUrl);
16331
+ const child = spawn2("alook", args, {
16332
+ detached: true,
16333
+ stdio: ["ignore", "ignore", "ignore"]
16334
+ });
16335
+ child.unref();
16336
+ log.info(`Spawned new daemon (pid=${child.pid})`);
16337
+ }
16338
+ clearTimeout(timeout);
16339
+ process.exit(0);
16340
+ });
16196
16341
  };
16197
16342
  process.on("SIGTERM", shutdown);
16198
16343
  process.on("SIGINT", shutdown);
@@ -16202,16 +16347,16 @@ function spawnSessionRunner(input) {
16202
16347
  const encoded = Buffer.from(JSON.stringify(input)).toString("base64");
16203
16348
  const logDir = sessionRunnerLogDir();
16204
16349
  mkdirSync3(logDir, { recursive: true });
16205
- const tmpLogPath = join3(logDir, `${input.task.id}.log`);
16350
+ const tmpLogPath = join4(logDir, `${input.task.id}.log`);
16206
16351
  const fd = openSync(tmpLogPath, "a");
16207
- const child = spawn(process.execPath, [sessionRunnerPath, encoded], {
16352
+ const child = spawn2(process.execPath, [sessionRunnerPath, encoded], {
16208
16353
  detached: true,
16209
16354
  stdio: ["ignore", fd, fd]
16210
16355
  });
16211
16356
  child.unref();
16212
16357
  closeSync(fd);
16213
16358
  if (child.pid) {
16214
- const pidLogPath = join3(logDir, `${child.pid}.log`);
16359
+ const pidLogPath = join4(logDir, `${child.pid}.log`);
16215
16360
  renameSync(tmpLogPath, pidLogPath);
16216
16361
  }
16217
16362
  return child;
@@ -16286,9 +16431,9 @@ async function startInBackground(profile, serverUrl) {
16286
16431
  return;
16287
16432
  }
16288
16433
  const logPath = daemonLogFilePath();
16289
- mkdirSync4(dirname3(logPath), { recursive: true, mode: 448 });
16434
+ mkdirSync4(dirname4(logPath), { recursive: true, mode: 448 });
16290
16435
  const logFd = openSync2(logPath, "a", 384);
16291
- const child = spawn2(process.execPath, buildChildArgs(profile, serverUrl), {
16436
+ const child = spawn3(process.execPath, buildChildArgs(profile, serverUrl), {
16292
16437
  detached: true,
16293
16438
  stdio: ["ignore", logFd, logFd]
16294
16439
  });
@@ -16396,8 +16541,8 @@ function configCommand() {
16396
16541
 
16397
16542
  // commands/email.ts
16398
16543
  import { Command as Command5 } from "commander";
16399
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
16400
- import { basename, join as join4 } from "path";
16544
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
16545
+ import { basename, join as join5 } from "path";
16401
16546
  import PostalMime from "postal-mime";
16402
16547
  var VALID_STATUSES = ["unread", "read", "archived"];
16403
16548
  var EMAIL_DIR = "/tmp/alook-emails";
@@ -16473,7 +16618,7 @@ function emailCommand() {
16473
16618
  mkdirSync5(EMAIL_DIR, { recursive: true });
16474
16619
  const downloadedPaths = [];
16475
16620
  for (const email3 of emails2) {
16476
- const emailDir = join4(EMAIL_DIR, email3.id);
16621
+ const emailDir = join5(EMAIL_DIR, email3.id);
16477
16622
  mkdirSync5(emailDir, { recursive: true });
16478
16623
  const metadata = {
16479
16624
  id: email3.id,
@@ -16486,7 +16631,7 @@ function emailCommand() {
16486
16631
  in_reply_to: email3.in_reply_to || "",
16487
16632
  references: email3.references || ""
16488
16633
  };
16489
- const metadataPath = join4(emailDir, "metadata.json");
16634
+ const metadataPath = join5(emailDir, "metadata.json");
16490
16635
  writeFileSync3(metadataPath, JSON.stringify(metadata, null, 2));
16491
16636
  downloadedPaths.push(metadataPath);
16492
16637
  let rawMime;
@@ -16502,17 +16647,17 @@ function emailCommand() {
16502
16647
  }
16503
16648
  const parsed = await new PostalMime().parse(rawMime);
16504
16649
  if (parsed.text) {
16505
- const bodyPath = join4(emailDir, "body.txt");
16650
+ const bodyPath = join5(emailDir, "body.txt");
16506
16651
  writeFileSync3(bodyPath, parsed.text);
16507
16652
  downloadedPaths.push(bodyPath);
16508
16653
  }
16509
16654
  if (parsed.html) {
16510
- const htmlPath = join4(emailDir, "body.html");
16655
+ const htmlPath = join5(emailDir, "body.html");
16511
16656
  writeFileSync3(htmlPath, parsed.html);
16512
16657
  downloadedPaths.push(htmlPath);
16513
16658
  }
16514
16659
  if (parsed.attachments && parsed.attachments.length > 0) {
16515
- const attDir = join4(emailDir, "attachments");
16660
+ const attDir = join5(emailDir, "attachments");
16516
16661
  mkdirSync5(attDir, { recursive: true });
16517
16662
  const usedFilenames = new Set;
16518
16663
  for (let i = 0;i < parsed.attachments.length; i++) {
@@ -16522,7 +16667,7 @@ function emailCommand() {
16522
16667
  filename = `${i}-${filename}`;
16523
16668
  }
16524
16669
  usedFilenames.add(filename);
16525
- const attPath = join4(attDir, filename);
16670
+ const attPath = join5(attDir, filename);
16526
16671
  const content = att.content;
16527
16672
  let buf;
16528
16673
  if (typeof content === "string") {
@@ -16571,7 +16716,7 @@ function emailCommand() {
16571
16716
  const client = new APIClient(serverUrl, token, workspaceId);
16572
16717
  let htmlBody;
16573
16718
  try {
16574
- htmlBody = readFileSync3(opts.bodyFile, "utf-8");
16719
+ htmlBody = readFileSync4(opts.bodyFile, "utf-8");
16575
16720
  } catch (err) {
16576
16721
  console.error(`Error: cannot read body file "${opts.bodyFile}": ${err instanceof Error ? err.message : err}`);
16577
16722
  process.exit(1);
@@ -16587,7 +16732,7 @@ function emailCommand() {
16587
16732
  let bytes;
16588
16733
  let size;
16589
16734
  try {
16590
- bytes = readFileSync3(path);
16735
+ bytes = readFileSync4(path);
16591
16736
  size = statSync2(path).size;
16592
16737
  } catch (err) {
16593
16738
  console.error(`Error: cannot read attachment "${path}": ${err instanceof Error ? err.message : err}`);
@@ -16846,28 +16991,6 @@ function calendarCommand() {
16846
16991
 
16847
16992
  // commands/version.ts
16848
16993
  import { Command as Command7 } from "commander";
16849
-
16850
- // lib/version.ts
16851
- import { readFileSync as readFileSync4 } from "fs";
16852
- import { join as join5, dirname as dirname4 } from "path";
16853
- import { fileURLToPath as fileURLToPath2 } from "url";
16854
- function getCurrentVersion() {
16855
- const __dirname2 = dirname4(fileURLToPath2(import.meta.url));
16856
- const candidates = [
16857
- join5(__dirname2, "..", "package.json"),
16858
- join5(__dirname2, "..", "..", "package.json")
16859
- ];
16860
- for (const candidate of candidates) {
16861
- try {
16862
- const pkg = JSON.parse(readFileSync4(candidate, "utf-8"));
16863
- if (typeof pkg.version === "string")
16864
- return pkg.version;
16865
- } catch {}
16866
- }
16867
- return "unknown";
16868
- }
16869
-
16870
- // commands/version.ts
16871
16994
  function versionCommand() {
16872
16995
  const cmd = new Command7("version").description("Show CLI version").action(() => {
16873
16996
  console.log(`alook version ${getCurrentVersion()}`);
@@ -16875,8 +16998,44 @@ function versionCommand() {
16875
16998
  return cmd;
16876
16999
  }
16877
17000
 
17001
+ // commands/update.ts
17002
+ import { Command as Command8 } from "commander";
17003
+ function updateCommand() {
17004
+ const cmd = new Command8("update").description("Update CLI to the latest version").action(async () => {
17005
+ const current = getCurrentVersion();
17006
+ console.log(`Current version: ${current}`);
17007
+ const latest = await fetchLatestVersion();
17008
+ if (!latest) {
17009
+ console.error("Failed to fetch latest version from npm registry.");
17010
+ process.exit(1);
17011
+ return;
17012
+ }
17013
+ if (semverGte(current, latest)) {
17014
+ console.log(`Already up to date (v${current}).`);
17015
+ return;
17016
+ }
17017
+ const healthPort = Number(process.env.ALOOK_HEALTH_PORT) || 19514;
17018
+ try {
17019
+ const res = await fetch(`http://127.0.0.1:${healthPort}/health`);
17020
+ if (res.ok) {
17021
+ console.warn("Warning: daemon is running on the old version. After update, restart with: alook daemon restart");
17022
+ }
17023
+ } catch {}
17024
+ console.log(`Updating to v${latest}...`);
17025
+ const result = await runNpmUpdate(latest);
17026
+ if (result.success) {
17027
+ console.log(`Updated successfully: v${current} → v${latest}`);
17028
+ } else {
17029
+ console.error(`Update failed:
17030
+ ${result.output}`);
17031
+ process.exit(1);
17032
+ }
17033
+ });
17034
+ return cmd;
17035
+ }
17036
+
16878
17037
  // src/index.ts
16879
- var program = new Command8;
17038
+ var program = new Command9;
16880
17039
  program.name("alook").description("Alook CLI").option("--server <url>", "Server URL").option("--profile <name>", "Profile name");
16881
17040
  program.addCommand(registerCommand());
16882
17041
  program.addCommand(statusCommand());
@@ -16885,4 +17044,5 @@ program.addCommand(emailCommand());
16885
17044
  program.addCommand(calendarCommand());
16886
17045
  program.addCommand(configCommand());
16887
17046
  program.addCommand(versionCommand());
17047
+ program.addCommand(updateCommand());
16888
17048
  program.parse();
@@ -13581,6 +13581,7 @@ var ClaimedTaskRowSchema = exports_external.object({
13581
13581
  result: exports_external.unknown().nullable(),
13582
13582
  context: exports_external.unknown().nullable(),
13583
13583
  type: exports_external.string().default(TASK_TYPES.USER_DM_MESSAGE),
13584
+ contextKey: exports_external.string().nullable().optional(),
13584
13585
  sessionId: exports_external.string().nullable(),
13585
13586
  createdAt: exports_external.coerce.date(),
13586
13587
  dispatchedAt: exports_external.coerce.date().nullable(),
@@ -13610,18 +13611,21 @@ var TaskApiBaseSchema = exports_external.object({
13610
13611
  result: exports_external.unknown().nullable(),
13611
13612
  error: exports_external.string().nullable(),
13612
13613
  created_at: exports_external.string(),
13613
- type: exports_external.string()
13614
+ type: exports_external.string(),
13615
+ context_key: exports_external.string().nullable().optional()
13614
13616
  });
13615
13617
  var TaskApiSchema = TaskApiBaseSchema.extend({
13616
13618
  agent: TaskAgentDataApiSchema.nullable().optional()
13617
13619
  });
13618
13620
  var PollRequestSchema = exports_external.object({
13619
13621
  daemon_id: exports_external.string().min(1),
13620
- max_tasks: exports_external.number().int().min(1).default(1)
13622
+ max_tasks: exports_external.number().int().min(1).default(1),
13623
+ cli_version: exports_external.string().optional()
13621
13624
  });
13622
13625
  var PollResponseSchema = exports_external.object({
13623
13626
  tasks: exports_external.array(TaskApiSchema),
13624
- evicted: exports_external.boolean().optional()
13627
+ evicted: exports_external.boolean().optional(),
13628
+ pending_update: exports_external.object({ version: exports_external.string() }).optional()
13625
13629
  });
13626
13630
  var RegisterResponseSchema = exports_external.object({
13627
13631
  runtimes: exports_external.array(exports_external.object({ id: exports_external.string() }))
@@ -13724,6 +13728,9 @@ var CalendarEventApiSchema = exports_external.object({
13724
13728
  created_at: exports_external.string(),
13725
13729
  updated_at: exports_external.string()
13726
13730
  });
13731
+ var AddWhitelistRequestSchema = exports_external.object({
13732
+ email: exports_external.string().email()
13733
+ });
13727
13734
  // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@4.20260418.1_@opentelemetry+api@1.9.1_bun-types@1.3.12_kysely@0.28.16/node_modules/drizzle-orm/entity.js
13728
13735
  var entityKind = Symbol.for("drizzle:entityKind");
13729
13736
  var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
@@ -15160,6 +15167,7 @@ var machine = sqliteTable("machine", {
15160
15167
  deviceInfo: text("device_info").notNull().default(""),
15161
15168
  lastSeenAt: text("last_seen_at"),
15162
15169
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString()),
15170
+ pendingUpdateVersion: text("pending_update_version"),
15163
15171
  updatedAt: text("updated_at").notNull().$defaultFn(() => new Date().toISOString())
15164
15172
  }, (t) => [primaryKey({ columns: [t.workspaceId, t.daemonId] })]);
15165
15173
  var agentRuntime = sqliteTable("agent_runtime", {
@@ -15239,6 +15247,7 @@ var agentTaskQueue = sqliteTable("agent_task_queue", {
15239
15247
  conversationId: text("conversation_id").notNull().references(() => conversation.id),
15240
15248
  prompt: text("prompt").notNull(),
15241
15249
  type: text("type").notNull().default(TASK_TYPES.USER_DM_MESSAGE),
15250
+ contextKey: text("context_key"),
15242
15251
  status: text("status").notNull().default("queued"),
15243
15252
  priority: integer2("priority").notNull().default(0),
15244
15253
  result: text("result", { mode: "json" }),
@@ -15377,10 +15386,14 @@ class DaemonClient {
15377
15386
  daemon_id: daemonId
15378
15387
  });
15379
15388
  }
15380
- async poll(token, daemonId, maxTasks) {
15381
- const raw = await this.request("POST", "/api/daemon/tasks/poll", token, { daemon_id: daemonId, max_tasks: maxTasks });
15389
+ async poll(token, daemonId, maxTasks, cliVersion) {
15390
+ const raw = await this.request("POST", "/api/daemon/tasks/poll", token, { daemon_id: daemonId, max_tasks: maxTasks, ...cliVersion && { cli_version: cliVersion } });
15382
15391
  const resp = PollResponseSchema.parse(raw);
15383
- return { tasks: resp.tasks, evicted: resp.evicted ?? false };
15392
+ return {
15393
+ tasks: resp.tasks,
15394
+ evicted: resp.evicted ?? false,
15395
+ pending_update: resp.pending_update
15396
+ };
15384
15397
  }
15385
15398
  startTask(token, taskId) {
15386
15399
  return this.request("POST", `/api/daemon/tasks/${taskId}/start`, token);
@@ -16340,8 +16353,8 @@ var SYSTEM_PROMPT_BODY = `## Memory Management
16340
16353
  - For SPECIFIC yet LONG rules or pattern, write to experiences/[NAME].md, and add index to ./memory.md for later recall.
16341
16354
  ### whats is ESSENTIAL and SHORT Memory?
16342
16355
  - basic user profile, e.g.:
16343
- - "user name is gus"
16344
- - "user is working on alook"
16356
+ - "user name is ..."
16357
+ - "user is working on ..."
16345
16358
  - certain local project mapping, e.g.:
16346
16359
  - "alook means the project under /user/home/alook/"
16347
16360
  - when to read certain stuff, e.g.:
@@ -16378,6 +16391,7 @@ those json are sorted by datetime in asc order.
16378
16391
  - When you start a new task, read the last ~10 lines of today's timeline to understand what has been asked and done recently.
16379
16392
  - if you don't know the current datetime, obtain the current datetime first.
16380
16393
  - When user ask you something you don't have in your current context, try to read the timeline jsonl files for answer (today or previous days).
16394
+ - When access other local projects, make sure you read the CLAUDE.md/AGENTS.md file under the project root dir to understand the requirements.
16381
16395
  `;
16382
16396
  function buildInstructionContent(task) {
16383
16397
  const displayName = task.agent?.name || "Alook Agent";
@@ -16431,18 +16445,22 @@ To reply to an email, add '--in-reply-to <EMAIL_ID>' to the send command. This s
16431
16445
  ### Calendar
16432
16446
  You have your own calendar to setup daily routines and reminders.
16433
16447
  Schedule future tasks for yourself. At the scheduled time, a new task is dispatched to you with the event as the prompt (task type 'calendar_event').
16448
+
16449
+ !USE Calendar when you think the tasks are recurring or it should be conducted in the future.
16450
+ !When scheduling calendar events relative to a weekday (e.g. "every Monday"), always run date '+%A' first to confirm today's weekday before calculating the target date
16434
16451
  ---
16452
+ Keep the event title informative and concise, less than 20 words.
16453
+ Place the event details in description.
16435
16454
  Create a one-off event:
16436
- - Run 'npx @alook/cli calendar set --agent_id ${task.agentId} --event_title "<PROMPT_TEXT>" --datetime <YYYY-MM-DDTHH:MM>'
16455
+ - Run 'npx @alook/cli calendar set --agent_id ${task.agentId} --event_title "<TASK_TITLE>" --description "<TASK_BODY>" --datetime <YYYY-MM-DDTHH:MM>'
16437
16456
  - '--datetime' is LOCAL time, format 'YYYY-MM-DDTHH:MM' (e.g. '2026-04-17T09:30'). Do NOT pass UTC / ISO strings with 'Z'.
16438
16457
  - '--event_title' becomes the task prompt when the event fires — write it as the instruction you want future-you to receive.
16439
- - Optional '--description "<text>"' — longer notes/context shown alongside the event in the web UI. Use it for anything that wouldn't fit cleanly in the title.
16440
16458
 
16441
16459
  Create a repeating event:
16442
16460
  - Add '--repeat <interval>' where interval is like '1day', '2hour', '1week', '1month'.
16443
16461
  - Optionally add '--repeat_stop_date <YYYY-MM-DD>' to stop the recurrence (local date).
16444
- - Example: 'npx @alook/cli calendar set --agent_id ${task.agentId} --event_title "daily standup summary" --datetime 2026-04-18T09:00 --repeat 1day --repeat_stop_date 2026-05-18'
16445
-
16462
+ - Example: 'npx @alook/cli calendar set --agent_id ${task.agentId} --event_title "<REPEAT_TASK_TITLE>" --description "<REPEAT_TASK_BODY>" --datetime 2026-04-18T09:00 --repeat 1day --repeat_stop_date 2026-05-18'
16463
+ ---
16446
16464
  List upcoming events:
16447
16465
  - Run 'npx @alook/cli calendar list --agent_id ${task.agentId}' (defaults: next 30 days, past 0 days).
16448
16466
  - Tune the window with '--future_days <N>' and '--past_days <N>'. Add '--json' for machine-readable output.
@@ -16775,9 +16793,10 @@ function updateEntry(timelineDir, taskId, updater) {
16775
16793
  }
16776
16794
  log.debug(`Timeline updateEntry: task_id ${taskId} not found in last 7 days`);
16777
16795
  }
16778
- function createTimelineEntry(taskId, prompt, type, sessionId, pid, provider) {
16796
+ function createTimelineEntry(taskId, prompt, type, sessionId, pid, provider, contextKey) {
16779
16797
  return {
16780
16798
  task_id: taskId,
16799
+ context_key: contextKey ?? null,
16781
16800
  session_id: sessionId || null,
16782
16801
  pid: pid ?? null,
16783
16802
  status: "running",
@@ -16790,7 +16809,9 @@ function createTimelineEntry(taskId, prompt, type, sessionId, pid, provider) {
16790
16809
  };
16791
16810
  }
16792
16811
  var DEFAULT_RESUME_MAX_AGE_MS = 3 * 60 * 60 * 1000;
16793
- function findResumableSessionId(timelineDir, type, provider, maxAgeMs = DEFAULT_RESUME_MAX_AGE_MS) {
16812
+ var EMAIL_RESUME_MAX_AGE_MS = 48 * 60 * 60 * 1000;
16813
+ function findResumableSessionByContextKey(timelineDir, contextKey, provider) {
16814
+ const maxAgeMs = contextKey.startsWith("email:") ? EMAIL_RESUME_MAX_AGE_MS : DEFAULT_RESUME_MAX_AGE_MS;
16794
16815
  const now = new Date;
16795
16816
  const cutoff = new Date(now.getTime() - maxAgeMs);
16796
16817
  const daysToScan = Math.ceil(maxAgeMs / 86400000) + 1;
@@ -16800,7 +16821,7 @@ function findResumableSessionId(timelineDir, type, provider, maxAgeMs = DEFAULT_
16800
16821
  }
16801
16822
  entries.sort((a, b) => new Date(b.datetime).getTime() - new Date(a.datetime).getTime());
16802
16823
  for (const entry of entries) {
16803
- if (entry.status === "completed" && entry.type === type && entry.provider === provider && entry.session_id && new Date(entry.datetime) >= cutoff) {
16824
+ if (entry.status === "completed" && entry.context_key === contextKey && entry.provider === provider && entry.session_id && new Date(entry.datetime) >= cutoff) {
16804
16825
  return entry.session_id;
16805
16826
  }
16806
16827
  }
@@ -16838,9 +16859,9 @@ async function runSession(input) {
16838
16859
  const backend = createBackend(provider, cliPath);
16839
16860
  const prompt = buildPrompt(task);
16840
16861
  const { workDir, logFile, timelineDir, env } = prepare({ workspacesRoot }, task);
16841
- const resumeSessionId = task.type === TASK_TYPES.USER_DM_MESSAGE ? findResumableSessionId(timelineDir, task.type, provider) ?? undefined : undefined;
16862
+ const resumeSessionId = task.contextKey ? findResumableSessionByContextKey(timelineDir, task.contextKey, provider) ?? undefined : undefined;
16842
16863
  if (resumeSessionId) {
16843
- log.info(`Task ${task.id} resuming session ${resumeSessionId}`);
16864
+ log.info(`Task ${task.id} resuming session ${resumeSessionId} (context_key: ${task.contextKey})`);
16844
16865
  }
16845
16866
  const session2 = backend.execute(prompt, {
16846
16867
  cwd: workDir,
@@ -16851,11 +16872,11 @@ async function runSession(input) {
16851
16872
  });
16852
16873
  const agentPid = session2.pid;
16853
16874
  const earlySessionId = await session2.sessionId;
16854
- await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, earlySessionId, process.pid, provider));
16875
+ await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, earlySessionId, process.pid, provider, task.contextKey));
16855
16876
  const pendingMessages = [];
16856
16877
  let seq = 0;
16857
16878
  const BATCH_SIZE = Number(process.env.ALOOK_MESSAGE_BATCH_SIZE) || 20;
16858
- const FLUSH_INTERVAL_MS = Number(process.env.ALOOK_MESSAGE_FLUSH_INTERVAL_MS) || 2000;
16879
+ const FLUSH_INTERVAL_MS = Number(process.env.ALOOK_MESSAGE_FLUSH_INTERVAL_MS) || 100;
16859
16880
  const flushMessages = async () => {
16860
16881
  if (pendingMessages.length === 0)
16861
16882
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/cli",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Alook CLI — register and run always-on AI coding agents.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/alookai/alook#readme",