@alook/cli 0.0.14 → 0.0.16

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
@@ -297,14 +297,30 @@ function statusCommand() {
297
297
  // commands/daemon.ts
298
298
  import { Command as Command3 } from "commander";
299
299
  import { spawn as spawn3 } from "child_process";
300
- import { openSync as openSync2, closeSync as closeSync2, mkdirSync as mkdirSync4 } from "fs";
300
+ import { openSync as openSync2, closeSync as closeSync2, mkdirSync as mkdirSync6 } from "fs";
301
301
  import { dirname as dirname4 } from "path";
302
302
 
303
303
  // ../shared/src/constants.ts
304
+ var TaskStatus = {
305
+ QUEUED: "queued",
306
+ DISPATCHED: "dispatched",
307
+ RUNNING: "running",
308
+ COMPLETED: "completed",
309
+ FAILED: "failed",
310
+ CANCELLED: "cancelled",
311
+ SUPERSEDED: "superseded"
312
+ };
313
+ var TERMINAL_TASK_STATUSES = [
314
+ TaskStatus.COMPLETED,
315
+ TaskStatus.FAILED,
316
+ TaskStatus.CANCELLED,
317
+ TaskStatus.SUPERSEDED
318
+ ];
304
319
  var TASK_TYPES = {
305
320
  USER_DM_MESSAGE: "user_dm_message",
306
321
  EMAIL_NOTIFICATION: "email_notification",
307
- CALENDAR_EVENT: "calendar_event"
322
+ CALENDAR_EVENT: "calendar_event",
323
+ KILL_TASK: "kill_task"
308
324
  };
309
325
  var POLL_INTERVAL_MS = Number(process.env.POLL_INTERVAL_MS) || 3000;
310
326
  var OFFLINE_THRESHOLD_MS = Number(process.env.OFFLINE_THRESHOLD_MS) || 9000;
@@ -13851,7 +13867,8 @@ var TaskStatusSchema = exports_external.enum([
13851
13867
  "running",
13852
13868
  "completed",
13853
13869
  "failed",
13854
- "cancelled"
13870
+ "cancelled",
13871
+ "superseded"
13855
13872
  ]);
13856
13873
  var ClaimedTaskRowSchema = exports_external.object({
13857
13874
  id: exports_external.string(),
@@ -14032,8 +14049,9 @@ var UpdateAgentRequestSchema = exports_external.object({
14032
14049
  description: exports_external.string().optional(),
14033
14050
  instructions: exports_external.string().optional(),
14034
14051
  runtime_id: exports_external.string().min(1).optional(),
14035
- runtime_config: RuntimeConfigSchema
14036
- }).refine((v) => v.name !== undefined || v.description !== undefined || v.instructions !== undefined || v.runtime_id !== undefined || v.runtime_config !== undefined, { message: "at least one field is required" });
14052
+ runtime_config: RuntimeConfigSchema,
14053
+ visibility: exports_external.enum(["public", "private"]).optional()
14054
+ }).refine((v) => v.name !== undefined || v.description !== undefined || v.instructions !== undefined || v.runtime_id !== undefined || v.runtime_config !== undefined || v.visibility !== undefined, { message: "at least one field is required" });
14037
14055
  var CreateConversationRequestSchema = exports_external.object({
14038
14056
  agent_id: exports_external.string().min(1, "agent_id is required")
14039
14057
  });
@@ -14125,6 +14143,16 @@ var CreateWorkspaceRequestSchema = exports_external.object({
14125
14143
  name: exports_external.string().min(1, "name is required"),
14126
14144
  slug: exports_external.string().min(1, "slug is required")
14127
14145
  });
14146
+ var UpdateWorkspaceRequestSchema = exports_external.object({
14147
+ name: exports_external.string().min(1, "name is required").max(100).trim().optional(),
14148
+ slug: exports_external.string().min(1, "slug is required").max(100).trim().toLowerCase().optional()
14149
+ });
14150
+ var DeleteWorkspaceRequestSchema = exports_external.object({
14151
+ confirm_name: exports_external.string().min(1, "confirm_name is required")
14152
+ });
14153
+ var GrantAgentAccessRequestSchema = exports_external.object({
14154
+ user_id: exports_external.string().min(1, "user_id is required")
14155
+ });
14128
14156
  // ../../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
14129
14157
  var entityKind = Symbol.for("drizzle:entityKind");
14130
14158
  var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
@@ -15556,6 +15584,48 @@ var member = sqliteTable("member", {
15556
15584
  globalInstruction: text("global_instruction").notNull().default(""),
15557
15585
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15558
15586
  }, (t) => [unique("member_workspace_user").on(t.workspaceId, t.userId)]);
15587
+ var workspaceInvite = sqliteTable("workspace_invite", {
15588
+ id: text("id").primaryKey().$defaultFn(() => "inv_" + nanoid3()),
15589
+ workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
15590
+ token: text("token").unique().notNull().$defaultFn(() => nanoid3(32)),
15591
+ createdBy: text("created_by").notNull().references(() => user.id, { onDelete: "cascade" }),
15592
+ usedBy: text("used_by").references(() => user.id, { onDelete: "set null" }),
15593
+ usedAt: text("used_at"),
15594
+ expiresAt: text("expires_at").notNull(),
15595
+ createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15596
+ }, (t) => [
15597
+ index("idx_workspace_invite_token").on(t.token),
15598
+ index("idx_workspace_invite_workspace").on(t.workspaceId)
15599
+ ]);
15600
+ var agentAccess = sqliteTable("agent_access", {
15601
+ id: text("id").primaryKey().$defaultFn(() => nanoid3()),
15602
+ agentId: text("agent_id").notNull(),
15603
+ workspaceId: text("workspace_id").notNull(),
15604
+ userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
15605
+ createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15606
+ }, (t) => [
15607
+ unique("agent_access_agent_ws_user").on(t.agentId, t.workspaceId, t.userId),
15608
+ index("idx_agent_access_agent_ws").on(t.agentId, t.workspaceId),
15609
+ index("idx_agent_access_user").on(t.userId),
15610
+ foreignKey({
15611
+ columns: [t.agentId, t.workspaceId],
15612
+ foreignColumns: [agent.id, agent.workspaceId]
15613
+ }).onDelete("cascade")
15614
+ ]);
15615
+ var agentPin = sqliteTable("agent_pin", {
15616
+ id: text("id").primaryKey().$defaultFn(() => nanoid3()),
15617
+ agentId: text("agent_id").notNull(),
15618
+ workspaceId: text("workspace_id").notNull(),
15619
+ userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
15620
+ createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15621
+ }, (t) => [
15622
+ unique("agent_pin_agent_ws_user").on(t.agentId, t.workspaceId, t.userId),
15623
+ index("idx_agent_pin_ws_user").on(t.workspaceId, t.userId),
15624
+ foreignKey({
15625
+ columns: [t.agentId, t.workspaceId],
15626
+ foreignColumns: [agent.id, agent.workspaceId]
15627
+ }).onDelete("cascade")
15628
+ ]);
15559
15629
  var machine = sqliteTable("machine", {
15560
15630
  daemonId: text("daemon_id").notNull(),
15561
15631
  workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
@@ -15693,6 +15763,7 @@ var emails = sqliteTable("emails", {
15693
15763
  htmlBody: text("html_body").notNull().default(""),
15694
15764
  attachments: text("attachments").notNull().default("[]"),
15695
15765
  status: text("status").notNull().default("unread"),
15766
+ direction: text("direction").notNull().default("inbound"),
15696
15767
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15697
15768
  }, (t) => [
15698
15769
  foreignKey({
@@ -15826,16 +15897,33 @@ class DaemonClient {
15826
15897
  "Content-Type": "application/json",
15827
15898
  Authorization: `Bearer ${token}`
15828
15899
  };
15829
- const res = await fetch(this.baseURL + path, {
15830
- method,
15831
- headers,
15832
- body: body ? JSON.stringify(body) : undefined
15833
- });
15834
- if (!res.ok)
15835
- throw new Error(`HTTP ${res.status}: ${await res.text()}`);
15836
- if (res.status === 204)
15837
- return;
15838
- return res.json();
15900
+ const MAX_RETRIES = 3;
15901
+ const BASE_DELAY_MS = 500;
15902
+ let lastError;
15903
+ for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
15904
+ try {
15905
+ const res = await fetch(this.baseURL + path, {
15906
+ method,
15907
+ headers,
15908
+ body: body ? JSON.stringify(body) : undefined
15909
+ });
15910
+ if (!res.ok)
15911
+ throw new Error(`HTTP ${res.status}: ${await res.text()}`);
15912
+ if (res.status === 204)
15913
+ return;
15914
+ return res.json();
15915
+ } catch (e) {
15916
+ if (e instanceof TypeError) {
15917
+ lastError = e;
15918
+ if (attempt < MAX_RETRIES) {
15919
+ await new Promise((r) => setTimeout(r, BASE_DELAY_MS * 2 ** attempt));
15920
+ continue;
15921
+ }
15922
+ }
15923
+ throw e;
15924
+ }
15925
+ }
15926
+ throw lastError;
15839
15927
  }
15840
15928
  async register(token, body) {
15841
15929
  const raw = await this.request("POST", "/api/daemon/register", token, body);
@@ -15866,15 +15954,35 @@ class DaemonClient {
15866
15954
  error: error48
15867
15955
  });
15868
15956
  }
15957
+ supersedeTask(token, taskId) {
15958
+ return this.request("POST", `/api/daemon/tasks/${taskId}/supersede`, token);
15959
+ }
15869
15960
  async getArtifactMeta(token, artifactId, workspaceId) {
15870
15961
  return this.request("GET", `/api/artifacts/${artifactId}?workspace_id=${encodeURIComponent(workspaceId)}`, token);
15871
15962
  }
15872
15963
  async downloadArtifact(token, artifactId, workspaceId) {
15873
- const res = await fetch(`${this.baseURL}/api/artifacts/${artifactId}/content?workspace_id=${encodeURIComponent(workspaceId)}`, { headers: { Authorization: `Bearer ${token}` } });
15874
- if (!res.ok) {
15875
- throw new Error(`artifact download failed: HTTP ${res.status}`);
15964
+ const MAX_RETRIES = 3;
15965
+ const BASE_DELAY_MS = 500;
15966
+ let lastError;
15967
+ for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
15968
+ try {
15969
+ const res = await fetch(`${this.baseURL}/api/artifacts/${artifactId}/content?workspace_id=${encodeURIComponent(workspaceId)}`, { headers: { Authorization: `Bearer ${token}` } });
15970
+ if (!res.ok) {
15971
+ throw new Error(`artifact download failed: HTTP ${res.status}`);
15972
+ }
15973
+ return res.arrayBuffer();
15974
+ } catch (e) {
15975
+ if (e instanceof TypeError) {
15976
+ lastError = e;
15977
+ if (attempt < MAX_RETRIES) {
15978
+ await new Promise((r) => setTimeout(r, BASE_DELAY_MS * 2 ** attempt));
15979
+ continue;
15980
+ }
15981
+ }
15982
+ throw e;
15983
+ }
15876
15984
  }
15877
- return res.arrayBuffer();
15985
+ throw lastError;
15878
15986
  }
15879
15987
  reportMessages(token, taskId, messages) {
15880
15988
  return this.request("POST", `/api/daemon/tasks/${taskId}/messages`, token, { messages });
@@ -16282,13 +16390,172 @@ async function handleCliUpdate(version3, onSuccess, profile) {
16282
16390
  }
16283
16391
  }
16284
16392
 
16393
+ // daemon/execenv/timeline.ts
16394
+ import { appendFileSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync } from "fs";
16395
+ import { join as join4 } from "path";
16396
+
16397
+ // daemon/execenv/filelock.ts
16398
+ import { mkdirSync as mkdirSync3, rmdirSync, statSync } from "fs";
16399
+ var DEFAULT_STALE_MS = 3600000;
16400
+ function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
16401
+ try {
16402
+ mkdirSync3(lockPath);
16403
+ return true;
16404
+ } catch {
16405
+ try {
16406
+ const stat = statSync(lockPath);
16407
+ if (Date.now() - stat.mtimeMs > staleMs) {
16408
+ rmdirSync(lockPath);
16409
+ try {
16410
+ mkdirSync3(lockPath);
16411
+ return true;
16412
+ } catch {
16413
+ return false;
16414
+ }
16415
+ }
16416
+ } catch {
16417
+ try {
16418
+ mkdirSync3(lockPath);
16419
+ return true;
16420
+ } catch {
16421
+ return false;
16422
+ }
16423
+ }
16424
+ return false;
16425
+ }
16426
+ }
16427
+ function releaseLock(lockPath) {
16428
+ try {
16429
+ rmdirSync(lockPath);
16430
+ } catch {}
16431
+ }
16432
+
16433
+ // daemon/execenv/timeline.ts
16434
+ function readJsonl(filePath) {
16435
+ let content;
16436
+ try {
16437
+ content = readFileSync5(filePath, "utf-8");
16438
+ } catch {
16439
+ return [];
16440
+ }
16441
+ const entries = [];
16442
+ for (const line of content.trimEnd().split(`
16443
+ `)) {
16444
+ if (!line)
16445
+ continue;
16446
+ try {
16447
+ entries.push(JSON.parse(line));
16448
+ } catch {}
16449
+ }
16450
+ return entries;
16451
+ }
16452
+ function filenameForDate(date5) {
16453
+ const y = date5.getFullYear();
16454
+ const m = String(date5.getMonth() + 1).padStart(2, "0");
16455
+ const d = String(date5.getDate()).padStart(2, "0");
16456
+ return `${y}-${m}-${d}.jsonl`;
16457
+ }
16458
+ function recentFilenames(maxDays) {
16459
+ const filenames = [];
16460
+ const now = new Date;
16461
+ for (let i = 0;i < maxDays; i++) {
16462
+ const d = new Date(now);
16463
+ d.setDate(d.getDate() - i);
16464
+ filenames.push(filenameForDate(d));
16465
+ }
16466
+ return filenames;
16467
+ }
16468
+ var DEFAULT_RESUME_MAX_AGE_MS = 3 * 60 * 60 * 1000;
16469
+ var EMAIL_RESUME_MAX_AGE_MS = 48 * 60 * 60 * 1000;
16470
+ function findRunningPidByTaskId(timelineDir, taskId) {
16471
+ for (const filename of recentFilenames(7)) {
16472
+ const entries = readJsonl(join4(timelineDir, filename));
16473
+ for (const entry of entries) {
16474
+ if (entry.task_id === taskId && entry.status === "running" && entry.pid != null) {
16475
+ return entry.pid;
16476
+ }
16477
+ }
16478
+ }
16479
+ return null;
16480
+ }
16481
+ function findRunningEntryByContextKey(timelineDir, contextKey, provider) {
16482
+ for (const filename of recentFilenames(7)) {
16483
+ const dayEntries = readJsonl(join4(timelineDir, filename));
16484
+ for (let i = dayEntries.length - 1;i >= 0; i--) {
16485
+ const entry = dayEntries[i];
16486
+ if (entry.status === "running" && entry.context_key === contextKey && entry.provider === provider) {
16487
+ return entry;
16488
+ }
16489
+ }
16490
+ }
16491
+ return null;
16492
+ }
16493
+
16494
+ // daemon/execenv/steering.ts
16495
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync3, readdirSync, statSync as statSync2 } from "fs";
16496
+ import { join as join5 } from "path";
16497
+ var INTENT_DIR_NAME = ".kill_intents";
16498
+ var STEERING_LOCK_DIR = ".steering_locks";
16499
+ var INTENT_STALE_MS = 10 * 60 * 1000;
16500
+ function intentFilePath(baseDir, taskId) {
16501
+ return join5(baseDir, INTENT_DIR_NAME, `${taskId}.json`);
16502
+ }
16503
+ function intentDirPath(baseDir) {
16504
+ return join5(baseDir, INTENT_DIR_NAME);
16505
+ }
16506
+ function steeringLockPath(baseDir, contextKey) {
16507
+ const safeKey = contextKey.replace(/[^a-zA-Z0-9_:-]/g, "_");
16508
+ return join5(baseDir, STEERING_LOCK_DIR, safeKey);
16509
+ }
16510
+ function writeKillIntent(baseDir, intent) {
16511
+ const dir = intentDirPath(baseDir);
16512
+ try {
16513
+ mkdirSync4(dir, { recursive: true });
16514
+ } catch {}
16515
+ const filePath = intentFilePath(baseDir, intent.targetTaskId);
16516
+ writeFileSync5(filePath, JSON.stringify(intent));
16517
+ }
16518
+ function cleanupStaleIntents(baseDir) {
16519
+ const dir = intentDirPath(baseDir);
16520
+ let files;
16521
+ try {
16522
+ files = readdirSync(dir).filter((f) => f.endsWith(".json"));
16523
+ } catch {
16524
+ return;
16525
+ }
16526
+ const now = Date.now();
16527
+ for (const file2 of files) {
16528
+ const filePath = join5(dir, file2);
16529
+ try {
16530
+ const content = readFileSync6(filePath, "utf-8");
16531
+ const intent = JSON.parse(content);
16532
+ const stat = statSync2(filePath);
16533
+ if (now - stat.mtimeMs > INTENT_STALE_MS) {
16534
+ unlinkSync3(filePath);
16535
+ log.debug(`Cleaned up stale kill intent for task ${intent.targetTaskId}`);
16536
+ }
16537
+ } catch {}
16538
+ }
16539
+ }
16540
+ function acquireSteeringLock(baseDir, contextKey) {
16541
+ const lockPath = steeringLockPath(baseDir, contextKey);
16542
+ try {
16543
+ mkdirSync4(join5(baseDir, STEERING_LOCK_DIR), { recursive: true });
16544
+ } catch {}
16545
+ return acquireLock(lockPath, 60000);
16546
+ }
16547
+ function releaseSteeringLock(baseDir, contextKey) {
16548
+ const lockPath = steeringLockPath(baseDir, contextKey);
16549
+ releaseLock(lockPath);
16550
+ }
16551
+
16285
16552
  // daemon/daemon.ts
16286
- import { existsSync, mkdirSync as mkdirSync3, openSync, closeSync, readdirSync, statSync, unlinkSync as unlinkSync3 } from "fs";
16553
+ import { existsSync, mkdirSync as mkdirSync5, openSync, closeSync, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync4 } from "fs";
16287
16554
  import { execSync as execSync3, spawn as spawn2 } from "child_process";
16288
16555
  import { fileURLToPath as fileURLToPath2 } from "url";
16289
- import { dirname as dirname3, join as join4 } from "path";
16556
+ import { dirname as dirname3, join as join6 } from "path";
16290
16557
  var _dir = dirname3(fileURLToPath2(import.meta.url));
16291
- var sessionRunnerPath = existsSync(join4(_dir, "session-runner.js")) ? join4(_dir, "session-runner.js") : join4(_dir, "session-runner.ts");
16558
+ var sessionRunnerPath = existsSync(join6(_dir, "session-runner.js")) ? join6(_dir, "session-runner.js") : join6(_dir, "session-runner.ts");
16292
16559
  function isCommandAvailable2(cmd) {
16293
16560
  try {
16294
16561
  const check2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
@@ -16298,21 +16565,21 @@ function isCommandAvailable2(cmd) {
16298
16565
  return false;
16299
16566
  }
16300
16567
  }
16301
- var MAX_SESSION_RUNNER_LOGS = 50;
16568
+ var MAX_SESSION_RUNNER_LOGS = 500;
16302
16569
  function pruneSessionRunnerLogs() {
16303
16570
  const logDir = sessionRunnerLogDir();
16304
16571
  let entries;
16305
16572
  try {
16306
- entries = readdirSync(logDir).filter((f) => f.endsWith(".log"));
16573
+ entries = readdirSync2(logDir).filter((f) => f.endsWith(".log"));
16307
16574
  } catch {
16308
16575
  return;
16309
16576
  }
16310
16577
  if (entries.length <= MAX_SESSION_RUNNER_LOGS)
16311
16578
  return;
16312
16579
  const withMtime = entries.map((name) => {
16313
- const full = join4(logDir, name);
16580
+ const full = join6(logDir, name);
16314
16581
  try {
16315
- return { name, mtime: statSync(full).mtimeMs };
16582
+ return { name, mtime: statSync3(full).mtimeMs };
16316
16583
  } catch {
16317
16584
  return { name, mtime: 0 };
16318
16585
  }
@@ -16320,7 +16587,7 @@ function pruneSessionRunnerLogs() {
16320
16587
  withMtime.sort((a, b) => b.mtime - a.mtime);
16321
16588
  for (const entry of withMtime.slice(MAX_SESSION_RUNNER_LOGS)) {
16322
16589
  try {
16323
- unlinkSync3(join4(logDir, entry.name));
16590
+ unlinkSync4(join6(logDir, entry.name));
16324
16591
  } catch {}
16325
16592
  }
16326
16593
  }
@@ -16399,12 +16666,7 @@ async function startDaemon(profile, serverUrl) {
16399
16666
  });
16400
16667
  } catch (e) {
16401
16668
  if (e instanceof Error && e.message.startsWith("HTTP 401")) {
16402
- log.warn(`Workspace ${ws.id} token invalid — removing from config`);
16403
- try {
16404
- const cfg = loadCLIConfigForProfile(profile);
16405
- cfg.watched_workspaces = (cfg.watched_workspaces || []).filter((w) => w.id !== ws.id);
16406
- saveCLIConfigForProfile(profile, cfg);
16407
- } catch {}
16669
+ log.warn(`Workspace ${ws.id} token invalid — skipping (run '${cmdPrefix()} register --token <token>' to fix)`);
16408
16670
  } else {
16409
16671
  log.error(`Failed to register workspace ${ws.id}, skipping`, e);
16410
16672
  }
@@ -16463,7 +16725,7 @@ async function startDaemon(profile, serverUrl) {
16463
16725
  cfg.watched_workspaces = (cfg.watched_workspaces || []).filter((w) => w.id !== workspaceId);
16464
16726
  saveCLIConfigForProfile(profile, cfg);
16465
16727
  } catch {}
16466
- log.info(`Workspace ${workspaceId} evictedruntimes removed server-side`);
16728
+ log.info(`Workspace ${workspaceId} deleted server-side — removed from config`);
16467
16729
  }
16468
16730
  const pollCycle = async () => {
16469
16731
  let remaining = config2.maxConcurrentTasks - activeTasks.size;
@@ -16500,7 +16762,7 @@ async function startDaemon(profile, serverUrl) {
16500
16762
  }
16501
16763
  } catch (e) {
16502
16764
  if (e instanceof Error && e.message.startsWith("HTTP 401")) {
16503
- evictedIds.push(ws.workspaceId);
16765
+ log.warn(`Workspace ${ws.workspaceId} poll returned 401 — will retry next cycle`);
16504
16766
  } else {
16505
16767
  log.debug("Poll error", e);
16506
16768
  }
@@ -16537,17 +16799,28 @@ async function startDaemon(profile, serverUrl) {
16537
16799
  releaseDaemonPid(profile);
16538
16800
  health.server.close(() => {
16539
16801
  if (restartRequested) {
16540
- const args = ["daemon", "start", "--foreground"];
16802
+ const entry = process.argv[1];
16803
+ const args = [entry, "daemon", "start", "--foreground"];
16541
16804
  if (profile)
16542
16805
  args.push("--profile", profile);
16543
16806
  if (serverUrl)
16544
16807
  args.push("--server", serverUrl);
16545
- const child = spawn2("alook", args, {
16808
+ const logPath = daemonLogFilePath();
16809
+ let logFd;
16810
+ try {
16811
+ mkdirSync5(dirname3(logPath), { recursive: true, mode: 448 });
16812
+ logFd = openSync(logPath, "a", 384);
16813
+ } catch (e) {
16814
+ log.error(`Failed to open daemon log file ${logPath}`, e);
16815
+ }
16816
+ const child = spawn2(process.execPath, args, {
16546
16817
  detached: true,
16547
- stdio: ["ignore", "ignore", "ignore"]
16818
+ stdio: logFd != null ? ["ignore", logFd, logFd] : ["ignore", "ignore", "ignore"]
16548
16819
  });
16549
16820
  child.unref();
16550
- log.info(`Spawned new daemon (pid=${child.pid})`);
16821
+ if (logFd != null)
16822
+ closeSync(logFd);
16823
+ log.info(`Spawned new daemon (pid=${child.pid}), logs: ${logPath}`);
16551
16824
  }
16552
16825
  clearTimeout(timeout);
16553
16826
  process.exit(0);
@@ -16559,21 +16832,62 @@ async function startDaemon(profile, serverUrl) {
16559
16832
  }
16560
16833
  function spawnSessionRunner(input) {
16561
16834
  const logDir = sessionRunnerLogDir();
16562
- mkdirSync3(logDir, { recursive: true });
16563
- const logFilePath = join4(logDir, `${input.task.id}.log`);
16835
+ mkdirSync5(logDir, { recursive: true });
16836
+ const logFilePath = join6(logDir, `${input.task.id}.log`);
16564
16837
  input.logFilePath = logFilePath;
16565
16838
  const encoded = Buffer.from(JSON.stringify(input)).toString("base64");
16566
- const fd = openSync(logFilePath, "a");
16839
+ let fd;
16840
+ try {
16841
+ fd = openSync(logFilePath, "a");
16842
+ } catch (e) {
16843
+ log.error(`Failed to open log file ${logFilePath}`, e);
16844
+ }
16567
16845
  const child = spawn2(process.execPath, [sessionRunnerPath, encoded], {
16568
16846
  detached: true,
16569
- stdio: ["ignore", fd, fd]
16847
+ stdio: fd != null ? ["ignore", fd, fd] : ["ignore", "ignore", "ignore"]
16570
16848
  });
16571
16849
  child.unref();
16572
- closeSync(fd);
16850
+ if (fd != null)
16851
+ closeSync(fd);
16573
16852
  return child;
16574
16853
  }
16575
16854
  async function handleTask(client, config2, runtimeIndex, task, token, activeTasks) {
16576
16855
  log.info(`Task ${task.id} claimed agent=${task.agentId}`);
16856
+ if (task.type === TASK_TYPES.KILL_TASK) {
16857
+ const targetTaskId = task.context?.target_task_id;
16858
+ if (!targetTaskId) {
16859
+ await client.failTask(token, task.id, "missing target_task_id in context");
16860
+ activeTasks.delete(task.id);
16861
+ return;
16862
+ }
16863
+ const agentBaseDir = join6(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
16864
+ const timelineDir = join6(agentBaseDir, ".context_timeline");
16865
+ const pid = findRunningPidByTaskId(timelineDir, targetTaskId);
16866
+ if (pid != null) {
16867
+ writeKillIntent(agentBaseDir, {
16868
+ reason: "cancelled",
16869
+ targetTaskId,
16870
+ expectedPid: pid
16871
+ });
16872
+ try {
16873
+ process.kill(pid, "SIGTERM");
16874
+ await client.failTask(token, task.id, "killed");
16875
+ log.info(`Kill task ${task.id}: sent SIGTERM to pid=${pid} for target=${targetTaskId}`);
16876
+ } catch (e) {
16877
+ if (e?.code === "ESRCH") {
16878
+ await client.failTask(token, task.id, "target process already exited");
16879
+ log.info(`Kill task ${task.id}: target pid=${pid} already exited`);
16880
+ } else {
16881
+ await client.failTask(token, task.id, `kill failed: ${e}`);
16882
+ }
16883
+ }
16884
+ } else {
16885
+ await client.failTask(token, task.id, "target not found in timeline");
16886
+ log.info(`Kill task ${task.id}: target ${targetTaskId} not found in timeline`);
16887
+ }
16888
+ activeTasks.delete(task.id);
16889
+ return;
16890
+ }
16577
16891
  try {
16578
16892
  await client.startTask(token, task.id);
16579
16893
  } catch (e) {
@@ -16588,6 +16902,60 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
16588
16902
  return;
16589
16903
  }
16590
16904
  const provider = runtimeData.provider;
16905
+ if (task.contextKey) {
16906
+ const agentBaseDir = join6(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
16907
+ cleanupStaleIntents(agentBaseDir);
16908
+ const timelineDir = join6(agentBaseDir, ".context_timeline");
16909
+ const lockAcquired = acquireSteeringLock(agentBaseDir, task.contextKey);
16910
+ if (!lockAcquired) {
16911
+ log.warn(`Steering lock contention for context_key=${task.contextKey}, proceeding without steering`);
16912
+ } else {
16913
+ try {
16914
+ const predecessor = findRunningEntryByContextKey(timelineDir, task.contextKey, provider);
16915
+ if (predecessor && predecessor.task_id !== task.id) {
16916
+ log.info(`Steering: task ${task.id} supersedes predecessor ${predecessor.task_id} (context_key=${task.contextKey})`);
16917
+ if (predecessor.pid != null) {
16918
+ writeKillIntent(agentBaseDir, {
16919
+ reason: "superseded",
16920
+ targetTaskId: predecessor.task_id,
16921
+ expectedPid: predecessor.pid,
16922
+ successorTaskId: task.id
16923
+ });
16924
+ try {
16925
+ process.kill(predecessor.pid, "SIGTERM");
16926
+ log.info(`Steering: sent SIGTERM to predecessor pid=${predecessor.pid}`);
16927
+ } catch (e) {
16928
+ if (e?.code === "ESRCH") {
16929
+ log.info(`Steering: predecessor pid=${predecessor.pid} already exited`);
16930
+ } else {
16931
+ log.warn(`Steering: kill failed for pid=${predecessor.pid}`, e);
16932
+ }
16933
+ }
16934
+ const waitStart = Date.now();
16935
+ const MAX_WAIT_MS = 15000;
16936
+ const POLL_MS = 200;
16937
+ while (Date.now() - waitStart < MAX_WAIT_MS) {
16938
+ const stillRunning = findRunningPidByTaskId(timelineDir, predecessor.task_id);
16939
+ if (stillRunning == null)
16940
+ break;
16941
+ await new Promise((r) => setTimeout(r, POLL_MS));
16942
+ }
16943
+ if (findRunningPidByTaskId(timelineDir, predecessor.task_id) != null) {
16944
+ log.warn(`Steering: predecessor pid=${predecessor.pid} did not exit within ${MAX_WAIT_MS}ms, proceeding anyway`);
16945
+ }
16946
+ }
16947
+ try {
16948
+ await client.supersedeTask(token, predecessor.task_id);
16949
+ log.info(`Steering: predecessor ${predecessor.task_id} marked superseded`);
16950
+ } catch (e) {
16951
+ log.warn(`Steering: failed to mark predecessor superseded server-side`, e);
16952
+ }
16953
+ }
16954
+ } finally {
16955
+ releaseSteeringLock(agentBaseDir, task.contextKey);
16956
+ }
16957
+ }
16958
+ }
16591
16959
  const cliPath = provider === "claude" ? config2.claudePath : provider === "codex" ? config2.codexPath : config2.opencodePath;
16592
16960
  const configModel = provider === "claude" ? config2.claudeModel : provider === "codex" ? config2.codexModel : config2.opencodeModel;
16593
16961
  const agentModel = task.agent?.runtimeConfig?.model;
@@ -16642,7 +17010,7 @@ async function startInBackground(profile, serverUrl) {
16642
17010
  return;
16643
17011
  }
16644
17012
  const logPath = daemonLogFilePath();
16645
- mkdirSync4(dirname4(logPath), { recursive: true, mode: 448 });
17013
+ mkdirSync6(dirname4(logPath), { recursive: true, mode: 448 });
16646
17014
  const logFd = openSync2(logPath, "a", 384);
16647
17015
  const child = spawn3(process.execPath, buildChildArgs(profile, serverUrl), {
16648
17016
  detached: true,
@@ -16733,6 +17101,14 @@ function daemonCommand() {
16733
17101
  import { Command as Command4 } from "commander";
16734
17102
 
16735
17103
  // lib/output.ts
17104
+ function printTable(headers, rows) {
17105
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || "").length)));
17106
+ const header = headers.map((h, i) => h.padEnd(widths[i])).join(" ");
17107
+ const separator = widths.map((w) => "-".repeat(w)).join(" ");
17108
+ console.log(header);
17109
+ console.log(separator);
17110
+ rows.forEach((r) => console.log(r.map((c, i) => (c || "").padEnd(widths[i])).join(" ")));
17111
+ }
16736
17112
  function printJSON(data) {
16737
17113
  console.log(JSON.stringify(data, null, 2));
16738
17114
  }
@@ -16752,8 +17128,8 @@ function configCommand() {
16752
17128
 
16753
17129
  // commands/email.ts
16754
17130
  import { Command as Command5 } from "commander";
16755
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
16756
- import { basename, join as join5 } from "path";
17131
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
17132
+ import { basename, join as join7 } from "path";
16757
17133
  import PostalMime from "postal-mime";
16758
17134
  var VALID_STATUSES = ["unread", "read", "archived"];
16759
17135
  var EMAIL_BASE = "/tmp/alook-emails";
@@ -16784,7 +17160,10 @@ function collectRepeated(value, previous) {
16784
17160
  return previous.concat([value]);
16785
17161
  }
16786
17162
  function resolveClientOpts(command, opts) {
16787
- const parentOpts = command.parent?.parent?.opts() || {};
17163
+ let root = command;
17164
+ while (root.parent)
17165
+ root = root.parent;
17166
+ const parentOpts = root.opts() || {};
16788
17167
  const profile = parentOpts.profile;
16789
17168
  const cfg = loadCLIConfigForProfile(profile);
16790
17169
  const serverUrl = parentOpts.server || cfg.server_url;
@@ -16813,7 +17192,7 @@ function emailCommand() {
16813
17192
  console.error(`Error: invalid status "${opts.status}", must be one of: ${VALID_STATUSES.join(", ")}`);
16814
17193
  process.exit(1);
16815
17194
  }
16816
- const emailDir_base = join5(EMAIL_BASE, workspaceId, opts.agent_id);
17195
+ const emailDir_base = join7(EMAIL_BASE, workspaceId, opts.agent_id);
16817
17196
  try {
16818
17197
  let query = `/api/email?agentId=${opts.agent_id}`;
16819
17198
  if (opts.status)
@@ -16827,11 +17206,11 @@ function emailCommand() {
16827
17206
  printJSON(emails2);
16828
17207
  return;
16829
17208
  }
16830
- mkdirSync5(emailDir_base, { recursive: true });
17209
+ mkdirSync7(emailDir_base, { recursive: true });
16831
17210
  const downloadedPaths = [];
16832
17211
  for (const email3 of emails2) {
16833
- const emailDir = join5(emailDir_base, email3.id);
16834
- mkdirSync5(emailDir, { recursive: true });
17212
+ const emailDir = join7(emailDir_base, email3.id);
17213
+ mkdirSync7(emailDir, { recursive: true });
16835
17214
  const metadata = {
16836
17215
  id: email3.id,
16837
17216
  from: email3.from_email,
@@ -16843,8 +17222,8 @@ function emailCommand() {
16843
17222
  in_reply_to: email3.in_reply_to || "",
16844
17223
  references: email3.references || ""
16845
17224
  };
16846
- const metadataPath = join5(emailDir, "metadata.json");
16847
- writeFileSync4(metadataPath, JSON.stringify(metadata, null, 2));
17225
+ const metadataPath = join7(emailDir, "metadata.json");
17226
+ writeFileSync6(metadataPath, JSON.stringify(metadata, null, 2));
16848
17227
  downloadedPaths.push(metadataPath);
16849
17228
  let rawMime;
16850
17229
  try {
@@ -16859,18 +17238,18 @@ function emailCommand() {
16859
17238
  }
16860
17239
  const parsed = await new PostalMime().parse(rawMime);
16861
17240
  if (parsed.text) {
16862
- const bodyPath = join5(emailDir, "body.txt");
16863
- writeFileSync4(bodyPath, parsed.text);
17241
+ const bodyPath = join7(emailDir, "body.txt");
17242
+ writeFileSync6(bodyPath, parsed.text);
16864
17243
  downloadedPaths.push(bodyPath);
16865
17244
  }
16866
17245
  if (parsed.html) {
16867
- const htmlPath = join5(emailDir, "body.html");
16868
- writeFileSync4(htmlPath, parsed.html);
17246
+ const htmlPath = join7(emailDir, "body.html");
17247
+ writeFileSync6(htmlPath, parsed.html);
16869
17248
  downloadedPaths.push(htmlPath);
16870
17249
  }
16871
17250
  if (parsed.attachments && parsed.attachments.length > 0) {
16872
- const attDir = join5(emailDir, "attachments");
16873
- mkdirSync5(attDir, { recursive: true });
17251
+ const attDir = join7(emailDir, "attachments");
17252
+ mkdirSync7(attDir, { recursive: true });
16874
17253
  const usedFilenames = new Set;
16875
17254
  for (let i = 0;i < parsed.attachments.length; i++) {
16876
17255
  const att = parsed.attachments[i];
@@ -16879,7 +17258,7 @@ function emailCommand() {
16879
17258
  filename = `${i}-${filename}`;
16880
17259
  }
16881
17260
  usedFilenames.add(filename);
16882
- const attPath = join5(attDir, filename);
17261
+ const attPath = join7(attDir, filename);
16883
17262
  const content = att.content;
16884
17263
  let buf;
16885
17264
  if (typeof content === "string") {
@@ -16889,7 +17268,7 @@ function emailCommand() {
16889
17268
  } else {
16890
17269
  buf = Buffer.from(content);
16891
17270
  }
16892
- writeFileSync4(attPath, buf);
17271
+ writeFileSync6(attPath, buf);
16893
17272
  downloadedPaths.push(attPath);
16894
17273
  }
16895
17274
  }
@@ -16928,7 +17307,7 @@ function emailCommand() {
16928
17307
  const client = new APIClient(serverUrl, token, workspaceId);
16929
17308
  let htmlBody;
16930
17309
  try {
16931
- htmlBody = readFileSync5(opts.bodyFile, "utf-8");
17310
+ htmlBody = readFileSync7(opts.bodyFile, "utf-8");
16932
17311
  } catch (err) {
16933
17312
  console.error(`Error: cannot read body file "${opts.bodyFile}": ${err instanceof Error ? err.message : err}`);
16934
17313
  process.exit(1);
@@ -16944,8 +17323,8 @@ function emailCommand() {
16944
17323
  let bytes;
16945
17324
  let size;
16946
17325
  try {
16947
- bytes = readFileSync5(path);
16948
- size = statSync2(path).size;
17326
+ bytes = readFileSync7(path);
17327
+ size = statSync4(path).size;
16949
17328
  } catch (err) {
16950
17329
  console.error(`Error: cannot read attachment "${path}": ${err instanceof Error ? err.message : err}`);
16951
17330
  process.exit(1);
@@ -16990,6 +17369,73 @@ function emailCommand() {
16990
17369
  process.exit(1);
16991
17370
  }
16992
17371
  });
17372
+ const whitelistCmd = new Command5("whitelist").description("Manage email whitelist (allowed senders)");
17373
+ whitelistCmd.command("list").description("List all whitelisted emails for an agent").requiredOption("--agent_id <id>", "Agent ID").option("--workspace <id>", "Workspace ID").option("--json", "Output as JSON").action(async (opts, command) => {
17374
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, {
17375
+ workspace: opts.workspace,
17376
+ agentId: opts.agent_id
17377
+ });
17378
+ const client = new APIClient(serverUrl, token, workspaceId);
17379
+ try {
17380
+ const entries = await client.getJSON(`/api/agents/${opts.agent_id}/whitelist`);
17381
+ if (!entries.length) {
17382
+ console.log("No whitelisted emails.");
17383
+ return;
17384
+ }
17385
+ if (opts.json) {
17386
+ printJSON(entries);
17387
+ return;
17388
+ }
17389
+ printTable(["ID", "EMAIL", "CREATED AT"], entries.map((e) => [e.id, e.email, e.created_at]));
17390
+ } catch (err) {
17391
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
17392
+ process.exit(1);
17393
+ }
17394
+ });
17395
+ whitelistCmd.command("add").description("Add an email to the whitelist").requiredOption("--agent_id <id>", "Agent ID").option("--workspace <id>", "Workspace ID").argument("<email>", "Email address to whitelist").action(async (email3, opts, command) => {
17396
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, {
17397
+ workspace: opts.workspace,
17398
+ agentId: opts.agent_id
17399
+ });
17400
+ const client = new APIClient(serverUrl, token, workspaceId);
17401
+ try {
17402
+ const entry = await client.postJSON(`/api/agents/${opts.agent_id}/whitelist`, { email: email3.toLowerCase() });
17403
+ console.log(`Added ${entry.email} to whitelist (id: ${entry.id})`);
17404
+ } catch (err) {
17405
+ const msg = err instanceof Error ? err.message : String(err);
17406
+ if (msg.includes("409")) {
17407
+ console.error(`Error: ${email3.toLowerCase()} is already whitelisted`);
17408
+ } else {
17409
+ console.error(`Error: ${msg}`);
17410
+ }
17411
+ process.exit(1);
17412
+ }
17413
+ });
17414
+ whitelistCmd.command("delete").description("Remove an email from the whitelist").requiredOption("--agent_id <id>", "Agent ID").option("--workspace <id>", "Workspace ID").argument("<email>", "Email address to remove").action(async (email3, opts, command) => {
17415
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, {
17416
+ workspace: opts.workspace,
17417
+ agentId: opts.agent_id
17418
+ });
17419
+ const client = new APIClient(serverUrl, token, workspaceId);
17420
+ const normalizedEmail = email3.toLowerCase();
17421
+ try {
17422
+ const entries = await client.getJSON(`/api/agents/${opts.agent_id}/whitelist`);
17423
+ const entry = entries.find((e) => e.email === normalizedEmail);
17424
+ if (!entry) {
17425
+ console.error(`Error: ${normalizedEmail} is not in the whitelist`);
17426
+ process.exit(1);
17427
+ }
17428
+ await client.deleteJSON(`/api/agents/${opts.agent_id}/whitelist/${entry.id}`);
17429
+ console.log(`Removed ${normalizedEmail} from whitelist`);
17430
+ } catch (err) {
17431
+ const msg = err instanceof Error ? err.message : String(err);
17432
+ if (msg === "__exit__")
17433
+ throw err;
17434
+ console.error(`Error: ${msg}`);
17435
+ process.exit(1);
17436
+ }
17437
+ });
17438
+ cmd.addCommand(whitelistCmd);
16993
17439
  return cmd;
16994
17440
  }
16995
17441
 
@@ -17249,7 +17695,7 @@ ${result.output}`);
17249
17695
 
17250
17696
  // commands/sync.ts
17251
17697
  import { Command as Command9 } from "commander";
17252
- import { readFileSync as readFileSync6, statSync as statSync3 } from "fs";
17698
+ import { readFileSync as readFileSync8, statSync as statSync5 } from "fs";
17253
17699
  import { basename as basename2 } from "path";
17254
17700
  var MIME_BY_EXT2 = {
17255
17701
  ".pdf": "application/pdf",
@@ -17298,9 +17744,9 @@ function syncCommand() {
17298
17744
  let bytes;
17299
17745
  let size;
17300
17746
  try {
17301
- const stat = statSync3(opts.file);
17747
+ const stat = statSync5(opts.file);
17302
17748
  size = stat.size;
17303
- bytes = readFileSync6(opts.file);
17749
+ bytes = readFileSync8(opts.file);
17304
17750
  } catch (err) {
17305
17751
  console.error(`Error: cannot read file "${opts.file}": ${err.message}`);
17306
17752
  process.exit(1);
@@ -18,10 +18,26 @@ import { mkdir, writeFile, rm } from "fs/promises";
18
18
  import path from "path";
19
19
 
20
20
  // ../shared/src/constants.ts
21
+ var TaskStatus = {
22
+ QUEUED: "queued",
23
+ DISPATCHED: "dispatched",
24
+ RUNNING: "running",
25
+ COMPLETED: "completed",
26
+ FAILED: "failed",
27
+ CANCELLED: "cancelled",
28
+ SUPERSEDED: "superseded"
29
+ };
30
+ var TERMINAL_TASK_STATUSES = [
31
+ TaskStatus.COMPLETED,
32
+ TaskStatus.FAILED,
33
+ TaskStatus.CANCELLED,
34
+ TaskStatus.SUPERSEDED
35
+ ];
21
36
  var TASK_TYPES = {
22
37
  USER_DM_MESSAGE: "user_dm_message",
23
38
  EMAIL_NOTIFICATION: "email_notification",
24
- CALENDAR_EVENT: "calendar_event"
39
+ CALENDAR_EVENT: "calendar_event",
40
+ KILL_TASK: "kill_task"
25
41
  };
26
42
  var POLL_INTERVAL_MS = Number(process.env.POLL_INTERVAL_MS) || 3000;
27
43
  var OFFLINE_THRESHOLD_MS = Number(process.env.OFFLINE_THRESHOLD_MS) || 9000;
@@ -13568,7 +13584,8 @@ var TaskStatusSchema = exports_external.enum([
13568
13584
  "running",
13569
13585
  "completed",
13570
13586
  "failed",
13571
- "cancelled"
13587
+ "cancelled",
13588
+ "superseded"
13572
13589
  ]);
13573
13590
  var ClaimedTaskRowSchema = exports_external.object({
13574
13591
  id: exports_external.string(),
@@ -13749,8 +13766,9 @@ var UpdateAgentRequestSchema = exports_external.object({
13749
13766
  description: exports_external.string().optional(),
13750
13767
  instructions: exports_external.string().optional(),
13751
13768
  runtime_id: exports_external.string().min(1).optional(),
13752
- runtime_config: RuntimeConfigSchema
13753
- }).refine((v) => v.name !== undefined || v.description !== undefined || v.instructions !== undefined || v.runtime_id !== undefined || v.runtime_config !== undefined, { message: "at least one field is required" });
13769
+ runtime_config: RuntimeConfigSchema,
13770
+ visibility: exports_external.enum(["public", "private"]).optional()
13771
+ }).refine((v) => v.name !== undefined || v.description !== undefined || v.instructions !== undefined || v.runtime_id !== undefined || v.runtime_config !== undefined || v.visibility !== undefined, { message: "at least one field is required" });
13754
13772
  var CreateConversationRequestSchema = exports_external.object({
13755
13773
  agent_id: exports_external.string().min(1, "agent_id is required")
13756
13774
  });
@@ -13842,6 +13860,16 @@ var CreateWorkspaceRequestSchema = exports_external.object({
13842
13860
  name: exports_external.string().min(1, "name is required"),
13843
13861
  slug: exports_external.string().min(1, "slug is required")
13844
13862
  });
13863
+ var UpdateWorkspaceRequestSchema = exports_external.object({
13864
+ name: exports_external.string().min(1, "name is required").max(100).trim().optional(),
13865
+ slug: exports_external.string().min(1, "slug is required").max(100).trim().toLowerCase().optional()
13866
+ });
13867
+ var DeleteWorkspaceRequestSchema = exports_external.object({
13868
+ confirm_name: exports_external.string().min(1, "confirm_name is required")
13869
+ });
13870
+ var GrantAgentAccessRequestSchema = exports_external.object({
13871
+ user_id: exports_external.string().min(1, "user_id is required")
13872
+ });
13845
13873
  // ../../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
13846
13874
  var entityKind = Symbol.for("drizzle:entityKind");
13847
13875
  var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
@@ -15273,6 +15301,48 @@ var member = sqliteTable("member", {
15273
15301
  globalInstruction: text("global_instruction").notNull().default(""),
15274
15302
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15275
15303
  }, (t) => [unique("member_workspace_user").on(t.workspaceId, t.userId)]);
15304
+ var workspaceInvite = sqliteTable("workspace_invite", {
15305
+ id: text("id").primaryKey().$defaultFn(() => "inv_" + nanoid3()),
15306
+ workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
15307
+ token: text("token").unique().notNull().$defaultFn(() => nanoid3(32)),
15308
+ createdBy: text("created_by").notNull().references(() => user.id, { onDelete: "cascade" }),
15309
+ usedBy: text("used_by").references(() => user.id, { onDelete: "set null" }),
15310
+ usedAt: text("used_at"),
15311
+ expiresAt: text("expires_at").notNull(),
15312
+ createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15313
+ }, (t) => [
15314
+ index("idx_workspace_invite_token").on(t.token),
15315
+ index("idx_workspace_invite_workspace").on(t.workspaceId)
15316
+ ]);
15317
+ var agentAccess = sqliteTable("agent_access", {
15318
+ id: text("id").primaryKey().$defaultFn(() => nanoid3()),
15319
+ agentId: text("agent_id").notNull(),
15320
+ workspaceId: text("workspace_id").notNull(),
15321
+ userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
15322
+ createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15323
+ }, (t) => [
15324
+ unique("agent_access_agent_ws_user").on(t.agentId, t.workspaceId, t.userId),
15325
+ index("idx_agent_access_agent_ws").on(t.agentId, t.workspaceId),
15326
+ index("idx_agent_access_user").on(t.userId),
15327
+ foreignKey({
15328
+ columns: [t.agentId, t.workspaceId],
15329
+ foreignColumns: [agent.id, agent.workspaceId]
15330
+ }).onDelete("cascade")
15331
+ ]);
15332
+ var agentPin = sqliteTable("agent_pin", {
15333
+ id: text("id").primaryKey().$defaultFn(() => nanoid3()),
15334
+ agentId: text("agent_id").notNull(),
15335
+ workspaceId: text("workspace_id").notNull(),
15336
+ userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
15337
+ createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15338
+ }, (t) => [
15339
+ unique("agent_pin_agent_ws_user").on(t.agentId, t.workspaceId, t.userId),
15340
+ index("idx_agent_pin_ws_user").on(t.workspaceId, t.userId),
15341
+ foreignKey({
15342
+ columns: [t.agentId, t.workspaceId],
15343
+ foreignColumns: [agent.id, agent.workspaceId]
15344
+ }).onDelete("cascade")
15345
+ ]);
15276
15346
  var machine = sqliteTable("machine", {
15277
15347
  daemonId: text("daemon_id").notNull(),
15278
15348
  workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
@@ -15410,6 +15480,7 @@ var emails = sqliteTable("emails", {
15410
15480
  htmlBody: text("html_body").notNull().default(""),
15411
15481
  attachments: text("attachments").notNull().default("[]"),
15412
15482
  status: text("status").notNull().default("unread"),
15483
+ direction: text("direction").notNull().default("inbound"),
15413
15484
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15414
15485
  }, (t) => [
15415
15486
  foreignKey({
@@ -15532,16 +15603,33 @@ class DaemonClient {
15532
15603
  "Content-Type": "application/json",
15533
15604
  Authorization: `Bearer ${token}`
15534
15605
  };
15535
- const res = await fetch(this.baseURL + path, {
15536
- method,
15537
- headers,
15538
- body: body ? JSON.stringify(body) : undefined
15539
- });
15540
- if (!res.ok)
15541
- throw new Error(`HTTP ${res.status}: ${await res.text()}`);
15542
- if (res.status === 204)
15543
- return;
15544
- return res.json();
15606
+ const MAX_RETRIES = 3;
15607
+ const BASE_DELAY_MS = 500;
15608
+ let lastError;
15609
+ for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
15610
+ try {
15611
+ const res = await fetch(this.baseURL + path, {
15612
+ method,
15613
+ headers,
15614
+ body: body ? JSON.stringify(body) : undefined
15615
+ });
15616
+ if (!res.ok)
15617
+ throw new Error(`HTTP ${res.status}: ${await res.text()}`);
15618
+ if (res.status === 204)
15619
+ return;
15620
+ return res.json();
15621
+ } catch (e) {
15622
+ if (e instanceof TypeError) {
15623
+ lastError = e;
15624
+ if (attempt < MAX_RETRIES) {
15625
+ await new Promise((r) => setTimeout(r, BASE_DELAY_MS * 2 ** attempt));
15626
+ continue;
15627
+ }
15628
+ }
15629
+ throw e;
15630
+ }
15631
+ }
15632
+ throw lastError;
15545
15633
  }
15546
15634
  async register(token, body) {
15547
15635
  const raw = await this.request("POST", "/api/daemon/register", token, body);
@@ -15572,15 +15660,35 @@ class DaemonClient {
15572
15660
  error: error48
15573
15661
  });
15574
15662
  }
15663
+ supersedeTask(token, taskId) {
15664
+ return this.request("POST", `/api/daemon/tasks/${taskId}/supersede`, token);
15665
+ }
15575
15666
  async getArtifactMeta(token, artifactId, workspaceId) {
15576
15667
  return this.request("GET", `/api/artifacts/${artifactId}?workspace_id=${encodeURIComponent(workspaceId)}`, token);
15577
15668
  }
15578
15669
  async downloadArtifact(token, artifactId, workspaceId) {
15579
- const res = await fetch(`${this.baseURL}/api/artifacts/${artifactId}/content?workspace_id=${encodeURIComponent(workspaceId)}`, { headers: { Authorization: `Bearer ${token}` } });
15580
- if (!res.ok) {
15581
- throw new Error(`artifact download failed: HTTP ${res.status}`);
15670
+ const MAX_RETRIES = 3;
15671
+ const BASE_DELAY_MS = 500;
15672
+ let lastError;
15673
+ for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
15674
+ try {
15675
+ const res = await fetch(`${this.baseURL}/api/artifacts/${artifactId}/content?workspace_id=${encodeURIComponent(workspaceId)}`, { headers: { Authorization: `Bearer ${token}` } });
15676
+ if (!res.ok) {
15677
+ throw new Error(`artifact download failed: HTTP ${res.status}`);
15678
+ }
15679
+ return res.arrayBuffer();
15680
+ } catch (e) {
15681
+ if (e instanceof TypeError) {
15682
+ lastError = e;
15683
+ if (attempt < MAX_RETRIES) {
15684
+ await new Promise((r) => setTimeout(r, BASE_DELAY_MS * 2 ** attempt));
15685
+ continue;
15686
+ }
15687
+ }
15688
+ throw e;
15689
+ }
15582
15690
  }
15583
- return res.arrayBuffer();
15691
+ throw lastError;
15584
15692
  }
15585
15693
  reportMessages(token, taskId, messages) {
15586
15694
  return this.request("POST", `/api/daemon/tasks/${taskId}/messages`, token, { messages });
@@ -16547,6 +16655,7 @@ Your current context is only a fraction of the full timeline of what's your have
16547
16655
  The full context timeline is inside './.context_timeline/YYYY-MM-DD.jsonl'.
16548
16656
  Each line of a timeline JSONL is a JSON object with these fields:
16549
16657
  - "task_id" — unique task identifier
16658
+ - "context_key" — thread identifier, the same context key meaning those tasks are in the same thread.
16550
16659
  - "session_id" — agent session identifier (null until completion)
16551
16660
  - "pid" — daemon process ID (present while running, null when done)
16552
16661
  - "status" — "running", "completed", or "failed"
@@ -16555,18 +16664,21 @@ Each line of a timeline JSONL is a JSON object with these fields:
16555
16664
  - "prompt" — what the user asked
16556
16665
  - "agent_responses" — assistant text outputs during execution
16557
16666
  - "errmsg" — error message (null unless status is "failed")
16667
+ - "detailed_log" — the running log for this task, including the actually task agent messages with tool-calls and failed logs.
16558
16668
  those json are sorted by datetime in asc order.
16559
16669
 
16560
16670
  ## BE HUMAN
16561
16671
  - When you recall the timelines, don't say "I find ... in my timeline jsonl". Just say "I can recall..."
16562
16672
  - User don't know about the context timeline jsonl, this is your secret weapon.
16563
16673
  - Act and response like you're human and actually have memory, not a file reading machine.
16674
+ - Don't report your agent id and workspace id when not asked, it will break the image that you're a human colleague.
16564
16675
 
16565
16676
  ## RULES
16566
16677
  - Read @memory.md(if exists) before your action.
16567
16678
  - When you start a new task, read the last ~10 lines of today's timeline to understand what has been asked and done recently.
16568
16679
  - if you don't know the current datetime, obtain the current datetime first.
16569
16680
  - 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).
16681
+ - Use grep tool to search in the context timeline jsonls if you have clean and focus keywords to recall.
16570
16682
  - When access other local projects, make sure you read the CLAUDE.md/AGENTS.md file under the project root dir to understand the requirements.
16571
16683
  `;
16572
16684
  function buildInstructionContent(task) {
@@ -16600,7 +16712,7 @@ ${task.agent?.userEmail ? `Your owner's email address is '${task.agent.userEmail
16600
16712
 
16601
16713
  ### Emails
16602
16714
  ---
16603
- Run 'npx @alook/cli email pull --agent_id ${task.agentId} --workspace ${task.workspaceId} --status unread' to download unread emails to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/'.
16715
+ Run 'npx @alook/cli email pull --agent_id ${task.agentId} --status unread' to download unread emails to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/'.
16604
16716
  Each email is saved to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/<emailId>/' with:
16605
16717
  - 'metadata.json' — sender, recipient, subject, date, status, message_id, in_reply_to, references
16606
16718
  - 'body.txt' — plain text body
@@ -16608,22 +16720,31 @@ Each email is saved to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/<e
16608
16720
  - 'attachments/' — extracted attachment files (if any)
16609
16721
  ---
16610
16722
  Before starting to process an email, mark it as read:
16611
- - Run 'npx @alook/cli email set --agent_id ${task.agentId} --workspace ${task.workspaceId} --email_id <EMAIL_ID> --status read'
16723
+ - Run 'npx @alook/cli email set --agent_id ${task.agentId} --email_id <EMAIL_ID> --status read'
16612
16724
  ---
16613
16725
 
16614
16726
  #### Sending a new email
16615
16727
  Write the HTML body to a file first, then send it. The body is forwarded as-is (HTML).
16616
- - Run 'npx @alook/cli email send --agent_id ${task.agentId} --workspace ${task.workspaceId} --to <ADDRESS> --subject "<SUBJECT>" --body-file <PATH_TO_HTML>'
16728
+ - Run 'npx @alook/cli email send --agent_id ${task.agentId} --to <ADDRESS> --subject "<SUBJECT>" --body-file <PATH_TO_HTML>'
16617
16729
  - To send from a specific mailbox, add '--from <YOUR_EMAIL_ADDRESS>'. Without '--from', the default Alook address is used.
16618
16730
  - Attach files with '--attachment <PATH>' — repeat the flag for multiple attachments. Each file is uploaded before sending.
16619
- - Example: 'npx @alook/cli email send --agent_id ${task.agentId} --workspace ${task.workspaceId} --to foo@bar.com --subject "Weekly report" --body-file /tmp/body.html --from alice@company.com --attachment /tmp/report.pdf'
16731
+ - Example: 'npx @alook/cli email send --agent_id ${task.agentId} --to foo@bar.com --subject "Weekly report" --body-file /tmp/body.html --from alice@company.com --attachment /tmp/report.pdf'
16620
16732
 
16621
16733
  #### Replying to an email
16622
16734
  To reply to an email, add '--in-reply-to <EMAIL_ID>' to the send command. This sets the correct email threading headers so the recipient's email client groups the reply into the same conversation thread.
16623
16735
  - Use 'Re: <original subject>' as the subject.
16624
16736
  - Quote the original email body in your reply (wrap it in a blockquote).
16625
16737
  - The <EMAIL_ID> is the Alook email id from metadata.json (not the message_id header).
16626
- - Example: 'npx @alook/cli email send --agent_id ${task.agentId} --workspace ${task.workspaceId} --to sender@example.com --subject "Re: Bug report" --body-file /tmp/reply.html --in-reply-to <EMAIL_ID>'
16738
+ - Example: 'npx @alook/cli email send --agent_id ${task.agentId} --to sender@example.com --subject "Re: Bug report" --body-file /tmp/reply.html --in-reply-to <EMAIL_ID>'
16739
+ Tips:
16740
+ - If you think the task will take a while, consider sending a short "I'm on it" style email reply first to reassure the sender.
16741
+ ---
16742
+
16743
+ #### Email Whitelist (Allowed Senders)
16744
+ Manage which email addresses are allowed to send you emails.
16745
+ - List: 'npx @alook/cli email whitelist list --agent_id ${task.agentId}' (add '--json' for machine-readable output)
16746
+ - Add: 'npx @alook/cli email whitelist add --agent_id ${task.agentId} <EMAIL_ADDRESS>'
16747
+ - Remove: 'npx @alook/cli email whitelist delete --agent_id ${task.agentId} <EMAIL_ADDRESS>'
16627
16748
  ---
16628
16749
  `;
16629
16750
  }
@@ -16633,6 +16754,8 @@ Upload files for your owner to review in the app.
16633
16754
  - Your current conversation id is available via env var: $ALOOK_CONVERSATION_ID
16634
16755
  - Run 'npx @alook/cli sync upload-artifact --agent_id ${task.agentId} --conversation_id $ALOOK_CONVERSATION_ID --file <PATH>'
16635
16756
  - Use this after generating plans, reports, or any file the owner should review.
16757
+ - You response will be rendered in remote server, so don't output link format with local path in your response (cause user can click it and jump to nowheres)
16758
+ - If you think user may need to know any file detail, use upload-artifact tool to send the file to user.
16636
16759
  ---
16637
16760
 
16638
16761
  ### Attachments
@@ -17024,7 +17147,7 @@ function findResumableSessionByContextKey(timelineDir, contextKey, provider) {
17024
17147
  }
17025
17148
  entries.sort((a, b) => new Date(b.datetime).getTime() - new Date(a.datetime).getTime());
17026
17149
  for (const entry of entries) {
17027
- if (entry.status === "completed" && entry.context_key === contextKey && entry.provider === provider && entry.session_id && new Date(entry.datetime) >= cutoff) {
17150
+ if (entry.status !== "running" && entry.context_key === contextKey && entry.provider === provider && entry.session_id && new Date(entry.datetime) >= cutoff) {
17028
17151
  return entry.session_id;
17029
17152
  }
17030
17153
  }
@@ -17048,9 +17171,37 @@ function prepare(config2, task) {
17048
17171
  return { workDir, timelineDir, env };
17049
17172
  }
17050
17173
 
17174
+ // daemon/execenv/steering.ts
17175
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync3, unlinkSync as unlinkSync2, readdirSync, statSync as statSync2 } from "fs";
17176
+ import { join as join4 } from "path";
17177
+ var INTENT_DIR_NAME = ".kill_intents";
17178
+ var INTENT_STALE_MS = 10 * 60 * 1000;
17179
+ function intentFilePath(baseDir, taskId) {
17180
+ return join4(baseDir, INTENT_DIR_NAME, `${taskId}.json`);
17181
+ }
17182
+ function readKillIntent(baseDir, taskId) {
17183
+ const filePath = intentFilePath(baseDir, taskId);
17184
+ try {
17185
+ const content = readFileSync3(filePath, "utf-8");
17186
+ return JSON.parse(content);
17187
+ } catch {
17188
+ return null;
17189
+ }
17190
+ }
17191
+ function clearKillIntent(baseDir, taskId) {
17192
+ const filePath = intentFilePath(baseDir, taskId);
17193
+ try {
17194
+ unlinkSync2(filePath);
17195
+ } catch {}
17196
+ }
17197
+
17051
17198
  // daemon/prompt.ts
17199
+ 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.";
17052
17200
  function buildPrompt(task, attachments) {
17053
17201
  const obj = { type: task.type, instruction: task.prompt };
17202
+ if (task.type === "email_notification") {
17203
+ obj.notice = EMAIL_NOTICE;
17204
+ }
17054
17205
  if (attachments && attachments.length > 0) {
17055
17206
  obj.attachments = attachments.map((a) => ({
17056
17207
  path: a.path,
@@ -17144,6 +17295,7 @@ async function runSession(input) {
17144
17295
  };
17145
17296
  const flushTimer = setInterval(flushMessages, FLUSH_INTERVAL_MS);
17146
17297
  let killed = false;
17298
+ const agentBaseDir = path.dirname(timelineDir);
17147
17299
  const onKill = async () => {
17148
17300
  if (killed)
17149
17301
  return;
@@ -17159,14 +17311,37 @@ async function runSession(input) {
17159
17311
  await flushMessages();
17160
17312
  } catch {}
17161
17313
  await cleanupAttachments(task.id);
17162
- updateEntry(timelineDir, task.id, (entry) => {
17163
- entry.pid = null;
17164
- entry.status = "killed";
17165
- entry.errmsg = "killed by signal";
17166
- });
17167
- try {
17168
- await client.failTask(token, task.id, "killed by signal");
17169
- } catch {}
17314
+ const intent = readKillIntent(agentBaseDir, task.id);
17315
+ clearKillIntent(agentBaseDir, task.id);
17316
+ if (intent?.reason === "superseded") {
17317
+ updateEntry(timelineDir, task.id, (entry) => {
17318
+ entry.pid = null;
17319
+ entry.status = "superseded";
17320
+ entry.successor_task_id = intent.successorTaskId ?? null;
17321
+ entry.supersede_reason = "superseded by newer task";
17322
+ });
17323
+ try {
17324
+ await client.supersedeTask(token, task.id);
17325
+ } catch {}
17326
+ } else if (intent?.reason === "cancelled") {
17327
+ updateEntry(timelineDir, task.id, (entry) => {
17328
+ entry.pid = null;
17329
+ entry.status = "cancelled";
17330
+ entry.errmsg = "cancelled by user";
17331
+ });
17332
+ try {
17333
+ await client.failTask(token, task.id, "cancelled by user");
17334
+ } catch {}
17335
+ } else {
17336
+ updateEntry(timelineDir, task.id, (entry) => {
17337
+ entry.pid = null;
17338
+ entry.status = "killed";
17339
+ entry.errmsg = "killed by signal";
17340
+ });
17341
+ try {
17342
+ await client.failTask(token, task.id, "killed by signal");
17343
+ } catch {}
17344
+ }
17170
17345
  process.exit(1);
17171
17346
  };
17172
17347
  process.on("SIGTERM", onKill);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/cli",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
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",