@alook/cli 0.0.25 → 0.0.27

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
@@ -13908,7 +13908,8 @@ var TaskAgentDataApiSchema = exports_external.object({
13908
13908
  runtime_config: exports_external.record(exports_external.string(), exports_external.unknown()).default({}),
13909
13909
  email_handle: exports_external.string().nullable().optional(),
13910
13910
  email_addresses: exports_external.array(exports_external.string()).default([]),
13911
- user_email: exports_external.string().nullable().optional()
13911
+ user_email: exports_external.string().nullable().optional(),
13912
+ user_name: exports_external.string().nullable().optional()
13912
13913
  });
13913
13914
  var TaskApiBaseSchema = exports_external.object({
13914
13915
  id: exports_external.string(),
@@ -13949,12 +13950,20 @@ var FileRequestItemSchema = exports_external.object({
13949
13950
  request_type: exports_external.enum(["tree", "read"]),
13950
13951
  path: exports_external.string()
13951
13952
  });
13953
+ var PollMeetingItemSchema = exports_external.object({
13954
+ id: exports_external.string(),
13955
+ meeting_url: exports_external.string(),
13956
+ participants: exports_external.array(exports_external.string()),
13957
+ workspace_id: exports_external.string(),
13958
+ agent_name: exports_external.string()
13959
+ });
13952
13960
  var PollResponseSchema = exports_external.object({
13953
13961
  tasks: exports_external.array(TaskApiSchema),
13954
13962
  evicted: exports_external.boolean().optional(),
13955
13963
  pending_update: exports_external.object({ version: exports_external.string() }).optional(),
13956
13964
  pending_rescan: exports_external.boolean().optional(),
13957
- file_requests: exports_external.array(FileRequestItemSchema).optional()
13965
+ file_requests: exports_external.array(FileRequestItemSchema).optional(),
13966
+ meetings: exports_external.array(PollMeetingItemSchema).optional()
13958
13967
  });
13959
13968
  var RegisterResponseSchema = exports_external.object({
13960
13969
  runtimes: exports_external.array(exports_external.object({ id: exports_external.string() }))
@@ -14102,10 +14111,11 @@ var SendEmailRequestSchema = exports_external.object({
14102
14111
  references: exports_external.string().optional(),
14103
14112
  attachments: exports_external.array(EmailAttachmentSchema).optional(),
14104
14113
  customAccountId: exports_external.string().optional(),
14105
- from: exports_external.string().email().optional()
14114
+ from: exports_external.string().email().optional(),
14115
+ conversationId: exports_external.string().optional()
14106
14116
  });
14107
14117
  var UpdateEmailStatusRequestSchema = exports_external.object({
14108
- status: exports_external.enum(["unread", "read", "archived"])
14118
+ status: exports_external.enum(["unread", "read", "archived", "sent"])
14109
14119
  });
14110
14120
  var MeetingInfoSchema = exports_external.object({
14111
14121
  title: exports_external.string(),
@@ -15942,6 +15952,15 @@ var machineToken = sqliteTable("machine_token", {
15942
15952
  lastUsedAt: text("last_used_at"),
15943
15953
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15944
15954
  }, (t) => [index("idx_machine_token").on(t.token)]);
15955
+ var conversationMap = sqliteTable("conversation_map", {
15956
+ id: text("id").primaryKey().$defaultFn(() => nanoid3()),
15957
+ key: text("key").notNull(),
15958
+ workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
15959
+ conversationId: text("conversation_id").notNull().references(() => conversation.id, { onDelete: "cascade" }),
15960
+ createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15961
+ }, (t) => [
15962
+ unique("conversation_map_key_workspace").on(t.key, t.workspaceId)
15963
+ ]);
15945
15964
  var workspaceFileRequest = sqliteTable("workspace_file_request", {
15946
15965
  id: text("id").primaryKey().$defaultFn(() => "wfr_" + nanoid3()),
15947
15966
  workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
@@ -16045,7 +16064,8 @@ class DaemonClient {
16045
16064
  evicted: resp.evicted ?? false,
16046
16065
  pending_update: resp.pending_update,
16047
16066
  pending_rescan: resp.pending_rescan,
16048
- file_requests: resp.file_requests
16067
+ file_requests: resp.file_requests,
16068
+ meetings: resp.meetings
16049
16069
  };
16050
16070
  }
16051
16071
  startTask(token, taskId) {
@@ -16095,15 +16115,6 @@ class DaemonClient {
16095
16115
  reportFileData(token, body) {
16096
16116
  return this.request("POST", "/api/daemon/workspace/report", token, body);
16097
16117
  }
16098
- async claimMeetings(token, daemonId) {
16099
- const raw = await this.request("POST", "/api/daemon/meetings/claim", token, { daemon_id: daemonId });
16100
- return raw.map((m) => ({
16101
- id: m.id,
16102
- meetingUrl: m.meeting_url,
16103
- participants: m.participants,
16104
- workspaceId: m.workspace_id
16105
- }));
16106
- }
16107
16118
  }
16108
16119
 
16109
16120
  // daemon/config.ts
@@ -16276,7 +16287,7 @@ function fromApiTask(api2) {
16276
16287
  type: api2.type,
16277
16288
  contextKey: api2.context_key ?? null,
16278
16289
  context: api2.context ?? undefined,
16279
- agent: api2.agent ? { name: api2.agent.name, instructions: api2.agent.instructions, emailHandle: api2.agent.email_handle ?? undefined, emailAddresses: api2.agent.email_addresses ?? [], userEmail: api2.agent.user_email ?? undefined, runtimeConfig: api2.agent.runtime_config ?? undefined } : undefined,
16290
+ agent: api2.agent ? { name: api2.agent.name, instructions: api2.agent.instructions, emailHandle: api2.agent.email_handle ?? undefined, emailAddresses: api2.agent.email_addresses ?? [], userEmail: api2.agent.user_email ?? undefined, userName: api2.agent.user_name ?? undefined, runtimeConfig: api2.agent.runtime_config ?? undefined } : undefined,
16280
16291
  sender: api2.sender ? { name: api2.sender.name, email: api2.sender.email, isOwner: api2.sender.is_owner } : undefined,
16281
16292
  repos: undefined,
16282
16293
  createdAt: api2.created_at
@@ -16583,8 +16594,7 @@ function recentFilenames(maxDays) {
16583
16594
  }
16584
16595
  return filenames;
16585
16596
  }
16586
- var DEFAULT_RESUME_MAX_AGE_MS = 3 * 60 * 60 * 1000;
16587
- var EMAIL_RESUME_MAX_AGE_MS = 48 * 60 * 60 * 1000;
16597
+ var RESUME_MAX_AGE_MS = 72 * 60 * 60 * 1000;
16588
16598
  function findRunningPidByTaskId(timelineDir, taskId) {
16589
16599
  for (const filename of recentFilenames(7)) {
16590
16600
  const entries = readJsonl(join4(timelineDir, filename));
@@ -16669,7 +16679,7 @@ function releaseSteeringLock(baseDir, contextKey) {
16669
16679
 
16670
16680
  // daemon/workspace-files.ts
16671
16681
  import { readdir, stat, readFile } from "fs/promises";
16672
- import { join as join6, resolve, extname, relative } from "path";
16682
+ import { join as join6, resolve, extname, relative, sep } from "path";
16673
16683
  var SKIP_DIRS = new Set([".git", "node_modules", ".next", ".wrangler", "__pycache__", ".venv"]);
16674
16684
  var TEXT_EXTENSIONS = new Set([
16675
16685
  ".md",
@@ -16755,24 +16765,61 @@ async function readFileContent(filePath) {
16755
16765
  }
16756
16766
  function validatePath(agentWorkdir, requestedPath) {
16757
16767
  const resolved = resolve(agentWorkdir, requestedPath);
16758
- if (resolved !== agentWorkdir && !resolved.startsWith(agentWorkdir + "/"))
16768
+ if (resolved !== agentWorkdir && !resolved.startsWith(agentWorkdir + sep))
16759
16769
  return null;
16760
16770
  return resolved;
16761
16771
  }
16762
16772
 
16773
+ // lib/shell-env.ts
16774
+ import { execSync as execSync3 } from "child_process";
16775
+
16776
+ // lib/platform.ts
16777
+ import { tmpdir } from "os";
16778
+ import { join as join7, sep as sep2 } from "path";
16779
+ var isWindows = process.platform === "win32";
16780
+ function tempDir(subdir) {
16781
+ return join7(tmpdir(), subdir);
16782
+ }
16783
+
16784
+ // lib/shell-env.ts
16785
+ function resolveLoginShellEnv() {
16786
+ if (isWindows) {
16787
+ return { ...process.env };
16788
+ }
16789
+ const shell = process.env.SHELL || "/bin/zsh";
16790
+ try {
16791
+ const output = execSync3(`${shell} -lc 'env'`, {
16792
+ encoding: "utf-8",
16793
+ timeout: 5000,
16794
+ stdio: ["ignore", "pipe", "ignore"]
16795
+ });
16796
+ const env = {};
16797
+ for (const line of output.split(`
16798
+ `)) {
16799
+ const idx = line.indexOf("=");
16800
+ if (idx > 0) {
16801
+ env[line.slice(0, idx)] = line.slice(idx + 1);
16802
+ }
16803
+ }
16804
+ if (env.PATH)
16805
+ return env;
16806
+ } catch {}
16807
+ return { ...process.env };
16808
+ }
16809
+
16763
16810
  // daemon/daemon.ts
16764
16811
  import { existsSync, mkdirSync as mkdirSync5, openSync, closeSync, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync4 } from "fs";
16765
16812
  import { readdir as readdir2, readFile as readFile2, unlink, stat as fsStat } from "fs/promises";
16766
- import { execSync as execSync3, spawn as spawn2 } from "child_process";
16813
+ import { execSync as execSync4, spawn as spawn2 } from "child_process";
16767
16814
  import { fileURLToPath as fileURLToPath2 } from "url";
16768
- import { dirname as dirname3, join as join7 } from "path";
16815
+ import { dirname as dirname3, join as join8 } from "path";
16769
16816
  var _dir = dirname3(fileURLToPath2(import.meta.url));
16770
- var sessionRunnerPath = existsSync(join7(_dir, "session-runner.js")) ? join7(_dir, "session-runner.js") : join7(_dir, "session-runner.ts");
16771
- var meetingRunnerPath = existsSync(join7(_dir, "meeting-runner.js")) ? join7(_dir, "meeting-runner.js") : join7(_dir, "meeting-runner.ts");
16817
+ var sessionRunnerPath = existsSync(join8(_dir, "session-runner.js")) ? join8(_dir, "session-runner.js") : join8(_dir, "session-runner.ts");
16818
+ var meetingRunnerPath = existsSync(join8(_dir, "meeting-runner.js")) ? join8(_dir, "meeting-runner.js") : join8(_dir, "meeting-runner.ts");
16772
16819
  function isCommandAvailable2(cmd) {
16773
16820
  try {
16774
16821
  const check2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
16775
- execSync3(check2, { stdio: "ignore" });
16822
+ execSync4(check2, { stdio: "ignore" });
16776
16823
  return true;
16777
16824
  } catch {
16778
16825
  return false;
@@ -16790,7 +16837,7 @@ function pruneSessionRunnerLogs() {
16790
16837
  if (entries.length <= MAX_SESSION_RUNNER_LOGS)
16791
16838
  return;
16792
16839
  const withMtime = entries.map((name) => {
16793
- const full = join7(logDir, name);
16840
+ const full = join8(logDir, name);
16794
16841
  try {
16795
16842
  return { name, mtime: statSync3(full).mtimeMs };
16796
16843
  } catch {
@@ -16800,7 +16847,7 @@ function pruneSessionRunnerLogs() {
16800
16847
  withMtime.sort((a, b) => b.mtime - a.mtime);
16801
16848
  for (const entry of withMtime.slice(MAX_SESSION_RUNNER_LOGS)) {
16802
16849
  try {
16803
- unlinkSync4(join7(logDir, entry.name));
16850
+ unlinkSync4(join8(logDir, entry.name));
16804
16851
  } catch {}
16805
16852
  }
16806
16853
  }
@@ -16841,7 +16888,7 @@ function isValidMarker(data) {
16841
16888
  var MARKER_STALE_MS = 24 * 60 * 60 * 1000;
16842
16889
  var TMP_STALE_MS = 60 * 60 * 1000;
16843
16890
  async function reconcilePendingCompletions(workspacesRoot) {
16844
- const dir = join7(workspacesRoot, ".pending_completions");
16891
+ const dir = join8(workspacesRoot, ".pending_completions");
16845
16892
  let entries;
16846
16893
  try {
16847
16894
  entries = await readdir2(dir);
@@ -16852,15 +16899,15 @@ async function reconcilePendingCompletions(workspacesRoot) {
16852
16899
  if (!name.endsWith(".tmp"))
16853
16900
  continue;
16854
16901
  try {
16855
- const s = await fsStat(join7(dir, name));
16902
+ const s = await fsStat(join8(dir, name));
16856
16903
  if (Date.now() - s.mtimeMs > TMP_STALE_MS) {
16857
- await unlink(join7(dir, name));
16904
+ await unlink(join8(dir, name));
16858
16905
  }
16859
16906
  } catch {}
16860
16907
  }
16861
16908
  const jsonFiles = entries.filter((f) => f.endsWith(".json"));
16862
16909
  for (const name of jsonFiles) {
16863
- const filePath = join7(dir, name);
16910
+ const filePath = join8(dir, name);
16864
16911
  try {
16865
16912
  let raw;
16866
16913
  try {
@@ -17070,7 +17117,7 @@ async function startDaemon(profile, serverUrl) {
17070
17117
  await new Promise((r) => setTimeout(r, staggerMs));
17071
17118
  }
17072
17119
  try {
17073
- const { tasks: apiTasks, evicted, pending_update, pending_rescan, file_requests } = await client.poll(ws.token, config2.daemonId, remaining, config2.cliVersion);
17120
+ const { tasks: apiTasks, evicted, pending_update, pending_rescan, file_requests, meetings } = await client.poll(ws.token, config2.daemonId, remaining, config2.cliVersion);
17074
17121
  if (evicted) {
17075
17122
  evictedIds.push(ws.workspaceId);
17076
17123
  continue;
@@ -17097,6 +17144,19 @@ async function startDaemon(profile, serverUrl) {
17097
17144
  handleFileRequest(client, config2, ws.workspaceId, req, ws.token).catch((e) => log.debug("File request error", e));
17098
17145
  }
17099
17146
  }
17147
+ if (meetings) {
17148
+ for (const m of meetings) {
17149
+ spawnMeetingRunner({
17150
+ meetingId: m.id,
17151
+ meetingUrl: m.meeting_url,
17152
+ participants: m.participants,
17153
+ workspaceId: m.workspace_id,
17154
+ callbackUrl: config2.serverURL,
17155
+ authToken: ws.token,
17156
+ agentName: m.agent_name
17157
+ });
17158
+ }
17159
+ }
17100
17160
  } catch (e) {
17101
17161
  if (e instanceof Error && e.message.startsWith("HTTP 401")) {
17102
17162
  log.warn(`Workspace ${ws.workspaceId} poll returned 401 — will retry next cycle`);
@@ -17108,23 +17168,6 @@ async function startDaemon(profile, serverUrl) {
17108
17168
  for (const id of evictedIds) {
17109
17169
  evictWorkspace(id);
17110
17170
  }
17111
- for (const ws of workspaceStates) {
17112
- try {
17113
- const meetings = await client.claimMeetings(ws.token, config2.daemonId);
17114
- for (const m of meetings) {
17115
- spawnMeetingRunner({
17116
- meetingId: m.id,
17117
- meetingUrl: m.meetingUrl,
17118
- participants: m.participants,
17119
- workspaceId: m.workspaceId,
17120
- callbackUrl: config2.serverURL,
17121
- authToken: ws.token
17122
- });
17123
- }
17124
- } catch (e) {
17125
- log.debug("Meeting claim error", e);
17126
- }
17127
- }
17128
17171
  try {
17129
17172
  await reconcilePendingCompletions(config2.workspacesRoot);
17130
17173
  } catch (e) {
@@ -17174,7 +17217,8 @@ async function startDaemon(profile, serverUrl) {
17174
17217
  }
17175
17218
  const child = spawn2(process.execPath, args, {
17176
17219
  detached: true,
17177
- stdio: logFd != null ? ["ignore", logFd, logFd] : ["ignore", "ignore", "ignore"]
17220
+ stdio: logFd != null ? ["ignore", logFd, logFd] : ["ignore", "ignore", "ignore"],
17221
+ env: resolveLoginShellEnv()
17178
17222
  });
17179
17223
  child.unref();
17180
17224
  if (logFd != null)
@@ -17192,7 +17236,7 @@ async function startDaemon(profile, serverUrl) {
17192
17236
  function spawnSessionRunner(input) {
17193
17237
  const logDir = sessionRunnerLogDir();
17194
17238
  mkdirSync5(logDir, { recursive: true });
17195
- const logFilePath = join7(logDir, `${input.task.id}.log`);
17239
+ const logFilePath = join8(logDir, `${input.task.id}.log`);
17196
17240
  input.logFilePath = logFilePath;
17197
17241
  const encoded = Buffer.from(JSON.stringify(input)).toString("base64");
17198
17242
  let fd;
@@ -17213,7 +17257,7 @@ function spawnSessionRunner(input) {
17213
17257
  function spawnMeetingRunner(input) {
17214
17258
  const logDir = sessionRunnerLogDir();
17215
17259
  mkdirSync5(logDir, { recursive: true });
17216
- const logFilePath = join7(logDir, `meeting-${input.meetingId}.log`);
17260
+ const logFilePath = join8(logDir, `meeting-${input.meetingId}.log`);
17217
17261
  const encoded = Buffer.from(JSON.stringify(input)).toString("base64");
17218
17262
  let fd;
17219
17263
  try {
@@ -17232,7 +17276,7 @@ function spawnMeetingRunner(input) {
17232
17276
  return child;
17233
17277
  }
17234
17278
  async function handleFileRequest(client, config2, workspaceId, req, token) {
17235
- const agentWorkdir = join7(config2.workspacesRoot, workspaceId, req.agent_id, "workdir");
17279
+ const agentWorkdir = join8(config2.workspacesRoot, workspaceId, req.agent_id, "workdir");
17236
17280
  const resolved = validatePath(agentWorkdir, req.path);
17237
17281
  if (!resolved) {
17238
17282
  await client.reportFileData(token, { request_id: req.id, error: "invalid path", path: req.path });
@@ -17263,9 +17307,18 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
17263
17307
  activeTasks.delete(task.id);
17264
17308
  return;
17265
17309
  }
17266
- const agentBaseDir = join7(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
17267
- const timelineDir = join7(agentBaseDir, ".context_timeline");
17268
- const pid = findRunningPidByTaskId(timelineDir, targetTaskId);
17310
+ const agentBaseDir = join8(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
17311
+ const timelineDir = join8(agentBaseDir, ".context_timeline");
17312
+ const MAX_WAIT_MS = Number(process.env.ALOOK_KILL_TASK_MAX_WAIT_MS) || 15000;
17313
+ const POLL_MS = Number(process.env.ALOOK_KILL_TASK_POLL_MS) || 200;
17314
+ const waitStart = Date.now();
17315
+ let pid = null;
17316
+ while (Date.now() - waitStart < MAX_WAIT_MS) {
17317
+ pid = findRunningPidByTaskId(timelineDir, targetTaskId);
17318
+ if (pid != null)
17319
+ break;
17320
+ await new Promise((r) => setTimeout(r, POLL_MS));
17321
+ }
17269
17322
  if (pid != null) {
17270
17323
  writeKillIntent(agentBaseDir, {
17271
17324
  reason: "cancelled",
@@ -17306,9 +17359,9 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
17306
17359
  }
17307
17360
  const provider = runtimeData.provider;
17308
17361
  if (task.contextKey) {
17309
- const agentBaseDir = join7(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
17362
+ const agentBaseDir = join8(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
17310
17363
  cleanupStaleIntents(agentBaseDir);
17311
- const timelineDir = join7(agentBaseDir, ".context_timeline");
17364
+ const timelineDir = join8(agentBaseDir, ".context_timeline");
17312
17365
  const lockAcquired = acquireSteeringLock(agentBaseDir, task.contextKey);
17313
17366
  if (!lockAcquired) {
17314
17367
  log.warn(`Steering lock contention for context_key=${task.contextKey}, proceeding without steering`);
@@ -17418,7 +17471,8 @@ async function startInBackground(profile, serverUrl) {
17418
17471
  const logFd = openSync2(logPath, "a", 384);
17419
17472
  const child = spawn3(process.execPath, buildChildArgs(profile, serverUrl), {
17420
17473
  detached: true,
17421
- stdio: ["ignore", logFd, logFd]
17474
+ stdio: ["ignore", logFd, logFd],
17475
+ env: resolveLoginShellEnv()
17422
17476
  });
17423
17477
  child.unref();
17424
17478
  closeSync2(logFd);
@@ -17471,10 +17525,12 @@ async function stopCommand(profile) {
17471
17525
  }
17472
17526
  await sleep(STOP_POLL_INTERVAL_MS);
17473
17527
  }
17474
- console.warn(`Daemon did not exit within ${shutdownMs}ms — sending SIGKILL.`);
17475
- try {
17476
- process.kill(pid, "SIGKILL");
17477
- } catch {}
17528
+ console.warn(`Daemon did not exit within ${shutdownMs}ms — force killing.`);
17529
+ if (!isWindows) {
17530
+ try {
17531
+ process.kill(pid, "SIGKILL");
17532
+ } catch {}
17533
+ }
17478
17534
  removePidFileIfMatches(pid, profile);
17479
17535
  console.log("Daemon stopped.");
17480
17536
  }
@@ -17533,10 +17589,11 @@ function configCommand() {
17533
17589
  // commands/email.ts
17534
17590
  import { Command as Command5 } from "commander";
17535
17591
  import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
17536
- import { basename, join as join8 } from "path";
17592
+ import { basename, join as join9 } from "path";
17537
17593
  import PostalMime from "postal-mime";
17538
- var VALID_STATUSES = ["unread", "read", "archived"];
17539
- var EMAIL_BASE = "/tmp/alook-emails";
17594
+ var VALID_STATUSES = ["unread", "read", "archived", "sent"];
17595
+ var VALID_FOLDERS = ["inbox", "sent", "untrust"];
17596
+ var EMAIL_BASE = tempDir("alook-emails");
17540
17597
  var MIME_BY_EXT = {
17541
17598
  ".pdf": "application/pdf",
17542
17599
  ".png": "image/png",
@@ -17604,18 +17661,42 @@ function resolveClientOpts(command, opts) {
17604
17661
  }
17605
17662
  function emailCommand() {
17606
17663
  const cmd = new Command5("email").description("Manage agent emails");
17607
- cmd.command("pull").description("Download and parse emails to /tmp/alook-emails/{workspaceId}/{agentId}/").requiredOption("--agent_id <id>", "Agent ID").option("--status <status>", "Filter by status (unread, read, archived)").option("--workspace <id>", "Workspace ID").option("--json", "Output as JSON instead of files").action(async (opts, command) => {
17664
+ cmd.command("pull").description("Download and parse emails to /tmp/alook-emails/{workspaceId}/{agentId}/").requiredOption("--agent_id <id>", "Agent ID").option("--status <status>", "Filter by status (unread, read, archived)").option("--folder <folder>", "Email folder (inbox, sent, untrust)").option("--limit <n>", "Maximum number of emails to download").option("--offset <n>", "Number of emails to skip").option("--workspace <id>", "Workspace ID").option("--json", "Output as JSON instead of files").action(async (opts, command) => {
17608
17665
  const { serverUrl, token, workspaceId } = resolveClientOpts(command, { workspace: opts.workspace, agentId: opts.agent_id });
17609
17666
  const client = new APIClient(serverUrl, token, workspaceId);
17610
17667
  if (opts.status && !VALID_STATUSES.includes(opts.status)) {
17611
17668
  console.error(`Error: invalid status "${opts.status}", must be one of: ${VALID_STATUSES.join(", ")}`);
17612
17669
  process.exit(1);
17613
17670
  }
17614
- const emailDir_base = join8(EMAIL_BASE, workspaceId, opts.agent_id);
17671
+ if (opts.folder && !VALID_FOLDERS.includes(opts.folder)) {
17672
+ console.error(`Error: invalid folder "${opts.folder}", must be one of: ${VALID_FOLDERS.join(", ")}`);
17673
+ process.exit(1);
17674
+ }
17675
+ if (opts.limit != null) {
17676
+ const n = parseInt(opts.limit, 10);
17677
+ if (isNaN(n) || n < 1 || n > 100) {
17678
+ console.error(`Error: --limit must be an integer between 1 and 100`);
17679
+ process.exit(1);
17680
+ }
17681
+ }
17682
+ if (opts.offset != null) {
17683
+ const n = parseInt(opts.offset, 10);
17684
+ if (isNaN(n) || n < 0) {
17685
+ console.error(`Error: --offset must be a non-negative integer`);
17686
+ process.exit(1);
17687
+ }
17688
+ }
17689
+ const emailDir_base = join9(EMAIL_BASE, workspaceId, opts.agent_id);
17615
17690
  try {
17616
17691
  let query = `/api/email?agentId=${opts.agent_id}`;
17617
17692
  if (opts.status)
17618
17693
  query += `&status=${opts.status}`;
17694
+ if (opts.folder)
17695
+ query += `&folder=${opts.folder}`;
17696
+ if (opts.limit)
17697
+ query += `&limit=${opts.limit}`;
17698
+ if (opts.offset)
17699
+ query += `&offset=${opts.offset}`;
17619
17700
  const emails2 = await client.getJSON(query);
17620
17701
  if (!emails2.length) {
17621
17702
  console.log("No emails found.");
@@ -17628,7 +17709,7 @@ function emailCommand() {
17628
17709
  mkdirSync7(emailDir_base, { recursive: true });
17629
17710
  const downloadedPaths = [];
17630
17711
  for (const email3 of emails2) {
17631
- const emailDir = join8(emailDir_base, email3.id);
17712
+ const emailDir = join9(emailDir_base, email3.id);
17632
17713
  mkdirSync7(emailDir, { recursive: true });
17633
17714
  const metadata = {
17634
17715
  id: email3.id,
@@ -17641,7 +17722,7 @@ function emailCommand() {
17641
17722
  in_reply_to: email3.in_reply_to || "",
17642
17723
  references: email3.references || ""
17643
17724
  };
17644
- const metadataPath = join8(emailDir, "metadata.json");
17725
+ const metadataPath = join9(emailDir, "metadata.json");
17645
17726
  writeFileSync6(metadataPath, JSON.stringify(metadata, null, 2));
17646
17727
  downloadedPaths.push(metadataPath);
17647
17728
  let rawMime;
@@ -17657,17 +17738,17 @@ function emailCommand() {
17657
17738
  }
17658
17739
  const parsed = await new PostalMime().parse(rawMime);
17659
17740
  if (parsed.text) {
17660
- const bodyPath = join8(emailDir, "body.txt");
17741
+ const bodyPath = join9(emailDir, "body.txt");
17661
17742
  writeFileSync6(bodyPath, parsed.text);
17662
17743
  downloadedPaths.push(bodyPath);
17663
17744
  }
17664
17745
  if (parsed.html) {
17665
- const htmlPath = join8(emailDir, "body.html");
17746
+ const htmlPath = join9(emailDir, "body.html");
17666
17747
  writeFileSync6(htmlPath, parsed.html);
17667
17748
  downloadedPaths.push(htmlPath);
17668
17749
  }
17669
17750
  if (parsed.attachments && parsed.attachments.length > 0) {
17670
- const attDir = join8(emailDir, "attachments");
17751
+ const attDir = join9(emailDir, "attachments");
17671
17752
  mkdirSync7(attDir, { recursive: true });
17672
17753
  const usedFilenames = new Set;
17673
17754
  for (let i = 0;i < parsed.attachments.length; i++) {
@@ -17677,7 +17758,7 @@ function emailCommand() {
17677
17758
  filename = `${i}-${filename}`;
17678
17759
  }
17679
17760
  usedFilenames.add(filename);
17680
- const attPath = join8(attDir, filename);
17761
+ const attPath = join9(attDir, filename);
17681
17762
  const content = att.content;
17682
17763
  let buf;
17683
17764
  if (typeof content === "string") {
@@ -17773,6 +17854,7 @@ function emailCommand() {
17773
17854
  console.warn(`Warning: could not fetch parent email ${opts.inReplyTo}, sending without threading`);
17774
17855
  }
17775
17856
  }
17857
+ const conversationId = process.env.ALOOK_CONVERSATION_ID;
17776
17858
  const res = await client.postJSON("/api/email/send", {
17777
17859
  agentId: opts.agent_id,
17778
17860
  to: opts.to,
@@ -17780,7 +17862,8 @@ function emailCommand() {
17780
17862
  htmlBody,
17781
17863
  attachments,
17782
17864
  ...inReplyTo ? { inReplyTo, references } : {},
17783
- ...opts.from ? { from: opts.from } : {}
17865
+ ...opts.from ? { from: opts.from } : {},
17866
+ ...conversationId ? { conversationId } : {}
17784
17867
  });
17785
17868
  console.log(`Sent email to ${res.to_email} (id: ${res.id})`);
17786
17869
  } catch (err) {
@@ -17788,6 +17871,118 @@ function emailCommand() {
17788
17871
  process.exit(1);
17789
17872
  }
17790
17873
  });
17874
+ cmd.command("forward").description("Forward an email to a new recipient").requiredOption("--agent_id <id>", "Agent ID").requiredOption("--email_id <id>", "Source email ID to forward").requiredOption("--to <addr>", "Recipient email address").option("--from <addr>", "Send from a specific email address (custom mailbox)").option("--note <text>", "Text to prepend above the forwarded message").option("--attachment <path>", "Extra file to attach (repeatable)", collectRepeated, []).option("--workspace <id>", "Workspace ID").action(async (opts, command) => {
17875
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, {
17876
+ workspace: opts.workspace,
17877
+ agentId: opts.agent_id
17878
+ });
17879
+ const client = new APIClient(serverUrl, token, workspaceId);
17880
+ try {
17881
+ let original;
17882
+ try {
17883
+ original = await client.getJSON(`/api/email/${opts.email_id}`);
17884
+ } catch (err) {
17885
+ const msg = err instanceof Error ? err.message : String(err);
17886
+ if (msg.includes("404")) {
17887
+ console.error(`Error: email ${opts.email_id} not found`);
17888
+ process.exit(1);
17889
+ }
17890
+ throw err;
17891
+ }
17892
+ let rawMime;
17893
+ try {
17894
+ rawMime = await client.getText(`/api/email/${opts.email_id}/raw`);
17895
+ } catch (err) {
17896
+ const msg = err instanceof Error ? err.message : String(err);
17897
+ if (msg.includes("404")) {
17898
+ console.error(`Error: raw email body not available for ${opts.email_id}`);
17899
+ process.exit(1);
17900
+ }
17901
+ throw err;
17902
+ }
17903
+ const parsed = await new PostalMime().parse(rawMime);
17904
+ const attachments = [];
17905
+ if (parsed.attachments && parsed.attachments.length > 0) {
17906
+ for (const att of parsed.attachments) {
17907
+ const filename = att.filename || "attachment.bin";
17908
+ const contentType = att.mimeType || "application/octet-stream";
17909
+ const content = att.content;
17910
+ let buf;
17911
+ if (typeof content === "string") {
17912
+ buf = Buffer.from(content, "base64");
17913
+ } else if (content instanceof ArrayBuffer) {
17914
+ buf = Buffer.from(new Uint8Array(content));
17915
+ } else {
17916
+ buf = Buffer.from(content);
17917
+ }
17918
+ const form = new FormData;
17919
+ form.append("file", new Blob([new Uint8Array(buf)], { type: contentType }), filename);
17920
+ const uploaded = await client.postMultipart("/api/email/upload", form);
17921
+ attachments.push({
17922
+ key: uploaded.key,
17923
+ filename: uploaded.filename,
17924
+ size: uploaded.size ?? buf.byteLength,
17925
+ contentType: uploaded.contentType ?? contentType
17926
+ });
17927
+ }
17928
+ }
17929
+ const extraPaths = opts.attachment ?? [];
17930
+ for (const path of extraPaths) {
17931
+ let bytes;
17932
+ let size;
17933
+ try {
17934
+ bytes = readFileSync7(path);
17935
+ size = statSync4(path).size;
17936
+ } catch (err) {
17937
+ console.error(`Error: cannot read attachment "${path}": ${err instanceof Error ? err.message : err}`);
17938
+ process.exit(1);
17939
+ }
17940
+ const filename = basename(path);
17941
+ const contentType = guessContentType(filename);
17942
+ const form = new FormData;
17943
+ form.append("file", new Blob([new Uint8Array(bytes)], { type: contentType }), filename);
17944
+ const uploaded = await client.postMultipart("/api/email/upload", form);
17945
+ attachments.push({
17946
+ key: uploaded.key,
17947
+ filename: uploaded.filename,
17948
+ size: uploaded.size ?? size,
17949
+ contentType: uploaded.contentType ?? contentType
17950
+ });
17951
+ }
17952
+ let htmlBody = "";
17953
+ if (opts.note) {
17954
+ htmlBody += `<p>${opts.note}</p>`;
17955
+ }
17956
+ htmlBody += `<br><br>---------- Forwarded message ----------<br>`;
17957
+ htmlBody += `From: ${original.from_email}<br>`;
17958
+ htmlBody += `Date: ${original.created_at}<br>`;
17959
+ htmlBody += `Subject: ${original.subject}<br>`;
17960
+ htmlBody += `To: ${original.to_email}<br><br>`;
17961
+ if (parsed.html) {
17962
+ htmlBody += parsed.html;
17963
+ } else if (parsed.text) {
17964
+ htmlBody += `<pre>${parsed.text}</pre>`;
17965
+ }
17966
+ const subject = /^fwd:/i.test(original.subject) ? original.subject : `Fwd: ${original.subject}`;
17967
+ const conversationId = process.env.ALOOK_CONVERSATION_ID;
17968
+ const res = await client.postJSON("/api/email/send", {
17969
+ agentId: opts.agent_id,
17970
+ to: opts.to,
17971
+ subject,
17972
+ htmlBody,
17973
+ attachments,
17974
+ ...opts.from ? { from: opts.from } : {},
17975
+ ...conversationId ? { conversationId } : {}
17976
+ });
17977
+ console.log(`Forwarded email to ${res.to_email} (id: ${res.id})`);
17978
+ } catch (err) {
17979
+ const msg = err instanceof Error ? err.message : String(err);
17980
+ if (msg === "__exit__")
17981
+ throw err;
17982
+ console.error(`Error: ${msg}`);
17983
+ process.exit(1);
17984
+ }
17985
+ });
17791
17986
  const whitelistCmd = new Command5("whitelist").description("Manage email whitelist (allowed senders)");
17792
17987
  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) => {
17793
17988
  const { serverUrl, token, workspaceId } = resolveClientOpts(command, {
@@ -390,9 +390,20 @@ function ensureChrome() {
390
390
  throw new Error("Failed to install Chromium via Playwright");
391
391
  return installed;
392
392
  }
393
+ // daemon/meeting-runner.ts
394
+ import { join as join2 } from "path";
395
+
396
+ // lib/platform.ts
397
+ import { tmpdir } from "os";
398
+ import { join, sep } from "path";
399
+ var isWindows = process.platform === "win32";
400
+ function tempDir(subdir) {
401
+ return join(tmpdir(), subdir);
402
+ }
403
+
393
404
  // daemon/meeting-runner.ts
394
405
  var SCRAPE_INTERVAL_MS = 3000;
395
- var BOT_NAME = "Alook Meeting Bot";
406
+ var DEFAULT_BOT_NAME = "Alook Meeting Bot";
396
407
  var MAX_RETRY_DURATION_MS = 30 * 60 * 1000;
397
408
  var RETRY_BACKOFF = [30000, 60000, 120000, 300000];
398
409
  function log(msg) {
@@ -445,7 +456,8 @@ async function tryJoinAndRecord(input, chromePath) {
445
456
  const meetingStartMs = Date.now();
446
457
  let transcript = [];
447
458
  try {
448
- await joinMeeting(page, input.meetingUrl, BOT_NAME);
459
+ const botName = input.agentName ? `${input.agentName} (Alook)` : DEFAULT_BOT_NAME;
460
+ await joinMeeting(page, input.meetingUrl, botName);
449
461
  log("Joined. Waiting for meeting UI...");
450
462
  await waitForMeetingReady(page);
451
463
  await page.evaluate(() => {
@@ -498,7 +510,7 @@ async function tryJoinAndRecord(input, chromePath) {
498
510
  } catch (err) {
499
511
  const msg = err instanceof Error ? err.message : String(err);
500
512
  if (msg.includes("Blocked from joining")) {
501
- const screenshotPath = `/tmp/meeting-${input.meetingId}-blocked.png`;
513
+ const screenshotPath = join2(tempDir("alook-meetings"), `meeting-${input.meetingId}-blocked.png`);
502
514
  await page.screenshot({ path: screenshotPath }).catch(() => {});
503
515
  log(`Blocked — screenshot: ${screenshotPath}`);
504
516
  return { status: "blocked", transcript, error: msg };
@@ -13625,7 +13625,8 @@ var TaskAgentDataApiSchema = exports_external.object({
13625
13625
  runtime_config: exports_external.record(exports_external.string(), exports_external.unknown()).default({}),
13626
13626
  email_handle: exports_external.string().nullable().optional(),
13627
13627
  email_addresses: exports_external.array(exports_external.string()).default([]),
13628
- user_email: exports_external.string().nullable().optional()
13628
+ user_email: exports_external.string().nullable().optional(),
13629
+ user_name: exports_external.string().nullable().optional()
13629
13630
  });
13630
13631
  var TaskApiBaseSchema = exports_external.object({
13631
13632
  id: exports_external.string(),
@@ -13666,12 +13667,20 @@ var FileRequestItemSchema = exports_external.object({
13666
13667
  request_type: exports_external.enum(["tree", "read"]),
13667
13668
  path: exports_external.string()
13668
13669
  });
13670
+ var PollMeetingItemSchema = exports_external.object({
13671
+ id: exports_external.string(),
13672
+ meeting_url: exports_external.string(),
13673
+ participants: exports_external.array(exports_external.string()),
13674
+ workspace_id: exports_external.string(),
13675
+ agent_name: exports_external.string()
13676
+ });
13669
13677
  var PollResponseSchema = exports_external.object({
13670
13678
  tasks: exports_external.array(TaskApiSchema),
13671
13679
  evicted: exports_external.boolean().optional(),
13672
13680
  pending_update: exports_external.object({ version: exports_external.string() }).optional(),
13673
13681
  pending_rescan: exports_external.boolean().optional(),
13674
- file_requests: exports_external.array(FileRequestItemSchema).optional()
13682
+ file_requests: exports_external.array(FileRequestItemSchema).optional(),
13683
+ meetings: exports_external.array(PollMeetingItemSchema).optional()
13675
13684
  });
13676
13685
  var RegisterResponseSchema = exports_external.object({
13677
13686
  runtimes: exports_external.array(exports_external.object({ id: exports_external.string() }))
@@ -13819,10 +13828,11 @@ var SendEmailRequestSchema = exports_external.object({
13819
13828
  references: exports_external.string().optional(),
13820
13829
  attachments: exports_external.array(EmailAttachmentSchema).optional(),
13821
13830
  customAccountId: exports_external.string().optional(),
13822
- from: exports_external.string().email().optional()
13831
+ from: exports_external.string().email().optional(),
13832
+ conversationId: exports_external.string().optional()
13823
13833
  });
13824
13834
  var UpdateEmailStatusRequestSchema = exports_external.object({
13825
- status: exports_external.enum(["unread", "read", "archived"])
13835
+ status: exports_external.enum(["unread", "read", "archived", "sent"])
13826
13836
  });
13827
13837
  var MeetingInfoSchema = exports_external.object({
13828
13838
  title: exports_external.string(),
@@ -15659,6 +15669,15 @@ var machineToken = sqliteTable("machine_token", {
15659
15669
  lastUsedAt: text("last_used_at"),
15660
15670
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15661
15671
  }, (t) => [index("idx_machine_token").on(t.token)]);
15672
+ var conversationMap = sqliteTable("conversation_map", {
15673
+ id: text("id").primaryKey().$defaultFn(() => nanoid3()),
15674
+ key: text("key").notNull(),
15675
+ workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
15676
+ conversationId: text("conversation_id").notNull().references(() => conversation.id, { onDelete: "cascade" }),
15677
+ createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15678
+ }, (t) => [
15679
+ unique("conversation_map_key_workspace").on(t.key, t.workspaceId)
15680
+ ]);
15662
15681
  var workspaceFileRequest = sqliteTable("workspace_file_request", {
15663
15682
  id: text("id").primaryKey().$defaultFn(() => "wfr_" + nanoid3()),
15664
15683
  workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
@@ -15751,7 +15770,8 @@ class DaemonClient {
15751
15770
  evicted: resp.evicted ?? false,
15752
15771
  pending_update: resp.pending_update,
15753
15772
  pending_rescan: resp.pending_rescan,
15754
- file_requests: resp.file_requests
15773
+ file_requests: resp.file_requests,
15774
+ meetings: resp.meetings
15755
15775
  };
15756
15776
  }
15757
15777
  startTask(token, taskId) {
@@ -15801,15 +15821,6 @@ class DaemonClient {
15801
15821
  reportFileData(token, body) {
15802
15822
  return this.request("POST", "/api/daemon/workspace/report", token, body);
15803
15823
  }
15804
- async claimMeetings(token, daemonId) {
15805
- const raw = await this.request("POST", "/api/daemon/meetings/claim", token, { daemon_id: daemonId });
15806
- return raw.map((m) => ({
15807
- id: m.id,
15808
- meetingUrl: m.meeting_url,
15809
- participants: m.participants,
15810
- workspaceId: m.workspace_id
15811
- }));
15812
- }
15813
15824
  }
15814
15825
 
15815
15826
  // daemon/agent/claude.ts
@@ -16735,11 +16746,21 @@ function createBackend(provider, cliPath) {
16735
16746
  }
16736
16747
 
16737
16748
  // daemon/execenv/index.ts
16738
- import { mkdirSync as mkdirSync2 } from "fs";
16749
+ import { mkdirSync } from "fs";
16739
16750
  import { join as join3 } from "path";
16740
16751
 
16741
16752
  // daemon/execenv/context.ts
16742
16753
  import { createHash } from "crypto";
16754
+
16755
+ // lib/platform.ts
16756
+ import { tmpdir } from "os";
16757
+ import { join, sep } from "path";
16758
+ var isWindows = process.platform === "win32";
16759
+ function tempDir(subdir) {
16760
+ return join(tmpdir(), subdir);
16761
+ }
16762
+
16763
+ // daemon/execenv/context.ts
16743
16764
  import {
16744
16765
  writeFileSync,
16745
16766
  readFileSync,
@@ -16749,7 +16770,7 @@ import {
16749
16770
  existsSync,
16750
16771
  readlinkSync
16751
16772
  } from "fs";
16752
- import { join } from "path";
16773
+ import { join as join2 } from "path";
16753
16774
  var CANONICAL_FILE = "AGENTS.md";
16754
16775
  var SYMLINK_ALIASES = ["CLAUDE.md"];
16755
16776
  var SYSTEM_PROMPT_BODY = `## Memory Management
@@ -16805,7 +16826,15 @@ those json are sorted by datetime in asc order.
16805
16826
  `;
16806
16827
  function buildInstructionContent(task) {
16807
16828
  const displayName = task.agent?.name || "Alook Agent";
16808
- let content = `You're ${displayName} in the Alook Platform.
16829
+ const alookAddr = task.agent?.emailHandle ? toAlookAddress(task.agent.emailHandle) : null;
16830
+ const customAddrs = (task.agent?.emailAddresses ?? []).filter((a) => a !== alookAddr);
16831
+ const primaryEmail = alookAddr ?? customAddrs[0] ?? null;
16832
+ let agentLine = `You're ${displayName}${primaryEmail ? ` (${primaryEmail})` : ""} in the Alook Platform.`;
16833
+ if (task.agent?.userName || task.agent?.userEmail) {
16834
+ const ownerParts = [task.agent.userName, task.agent.userEmail ? `(${task.agent.userEmail})` : null].filter(Boolean).join(" ");
16835
+ agentLine += ` Your owner and creator is ${ownerParts}.`;
16836
+ }
16837
+ let content = `${agentLine}
16809
16838
  ${SYSTEM_PROMPT_BODY}`;
16810
16839
  if (task.agent?.instructions) {
16811
16840
  content += `## BIG BOSS Instructions
@@ -16819,29 +16848,34 @@ ${task.agent.instructions}
16819
16848
  You can communicate with the world through Alook CLI.
16820
16849
  Your alook agent id is '${task.agentId}'. remember this, most of alook cli will requires you input your agent id.
16821
16850
  `;
16822
- const alookAddr = task.agent?.emailHandle ? toAlookAddress(task.agent.emailHandle) : null;
16823
- const customAddrs = (task.agent?.emailAddresses ?? []).filter((a) => a !== alookAddr);
16824
16851
  if (alookAddr || customAddrs.length > 0) {
16825
16852
  const lines = [];
16826
16853
  if (alookAddr)
16827
16854
  lines.push(`- '${alookAddr}' (default, Alook platform address)`);
16828
16855
  for (const a of customAddrs)
16829
16856
  lines.push(`- '${a}' (custom IMAP/SMTP mailbox)`);
16830
- content += `Your email addresses:
16857
+ content += `
16858
+ Your email addresses:
16831
16859
  ${lines.join(`
16832
16860
  `)}
16833
- ${task.agent?.userEmail ? `Your owner's email address is '${task.agent.userEmail}'.` : ""}
16861
+
16834
16862
 
16835
16863
  ### Emails
16836
16864
  ---
16837
- Run 'npx @alook/cli email pull --agent_id ${task.agentId} --status unread' to download unread emails to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/'.
16838
- Each email is saved to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/<emailId>/' with:
16865
+ Run 'npx @alook/cli email pull --agent_id ${task.agentId} --status unread' to download unread emails from inbox to '${tempDir("alook-emails")}/${task.workspaceId}/${task.agentId}/'.
16866
+ ---
16867
+ To download sent emails, add '--folder sent': 'npx @alook/cli email pull --agent_id ${task.agentId} --folder sent'
16868
+ Valid folders: inbox (default), sent, untrust.
16869
+ To limit the number of emails downloaded, add '--limit <N>' (e.g. '--limit 20'). Use '--offset <N>' to skip emails for pagination.
16870
+ Example: 'npx @alook/cli email pull --agent_id ${task.agentId} --status unread --limit 20 --offset 0'
16871
+ ---
16872
+ Each email is saved to '${tempDir("alook-emails")}/${task.workspaceId}/${task.agentId}/<emailId>/' with:
16839
16873
  - 'metadata.json' — sender, recipient, subject, date, status, message_id, in_reply_to, references
16840
16874
  - 'body.txt' — plain text body
16841
16875
  - 'body.html' — HTML body (if available)
16842
16876
  - 'attachments/' — extracted attachment files (if any)
16843
16877
  ---
16844
- Before starting to process an email, mark it as read:
16878
+ Before starting to process an INBOX email, mark it as read:
16845
16879
  - Run 'npx @alook/cli email set --agent_id ${task.agentId} --email_id <EMAIL_ID> --status read'
16846
16880
  ---
16847
16881
 
@@ -16862,6 +16896,15 @@ Tips:
16862
16896
  - 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.
16863
16897
  ---
16864
16898
 
16899
+ #### Forwarding an email
16900
+ Forward any email to a new recipient, with an optional note prepended above the original content. All original attachments are re-attached automatically.
16901
+ - Run 'npx @alook/cli email forward --agent_id ${task.agentId} --email_id <EMAIL_ID> --to <RECIPIENT>'
16902
+ - Add '--note "FYI, see the request below."' to prepend a note above the forwarded body.
16903
+ - Add '--from <YOUR_EMAIL_ADDRESS>' to send from a specific mailbox.
16904
+ - Add '--attachment <PATH>' to attach extra files (repeatable).
16905
+ - Example: 'npx @alook/cli email forward --agent_id ${task.agentId} --email_id em_abc --to boss@company.com --note "FYI" --attachment /tmp/summary.pdf'
16906
+ ---
16907
+
16865
16908
  #### Email Whitelist (Allowed Senders)
16866
16909
  Manage which email addresses are allowed to send you emails.
16867
16910
  - List: 'npx @alook/cli email whitelist list --agent_id ${task.agentId}' (add '--json' for machine-readable output)
@@ -16944,13 +16987,13 @@ function hasContentChanged(filePath, newContent) {
16944
16987
  }
16945
16988
  }
16946
16989
  function ensureSymlinks(workDir) {
16947
- const canonicalPath = join(workDir, CANONICAL_FILE);
16990
+ const canonicalPath = join2(workDir, CANONICAL_FILE);
16948
16991
  if (!existsSync(canonicalPath))
16949
16992
  return;
16950
16993
  for (const alias of SYMLINK_ALIASES) {
16951
16994
  if (alias === CANONICAL_FILE)
16952
16995
  continue;
16953
- const aliasPath = join(workDir, alias);
16996
+ const aliasPath = join2(workDir, alias);
16954
16997
  try {
16955
16998
  const stat = lstatSync(aliasPath);
16956
16999
  if (stat.isSymbolicLink()) {
@@ -16970,7 +17013,7 @@ function ensureSymlinks(workDir) {
16970
17013
  }
16971
17014
  function writeInstructionFileIfChanged(workDir, task) {
16972
17015
  const content = buildInstructionContent(task);
16973
- const filePath = join(workDir, CANONICAL_FILE);
17016
+ const filePath = join2(workDir, CANONICAL_FILE);
16974
17017
  const changed = hasContentChanged(filePath, content);
16975
17018
  if (changed) {
16976
17019
  writeFileSync(filePath, content, "utf-8");
@@ -16978,16 +17021,34 @@ function writeInstructionFileIfChanged(workDir, task) {
16978
17021
  ensureSymlinks(workDir);
16979
17022
  return changed;
16980
17023
  }
17024
+
17025
+ // daemon/execenv/index.ts
17026
+ function prepare(config2, task) {
17027
+ const workDir = join3(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
17028
+ mkdirSync(workDir, { recursive: true });
17029
+ const timelineDir = join3(workDir, ".context_timeline");
17030
+ mkdirSync(timelineDir, { recursive: true });
17031
+ writeInstructionFileIfChanged(workDir, task);
17032
+ const env = {
17033
+ ALOOK_WORKSPACE_ID: task.workspaceId,
17034
+ ALOOK_AGENT_ID: task.agentId,
17035
+ ALOOK_TASK_ID: task.id,
17036
+ ALOOK_CONVERSATION_ID: task.conversationId,
17037
+ ALOOK_HEALTH_PORT: process.env.ALOOK_HEALTH_PORT || "19514"
17038
+ };
17039
+ return { workDir, timelineDir, env };
17040
+ }
17041
+
16981
17042
  // daemon/execenv/timeline.ts
16982
17043
  import { appendFileSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync } from "fs";
16983
- import { join as join2 } from "path";
17044
+ import { join as join4 } from "path";
16984
17045
 
16985
17046
  // daemon/execenv/filelock.ts
16986
- import { mkdirSync, rmdirSync, statSync } from "fs";
17047
+ import { mkdirSync as mkdirSync2, rmdirSync, statSync } from "fs";
16987
17048
  var DEFAULT_STALE_MS = 3600000;
16988
17049
  function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
16989
17050
  try {
16990
- mkdirSync(lockPath);
17051
+ mkdirSync2(lockPath);
16991
17052
  return true;
16992
17053
  } catch {
16993
17054
  try {
@@ -16995,7 +17056,7 @@ function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
16995
17056
  if (Date.now() - stat.mtimeMs > staleMs) {
16996
17057
  rmdirSync(lockPath);
16997
17058
  try {
16998
- mkdirSync(lockPath);
17059
+ mkdirSync2(lockPath);
16999
17060
  return true;
17000
17061
  } catch {
17001
17062
  return false;
@@ -17003,7 +17064,7 @@ function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
17003
17064
  }
17004
17065
  } catch {
17005
17066
  try {
17006
- mkdirSync(lockPath);
17067
+ mkdirSync2(lockPath);
17007
17068
  return true;
17008
17069
  } catch {
17009
17070
  return false;
@@ -17166,14 +17227,14 @@ function localISOString() {
17166
17227
  return `${y}-${mo}-${d}T${h}:${mi}:${s}${sign}${hh}:${mm}`;
17167
17228
  }
17168
17229
  function lockPathFor(timelineDir, filename) {
17169
- return join2(timelineDir, `.${filename}.lock`);
17230
+ return join4(timelineDir, `.${filename}.lock`);
17170
17231
  }
17171
17232
  function sleep(ms) {
17172
17233
  return new Promise((resolve) => setTimeout(resolve, ms));
17173
17234
  }
17174
17235
  async function initEntryAsync(timelineDir, entry) {
17175
17236
  const filename = todayFilename();
17176
- const filePath = join2(timelineDir, filename);
17237
+ const filePath = join4(timelineDir, filename);
17177
17238
  const lockPath = lockPathFor(timelineDir, filename);
17178
17239
  try {
17179
17240
  let acquired = acquireLock(lockPath);
@@ -17197,7 +17258,7 @@ async function initEntryAsync(timelineDir, entry) {
17197
17258
  }
17198
17259
  function updateEntry(timelineDir, taskId, updater) {
17199
17260
  for (const filename of recentFilenames(7)) {
17200
- const filePath = join2(timelineDir, filename);
17261
+ const filePath = join4(timelineDir, filename);
17201
17262
  const lockPath = lockPathFor(timelineDir, filename);
17202
17263
  try {
17203
17264
  const acquired = acquireLock(lockPath);
@@ -17225,7 +17286,7 @@ function updateEntry(timelineDir, taskId, updater) {
17225
17286
  });
17226
17287
  if (!found)
17227
17288
  continue;
17228
- const tmpPath = join2(timelineDir, `.${filename}.tmp`);
17289
+ const tmpPath = join4(timelineDir, `.${filename}.tmp`);
17229
17290
  writeFileSync2(tmpPath, updated.join(`
17230
17291
  `) + `
17231
17292
  `);
@@ -17256,16 +17317,13 @@ function createTimelineEntry(taskId, prompt, type, sessionId, pid, provider, con
17256
17317
  detailed_log: detailedLog ?? null
17257
17318
  };
17258
17319
  }
17259
- var DEFAULT_RESUME_MAX_AGE_MS = 3 * 60 * 60 * 1000;
17260
- var EMAIL_RESUME_MAX_AGE_MS = 48 * 60 * 60 * 1000;
17320
+ var RESUME_MAX_AGE_MS = 72 * 60 * 60 * 1000;
17261
17321
  function findResumableSessionByContextKey(timelineDir, contextKey, provider) {
17262
- const maxAgeMs = contextKey.startsWith("email:") ? EMAIL_RESUME_MAX_AGE_MS : DEFAULT_RESUME_MAX_AGE_MS;
17263
17322
  const now = new Date;
17264
- const cutoff = new Date(now.getTime() - maxAgeMs);
17265
- const daysToScan = Math.ceil(maxAgeMs / 86400000) + 1;
17323
+ const cutoff = new Date(now.getTime() - RESUME_MAX_AGE_MS);
17266
17324
  const entries = [];
17267
- for (const filename of recentFilenames(daysToScan)) {
17268
- entries.push(...readJsonl(join2(timelineDir, filename)));
17325
+ for (const filename of recentFilenames(7)) {
17326
+ entries.push(...readJsonl(join4(timelineDir, filename)));
17269
17327
  }
17270
17328
  entries.sort((a, b) => new Date(b.datetime).getTime() - new Date(a.datetime).getTime());
17271
17329
  for (const entry of entries) {
@@ -17276,30 +17334,13 @@ function findResumableSessionByContextKey(timelineDir, contextKey, provider) {
17276
17334
  return null;
17277
17335
  }
17278
17336
 
17279
- // daemon/execenv/index.ts
17280
- function prepare(config2, task) {
17281
- const workDir = join3(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
17282
- mkdirSync2(workDir, { recursive: true });
17283
- const timelineDir = join3(workDir, ".context_timeline");
17284
- mkdirSync2(timelineDir, { recursive: true });
17285
- writeInstructionFileIfChanged(workDir, task);
17286
- const env = {
17287
- ALOOK_WORKSPACE_ID: task.workspaceId,
17288
- ALOOK_AGENT_ID: task.agentId,
17289
- ALOOK_TASK_ID: task.id,
17290
- ALOOK_CONVERSATION_ID: task.conversationId,
17291
- ALOOK_HEALTH_PORT: process.env.ALOOK_HEALTH_PORT || "19514"
17292
- };
17293
- return { workDir, timelineDir, env };
17294
- }
17295
-
17296
17337
  // daemon/execenv/steering.ts
17297
17338
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync3, unlinkSync as unlinkSync2, readdirSync, statSync as statSync2 } from "fs";
17298
- import { join as join4 } from "path";
17339
+ import { join as join5 } from "path";
17299
17340
  var INTENT_DIR_NAME = ".kill_intents";
17300
17341
  var INTENT_STALE_MS = 10 * 60 * 1000;
17301
17342
  function intentFilePath(baseDir, taskId) {
17302
- return join4(baseDir, INTENT_DIR_NAME, `${taskId}.json`);
17343
+ return join5(baseDir, INTENT_DIR_NAME, `${taskId}.json`);
17303
17344
  }
17304
17345
  function readKillIntent(baseDir, taskId) {
17305
17346
  const filePath = intentFilePath(baseDir, taskId);
@@ -17319,10 +17360,19 @@ function clearKillIntent(baseDir, taskId) {
17319
17360
 
17320
17361
  // daemon/prompt.ts
17321
17362
  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.";
17363
+ function buildDmNotice(name, email3) {
17364
+ return `This task was triggered by an incoming email on a conversation with ${name} (${email3}).` + ` ${name} is present in this session — reply to them directly.` + ` If you need to communicate with anyone else, use the email sending tool.`;
17365
+ }
17322
17366
  function buildPrompt(task, attachments) {
17323
17367
  const obj = { type: task.type, instruction: task.prompt };
17324
17368
  if (task.type === "email_notification") {
17325
- obj.notice = EMAIL_NOTICE;
17369
+ const ctx = task.context;
17370
+ const dmUser = ctx?.dmUser;
17371
+ if (ctx?.conversationType === "user_dm_message" && dmUser) {
17372
+ obj.notice = buildDmNotice(dmUser.name, dmUser.email);
17373
+ } else {
17374
+ obj.notice = EMAIL_NOTICE;
17375
+ }
17326
17376
  }
17327
17377
  if (task.sender) {
17328
17378
  obj.sender = {
@@ -17342,7 +17392,7 @@ function buildPrompt(task, attachments) {
17342
17392
  }
17343
17393
 
17344
17394
  // daemon/session-runner.ts
17345
- var ATTACHMENTS_BASE = "/tmp/alook-attachments";
17395
+ var ATTACHMENTS_BASE = tempDir("alook-attachments");
17346
17396
  async function writeMarkerFile(workspacesRoot, marker) {
17347
17397
  const dir = path.join(workspacesRoot, ".pending_completions");
17348
17398
  await mkdir(dir, { recursive: true, mode: 448 });
@@ -17395,6 +17445,46 @@ async function runSession(input) {
17395
17445
  const client = new DaemonClient(serverURL);
17396
17446
  const backend = createBackend(provider, cliPath);
17397
17447
  const { workDir, timelineDir, env } = prepare({ workspacesRoot }, task);
17448
+ const agentBaseDir = path.dirname(timelineDir);
17449
+ await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, undefined, process.pid, provider, task.contextKey, input.logFilePath));
17450
+ let killed = false;
17451
+ const earlyOnKill = async () => {
17452
+ if (killed)
17453
+ return;
17454
+ killed = true;
17455
+ log.info(`killed by signal (messages=0, tools=0)`);
17456
+ await cleanupAttachments(task.id);
17457
+ const intent = readKillIntent(agentBaseDir, task.id);
17458
+ clearKillIntent(agentBaseDir, task.id);
17459
+ if (intent?.reason === "superseded") {
17460
+ updateEntry(timelineDir, task.id, (entry) => {
17461
+ entry.pid = null;
17462
+ entry.status = "superseded";
17463
+ entry.successor_task_id = intent.successorTaskId ?? null;
17464
+ entry.supersede_reason = "superseded by newer task";
17465
+ });
17466
+ try {
17467
+ await client.supersedeTask(token, task.id);
17468
+ } catch {}
17469
+ } else if (intent?.reason === "cancelled") {
17470
+ updateEntry(timelineDir, task.id, (entry) => {
17471
+ entry.pid = null;
17472
+ entry.status = "cancelled";
17473
+ entry.errmsg = "cancelled by user";
17474
+ });
17475
+ await reportToServer(() => client.failTask(token, task.id, "cancelled by user"), { taskId: task.id, type: "fail", payload: { error: "cancelled by user" }, token, serverURL, createdAt: new Date().toISOString() }, workspacesRoot);
17476
+ } else {
17477
+ updateEntry(timelineDir, task.id, (entry) => {
17478
+ entry.pid = null;
17479
+ entry.status = "killed";
17480
+ entry.errmsg = "killed by signal";
17481
+ });
17482
+ await reportToServer(() => client.failTask(token, task.id, "killed by signal"), { taskId: task.id, type: "fail", payload: { error: "killed by signal" }, token, serverURL, createdAt: new Date().toISOString() }, workspacesRoot);
17483
+ }
17484
+ process.exit(1);
17485
+ };
17486
+ process.on("SIGTERM", earlyOnKill);
17487
+ process.on("SIGINT", earlyOnKill);
17398
17488
  const attachmentIds = task.context?.attachment_ids ?? [];
17399
17489
  let attachments;
17400
17490
  if (attachmentIds.length > 0) {
@@ -17406,7 +17496,14 @@ async function runSession(input) {
17406
17496
  await cleanupAttachments(task.id);
17407
17497
  const errMsg = `failed to download attachments: ${e}`;
17408
17498
  log.error(errMsg);
17499
+ updateEntry(timelineDir, task.id, (entry) => {
17500
+ entry.pid = null;
17501
+ entry.status = "failed";
17502
+ entry.errmsg = errMsg;
17503
+ });
17409
17504
  await client.failTask(token, task.id, errMsg);
17505
+ process.removeListener("SIGTERM", earlyOnKill);
17506
+ process.removeListener("SIGINT", earlyOnKill);
17410
17507
  return;
17411
17508
  }
17412
17509
  }
@@ -17426,7 +17523,9 @@ async function runSession(input) {
17426
17523
  const earlySessionId = await session2.sessionId;
17427
17524
  log.info(`agent started (pid=${agentPid ?? "unknown"}, session=${earlySessionId})`);
17428
17525
  log.info(JSON.stringify({ role: "user", type: "text", content: prompt }));
17429
- await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, earlySessionId, process.pid, provider, task.contextKey, input.logFilePath));
17526
+ updateEntry(timelineDir, task.id, (entry) => {
17527
+ entry.session_id = earlySessionId || null;
17528
+ });
17430
17529
  const pendingMessages = [];
17431
17530
  let seq = 0;
17432
17531
  let toolCount = 0;
@@ -17443,8 +17542,8 @@ async function runSession(input) {
17443
17542
  }
17444
17543
  };
17445
17544
  const flushTimer = setInterval(flushMessages, FLUSH_INTERVAL_MS);
17446
- let killed = false;
17447
- const agentBaseDir = path.dirname(timelineDir);
17545
+ process.removeListener("SIGTERM", earlyOnKill);
17546
+ process.removeListener("SIGINT", earlyOnKill);
17448
17547
  const onKill = async () => {
17449
17548
  if (killed)
17450
17549
  return;
@@ -17531,7 +17630,11 @@ async function runSession(input) {
17531
17630
  });
17532
17631
  if (msg.type === "tool-use")
17533
17632
  toolCount++;
17534
- log.info(JSON.stringify({ role: "assistant", ...msg }));
17633
+ if (msg.type === "tool-result" && msg.output && msg.output.length > 500) {
17634
+ log.info(JSON.stringify({ role: "assistant", ...msg, output: msg.output.slice(0, 500) + `... (${msg.output.length} chars)` }));
17635
+ } else {
17636
+ log.info(JSON.stringify({ role: "assistant", ...msg }));
17637
+ }
17535
17638
  if (msg.type === "text" && msg.content) {
17536
17639
  updateEntry(timelineDir, task.id, (entry) => {
17537
17640
  entry.agent_responses.push(msg.content);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/cli",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
4
4
  "description": "Alook CLI — Enable Your Person Colleague",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/alookai/alook#readme",