@alook/cli 0.0.15 → 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({
@@ -15883,6 +15954,9 @@ class DaemonClient {
15883
15954
  error: error48
15884
15955
  });
15885
15956
  }
15957
+ supersedeTask(token, taskId) {
15958
+ return this.request("POST", `/api/daemon/tasks/${taskId}/supersede`, token);
15959
+ }
15886
15960
  async getArtifactMeta(token, artifactId, workspaceId) {
15887
15961
  return this.request("GET", `/api/artifacts/${artifactId}?workspace_id=${encodeURIComponent(workspaceId)}`, token);
15888
15962
  }
@@ -16316,13 +16390,172 @@ async function handleCliUpdate(version3, onSuccess, profile) {
16316
16390
  }
16317
16391
  }
16318
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
+
16319
16552
  // daemon/daemon.ts
16320
- 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";
16321
16554
  import { execSync as execSync3, spawn as spawn2 } from "child_process";
16322
16555
  import { fileURLToPath as fileURLToPath2 } from "url";
16323
- import { dirname as dirname3, join as join4 } from "path";
16556
+ import { dirname as dirname3, join as join6 } from "path";
16324
16557
  var _dir = dirname3(fileURLToPath2(import.meta.url));
16325
- 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");
16326
16559
  function isCommandAvailable2(cmd) {
16327
16560
  try {
16328
16561
  const check2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
@@ -16332,21 +16565,21 @@ function isCommandAvailable2(cmd) {
16332
16565
  return false;
16333
16566
  }
16334
16567
  }
16335
- var MAX_SESSION_RUNNER_LOGS = 50;
16568
+ var MAX_SESSION_RUNNER_LOGS = 500;
16336
16569
  function pruneSessionRunnerLogs() {
16337
16570
  const logDir = sessionRunnerLogDir();
16338
16571
  let entries;
16339
16572
  try {
16340
- entries = readdirSync(logDir).filter((f) => f.endsWith(".log"));
16573
+ entries = readdirSync2(logDir).filter((f) => f.endsWith(".log"));
16341
16574
  } catch {
16342
16575
  return;
16343
16576
  }
16344
16577
  if (entries.length <= MAX_SESSION_RUNNER_LOGS)
16345
16578
  return;
16346
16579
  const withMtime = entries.map((name) => {
16347
- const full = join4(logDir, name);
16580
+ const full = join6(logDir, name);
16348
16581
  try {
16349
- return { name, mtime: statSync(full).mtimeMs };
16582
+ return { name, mtime: statSync3(full).mtimeMs };
16350
16583
  } catch {
16351
16584
  return { name, mtime: 0 };
16352
16585
  }
@@ -16354,7 +16587,7 @@ function pruneSessionRunnerLogs() {
16354
16587
  withMtime.sort((a, b) => b.mtime - a.mtime);
16355
16588
  for (const entry of withMtime.slice(MAX_SESSION_RUNNER_LOGS)) {
16356
16589
  try {
16357
- unlinkSync3(join4(logDir, entry.name));
16590
+ unlinkSync4(join6(logDir, entry.name));
16358
16591
  } catch {}
16359
16592
  }
16360
16593
  }
@@ -16433,12 +16666,7 @@ async function startDaemon(profile, serverUrl) {
16433
16666
  });
16434
16667
  } catch (e) {
16435
16668
  if (e instanceof Error && e.message.startsWith("HTTP 401")) {
16436
- log.warn(`Workspace ${ws.id} token invalid — removing from config`);
16437
- try {
16438
- const cfg = loadCLIConfigForProfile(profile);
16439
- cfg.watched_workspaces = (cfg.watched_workspaces || []).filter((w) => w.id !== ws.id);
16440
- saveCLIConfigForProfile(profile, cfg);
16441
- } catch {}
16669
+ log.warn(`Workspace ${ws.id} token invalid — skipping (run '${cmdPrefix()} register --token <token>' to fix)`);
16442
16670
  } else {
16443
16671
  log.error(`Failed to register workspace ${ws.id}, skipping`, e);
16444
16672
  }
@@ -16497,7 +16725,7 @@ async function startDaemon(profile, serverUrl) {
16497
16725
  cfg.watched_workspaces = (cfg.watched_workspaces || []).filter((w) => w.id !== workspaceId);
16498
16726
  saveCLIConfigForProfile(profile, cfg);
16499
16727
  } catch {}
16500
- log.info(`Workspace ${workspaceId} evictedruntimes removed server-side`);
16728
+ log.info(`Workspace ${workspaceId} deleted server-side — removed from config`);
16501
16729
  }
16502
16730
  const pollCycle = async () => {
16503
16731
  let remaining = config2.maxConcurrentTasks - activeTasks.size;
@@ -16534,7 +16762,7 @@ async function startDaemon(profile, serverUrl) {
16534
16762
  }
16535
16763
  } catch (e) {
16536
16764
  if (e instanceof Error && e.message.startsWith("HTTP 401")) {
16537
- evictedIds.push(ws.workspaceId);
16765
+ log.warn(`Workspace ${ws.workspaceId} poll returned 401 — will retry next cycle`);
16538
16766
  } else {
16539
16767
  log.debug("Poll error", e);
16540
16768
  }
@@ -16571,17 +16799,28 @@ async function startDaemon(profile, serverUrl) {
16571
16799
  releaseDaemonPid(profile);
16572
16800
  health.server.close(() => {
16573
16801
  if (restartRequested) {
16574
- const args = ["daemon", "start", "--foreground"];
16802
+ const entry = process.argv[1];
16803
+ const args = [entry, "daemon", "start", "--foreground"];
16575
16804
  if (profile)
16576
16805
  args.push("--profile", profile);
16577
16806
  if (serverUrl)
16578
16807
  args.push("--server", serverUrl);
16579
- 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, {
16580
16817
  detached: true,
16581
- stdio: ["ignore", "ignore", "ignore"]
16818
+ stdio: logFd != null ? ["ignore", logFd, logFd] : ["ignore", "ignore", "ignore"]
16582
16819
  });
16583
16820
  child.unref();
16584
- 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}`);
16585
16824
  }
16586
16825
  clearTimeout(timeout);
16587
16826
  process.exit(0);
@@ -16593,21 +16832,62 @@ async function startDaemon(profile, serverUrl) {
16593
16832
  }
16594
16833
  function spawnSessionRunner(input) {
16595
16834
  const logDir = sessionRunnerLogDir();
16596
- mkdirSync3(logDir, { recursive: true });
16597
- const logFilePath = join4(logDir, `${input.task.id}.log`);
16835
+ mkdirSync5(logDir, { recursive: true });
16836
+ const logFilePath = join6(logDir, `${input.task.id}.log`);
16598
16837
  input.logFilePath = logFilePath;
16599
16838
  const encoded = Buffer.from(JSON.stringify(input)).toString("base64");
16600
- 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
+ }
16601
16845
  const child = spawn2(process.execPath, [sessionRunnerPath, encoded], {
16602
16846
  detached: true,
16603
- stdio: ["ignore", fd, fd]
16847
+ stdio: fd != null ? ["ignore", fd, fd] : ["ignore", "ignore", "ignore"]
16604
16848
  });
16605
16849
  child.unref();
16606
- closeSync(fd);
16850
+ if (fd != null)
16851
+ closeSync(fd);
16607
16852
  return child;
16608
16853
  }
16609
16854
  async function handleTask(client, config2, runtimeIndex, task, token, activeTasks) {
16610
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
+ }
16611
16891
  try {
16612
16892
  await client.startTask(token, task.id);
16613
16893
  } catch (e) {
@@ -16622,6 +16902,60 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
16622
16902
  return;
16623
16903
  }
16624
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
+ }
16625
16959
  const cliPath = provider === "claude" ? config2.claudePath : provider === "codex" ? config2.codexPath : config2.opencodePath;
16626
16960
  const configModel = provider === "claude" ? config2.claudeModel : provider === "codex" ? config2.codexModel : config2.opencodeModel;
16627
16961
  const agentModel = task.agent?.runtimeConfig?.model;
@@ -16676,7 +17010,7 @@ async function startInBackground(profile, serverUrl) {
16676
17010
  return;
16677
17011
  }
16678
17012
  const logPath = daemonLogFilePath();
16679
- mkdirSync4(dirname4(logPath), { recursive: true, mode: 448 });
17013
+ mkdirSync6(dirname4(logPath), { recursive: true, mode: 448 });
16680
17014
  const logFd = openSync2(logPath, "a", 384);
16681
17015
  const child = spawn3(process.execPath, buildChildArgs(profile, serverUrl), {
16682
17016
  detached: true,
@@ -16767,6 +17101,14 @@ function daemonCommand() {
16767
17101
  import { Command as Command4 } from "commander";
16768
17102
 
16769
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
+ }
16770
17112
  function printJSON(data) {
16771
17113
  console.log(JSON.stringify(data, null, 2));
16772
17114
  }
@@ -16786,8 +17128,8 @@ function configCommand() {
16786
17128
 
16787
17129
  // commands/email.ts
16788
17130
  import { Command as Command5 } from "commander";
16789
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
16790
- 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";
16791
17133
  import PostalMime from "postal-mime";
16792
17134
  var VALID_STATUSES = ["unread", "read", "archived"];
16793
17135
  var EMAIL_BASE = "/tmp/alook-emails";
@@ -16818,7 +17160,10 @@ function collectRepeated(value, previous) {
16818
17160
  return previous.concat([value]);
16819
17161
  }
16820
17162
  function resolveClientOpts(command, opts) {
16821
- const parentOpts = command.parent?.parent?.opts() || {};
17163
+ let root = command;
17164
+ while (root.parent)
17165
+ root = root.parent;
17166
+ const parentOpts = root.opts() || {};
16822
17167
  const profile = parentOpts.profile;
16823
17168
  const cfg = loadCLIConfigForProfile(profile);
16824
17169
  const serverUrl = parentOpts.server || cfg.server_url;
@@ -16847,7 +17192,7 @@ function emailCommand() {
16847
17192
  console.error(`Error: invalid status "${opts.status}", must be one of: ${VALID_STATUSES.join(", ")}`);
16848
17193
  process.exit(1);
16849
17194
  }
16850
- const emailDir_base = join5(EMAIL_BASE, workspaceId, opts.agent_id);
17195
+ const emailDir_base = join7(EMAIL_BASE, workspaceId, opts.agent_id);
16851
17196
  try {
16852
17197
  let query = `/api/email?agentId=${opts.agent_id}`;
16853
17198
  if (opts.status)
@@ -16861,11 +17206,11 @@ function emailCommand() {
16861
17206
  printJSON(emails2);
16862
17207
  return;
16863
17208
  }
16864
- mkdirSync5(emailDir_base, { recursive: true });
17209
+ mkdirSync7(emailDir_base, { recursive: true });
16865
17210
  const downloadedPaths = [];
16866
17211
  for (const email3 of emails2) {
16867
- const emailDir = join5(emailDir_base, email3.id);
16868
- mkdirSync5(emailDir, { recursive: true });
17212
+ const emailDir = join7(emailDir_base, email3.id);
17213
+ mkdirSync7(emailDir, { recursive: true });
16869
17214
  const metadata = {
16870
17215
  id: email3.id,
16871
17216
  from: email3.from_email,
@@ -16877,8 +17222,8 @@ function emailCommand() {
16877
17222
  in_reply_to: email3.in_reply_to || "",
16878
17223
  references: email3.references || ""
16879
17224
  };
16880
- const metadataPath = join5(emailDir, "metadata.json");
16881
- writeFileSync4(metadataPath, JSON.stringify(metadata, null, 2));
17225
+ const metadataPath = join7(emailDir, "metadata.json");
17226
+ writeFileSync6(metadataPath, JSON.stringify(metadata, null, 2));
16882
17227
  downloadedPaths.push(metadataPath);
16883
17228
  let rawMime;
16884
17229
  try {
@@ -16893,18 +17238,18 @@ function emailCommand() {
16893
17238
  }
16894
17239
  const parsed = await new PostalMime().parse(rawMime);
16895
17240
  if (parsed.text) {
16896
- const bodyPath = join5(emailDir, "body.txt");
16897
- writeFileSync4(bodyPath, parsed.text);
17241
+ const bodyPath = join7(emailDir, "body.txt");
17242
+ writeFileSync6(bodyPath, parsed.text);
16898
17243
  downloadedPaths.push(bodyPath);
16899
17244
  }
16900
17245
  if (parsed.html) {
16901
- const htmlPath = join5(emailDir, "body.html");
16902
- writeFileSync4(htmlPath, parsed.html);
17246
+ const htmlPath = join7(emailDir, "body.html");
17247
+ writeFileSync6(htmlPath, parsed.html);
16903
17248
  downloadedPaths.push(htmlPath);
16904
17249
  }
16905
17250
  if (parsed.attachments && parsed.attachments.length > 0) {
16906
- const attDir = join5(emailDir, "attachments");
16907
- mkdirSync5(attDir, { recursive: true });
17251
+ const attDir = join7(emailDir, "attachments");
17252
+ mkdirSync7(attDir, { recursive: true });
16908
17253
  const usedFilenames = new Set;
16909
17254
  for (let i = 0;i < parsed.attachments.length; i++) {
16910
17255
  const att = parsed.attachments[i];
@@ -16913,7 +17258,7 @@ function emailCommand() {
16913
17258
  filename = `${i}-${filename}`;
16914
17259
  }
16915
17260
  usedFilenames.add(filename);
16916
- const attPath = join5(attDir, filename);
17261
+ const attPath = join7(attDir, filename);
16917
17262
  const content = att.content;
16918
17263
  let buf;
16919
17264
  if (typeof content === "string") {
@@ -16923,7 +17268,7 @@ function emailCommand() {
16923
17268
  } else {
16924
17269
  buf = Buffer.from(content);
16925
17270
  }
16926
- writeFileSync4(attPath, buf);
17271
+ writeFileSync6(attPath, buf);
16927
17272
  downloadedPaths.push(attPath);
16928
17273
  }
16929
17274
  }
@@ -16962,7 +17307,7 @@ function emailCommand() {
16962
17307
  const client = new APIClient(serverUrl, token, workspaceId);
16963
17308
  let htmlBody;
16964
17309
  try {
16965
- htmlBody = readFileSync5(opts.bodyFile, "utf-8");
17310
+ htmlBody = readFileSync7(opts.bodyFile, "utf-8");
16966
17311
  } catch (err) {
16967
17312
  console.error(`Error: cannot read body file "${opts.bodyFile}": ${err instanceof Error ? err.message : err}`);
16968
17313
  process.exit(1);
@@ -16978,8 +17323,8 @@ function emailCommand() {
16978
17323
  let bytes;
16979
17324
  let size;
16980
17325
  try {
16981
- bytes = readFileSync5(path);
16982
- size = statSync2(path).size;
17326
+ bytes = readFileSync7(path);
17327
+ size = statSync4(path).size;
16983
17328
  } catch (err) {
16984
17329
  console.error(`Error: cannot read attachment "${path}": ${err instanceof Error ? err.message : err}`);
16985
17330
  process.exit(1);
@@ -17024,6 +17369,73 @@ function emailCommand() {
17024
17369
  process.exit(1);
17025
17370
  }
17026
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);
17027
17439
  return cmd;
17028
17440
  }
17029
17441
 
@@ -17283,7 +17695,7 @@ ${result.output}`);
17283
17695
 
17284
17696
  // commands/sync.ts
17285
17697
  import { Command as Command9 } from "commander";
17286
- import { readFileSync as readFileSync6, statSync as statSync3 } from "fs";
17698
+ import { readFileSync as readFileSync8, statSync as statSync5 } from "fs";
17287
17699
  import { basename as basename2 } from "path";
17288
17700
  var MIME_BY_EXT2 = {
17289
17701
  ".pdf": "application/pdf",
@@ -17332,9 +17744,9 @@ function syncCommand() {
17332
17744
  let bytes;
17333
17745
  let size;
17334
17746
  try {
17335
- const stat = statSync3(opts.file);
17747
+ const stat = statSync5(opts.file);
17336
17748
  size = stat.size;
17337
- bytes = readFileSync6(opts.file);
17749
+ bytes = readFileSync8(opts.file);
17338
17750
  } catch (err) {
17339
17751
  console.error(`Error: cannot read file "${opts.file}": ${err.message}`);
17340
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({
@@ -15589,6 +15660,9 @@ class DaemonClient {
15589
15660
  error: error48
15590
15661
  });
15591
15662
  }
15663
+ supersedeTask(token, taskId) {
15664
+ return this.request("POST", `/api/daemon/tasks/${taskId}/supersede`, token);
15665
+ }
15592
15666
  async getArtifactMeta(token, artifactId, workspaceId) {
15593
15667
  return this.request("GET", `/api/artifacts/${artifactId}?workspace_id=${encodeURIComponent(workspaceId)}`, token);
15594
15668
  }
@@ -16581,6 +16655,7 @@ Your current context is only a fraction of the full timeline of what's your have
16581
16655
  The full context timeline is inside './.context_timeline/YYYY-MM-DD.jsonl'.
16582
16656
  Each line of a timeline JSONL is a JSON object with these fields:
16583
16657
  - "task_id" — unique task identifier
16658
+ - "context_key" — thread identifier, the same context key meaning those tasks are in the same thread.
16584
16659
  - "session_id" — agent session identifier (null until completion)
16585
16660
  - "pid" — daemon process ID (present while running, null when done)
16586
16661
  - "status" — "running", "completed", or "failed"
@@ -16589,18 +16664,21 @@ Each line of a timeline JSONL is a JSON object with these fields:
16589
16664
  - "prompt" — what the user asked
16590
16665
  - "agent_responses" — assistant text outputs during execution
16591
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.
16592
16668
  those json are sorted by datetime in asc order.
16593
16669
 
16594
16670
  ## BE HUMAN
16595
16671
  - When you recall the timelines, don't say "I find ... in my timeline jsonl". Just say "I can recall..."
16596
16672
  - User don't know about the context timeline jsonl, this is your secret weapon.
16597
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.
16598
16675
 
16599
16676
  ## RULES
16600
16677
  - Read @memory.md(if exists) before your action.
16601
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.
16602
16679
  - if you don't know the current datetime, obtain the current datetime first.
16603
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.
16604
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.
16605
16683
  `;
16606
16684
  function buildInstructionContent(task) {
@@ -16634,7 +16712,7 @@ ${task.agent?.userEmail ? `Your owner's email address is '${task.agent.userEmail
16634
16712
 
16635
16713
  ### Emails
16636
16714
  ---
16637
- 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}/'.
16638
16716
  Each email is saved to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/<emailId>/' with:
16639
16717
  - 'metadata.json' — sender, recipient, subject, date, status, message_id, in_reply_to, references
16640
16718
  - 'body.txt' — plain text body
@@ -16642,22 +16720,31 @@ Each email is saved to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/<e
16642
16720
  - 'attachments/' — extracted attachment files (if any)
16643
16721
  ---
16644
16722
  Before starting to process an email, mark it as read:
16645
- - 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'
16646
16724
  ---
16647
16725
 
16648
16726
  #### Sending a new email
16649
16727
  Write the HTML body to a file first, then send it. The body is forwarded as-is (HTML).
16650
- - 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>'
16651
16729
  - To send from a specific mailbox, add '--from <YOUR_EMAIL_ADDRESS>'. Without '--from', the default Alook address is used.
16652
16730
  - Attach files with '--attachment <PATH>' — repeat the flag for multiple attachments. Each file is uploaded before sending.
16653
- - 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'
16654
16732
 
16655
16733
  #### Replying to an email
16656
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.
16657
16735
  - Use 'Re: <original subject>' as the subject.
16658
16736
  - Quote the original email body in your reply (wrap it in a blockquote).
16659
16737
  - The <EMAIL_ID> is the Alook email id from metadata.json (not the message_id header).
16660
- - 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>'
16661
16748
  ---
16662
16749
  `;
16663
16750
  }
@@ -16667,6 +16754,8 @@ Upload files for your owner to review in the app.
16667
16754
  - Your current conversation id is available via env var: $ALOOK_CONVERSATION_ID
16668
16755
  - Run 'npx @alook/cli sync upload-artifact --agent_id ${task.agentId} --conversation_id $ALOOK_CONVERSATION_ID --file <PATH>'
16669
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.
16670
16759
  ---
16671
16760
 
16672
16761
  ### Attachments
@@ -17058,7 +17147,7 @@ function findResumableSessionByContextKey(timelineDir, contextKey, provider) {
17058
17147
  }
17059
17148
  entries.sort((a, b) => new Date(b.datetime).getTime() - new Date(a.datetime).getTime());
17060
17149
  for (const entry of entries) {
17061
- 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) {
17062
17151
  return entry.session_id;
17063
17152
  }
17064
17153
  }
@@ -17082,9 +17171,37 @@ function prepare(config2, task) {
17082
17171
  return { workDir, timelineDir, env };
17083
17172
  }
17084
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
+
17085
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.";
17086
17200
  function buildPrompt(task, attachments) {
17087
17201
  const obj = { type: task.type, instruction: task.prompt };
17202
+ if (task.type === "email_notification") {
17203
+ obj.notice = EMAIL_NOTICE;
17204
+ }
17088
17205
  if (attachments && attachments.length > 0) {
17089
17206
  obj.attachments = attachments.map((a) => ({
17090
17207
  path: a.path,
@@ -17178,6 +17295,7 @@ async function runSession(input) {
17178
17295
  };
17179
17296
  const flushTimer = setInterval(flushMessages, FLUSH_INTERVAL_MS);
17180
17297
  let killed = false;
17298
+ const agentBaseDir = path.dirname(timelineDir);
17181
17299
  const onKill = async () => {
17182
17300
  if (killed)
17183
17301
  return;
@@ -17193,14 +17311,37 @@ async function runSession(input) {
17193
17311
  await flushMessages();
17194
17312
  } catch {}
17195
17313
  await cleanupAttachments(task.id);
17196
- updateEntry(timelineDir, task.id, (entry) => {
17197
- entry.pid = null;
17198
- entry.status = "killed";
17199
- entry.errmsg = "killed by signal";
17200
- });
17201
- try {
17202
- await client.failTask(token, task.id, "killed by signal");
17203
- } 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
+ }
17204
17345
  process.exit(1);
17205
17346
  };
17206
17347
  process.on("SIGTERM", onKill);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/cli",
3
- "version": "0.0.15",
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",