@alook/cli 0.0.26 → 0.0.28

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(),
@@ -15709,13 +15719,13 @@ var agent = sqliteTable("agent", {
15709
15719
  description: text("description").notNull().default(""),
15710
15720
  instructions: text("instructions").notNull().default(""),
15711
15721
  avatarUrl: text("avatar_url"),
15712
- runtimeId: text("runtime_id").references(() => agentRuntime.id),
15722
+ runtimeId: text("runtime_id").notNull().references(() => agentRuntime.id, { onDelete: "cascade" }),
15713
15723
  runtimeMode: text("runtime_mode").notNull().default("local"),
15714
15724
  runtimeConfig: text("runtime_config", { mode: "json" }),
15715
15725
  visibility: text("visibility").notNull().default("private"),
15716
15726
  status: text("status").notNull().default("idle"),
15717
15727
  maxConcurrentTasks: integer2("max_concurrent_tasks").notNull().default(6),
15718
- ownerId: text("owner_id").references(() => user.id),
15728
+ ownerId: text("owner_id").references(() => user.id, { onDelete: "cascade" }),
15719
15729
  tools: text("tools", { mode: "json" }),
15720
15730
  triggers: text("triggers", { mode: "json" }),
15721
15731
  emailHandle: text("email_handle").unique(),
@@ -15774,9 +15784,9 @@ var message = sqliteTable("message", {
15774
15784
  var agentTaskQueue = sqliteTable("agent_task_queue", {
15775
15785
  id: text("id").primaryKey().$defaultFn(() => nanoid3()),
15776
15786
  agentId: text("agent_id").notNull(),
15777
- runtimeId: text("runtime_id").notNull().references(() => agentRuntime.id),
15778
- workspaceId: text("workspace_id").notNull().references(() => workspace.id),
15779
- conversationId: text("conversation_id").notNull().references(() => conversation.id),
15787
+ runtimeId: text("runtime_id").notNull().references(() => agentRuntime.id, { onDelete: "cascade" }),
15788
+ workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
15789
+ conversationId: text("conversation_id").notNull().references(() => conversation.id, { onDelete: "cascade" }),
15780
15790
  prompt: text("prompt").notNull(),
15781
15791
  type: text("type").notNull().default(TASK_TYPES.USER_DM_MESSAGE),
15782
15792
  contextKey: text("context_key"),
@@ -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" }),
@@ -15952,7 +15971,13 @@ var workspaceFileRequest = sqliteTable("workspace_file_request", {
15952
15971
  result: text("result"),
15953
15972
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString()),
15954
15973
  updatedAt: text("updated_at").notNull().$defaultFn(() => new Date().toISOString())
15955
- }, (t) => [index("idx_wfr_workspace_status").on(t.workspaceId, t.status)]);
15974
+ }, (t) => [
15975
+ index("idx_wfr_workspace_status").on(t.workspaceId, t.status),
15976
+ foreignKey({
15977
+ columns: [t.agentId, t.workspaceId],
15978
+ foreignColumns: [agent.id, agent.workspaceId]
15979
+ }).onDelete("cascade")
15980
+ ]);
15956
15981
  // ../shared/src/db/queries/task.ts
15957
15982
  var DEFAULT_STALE_SECONDS = Number(process.env.ALOOK_STALE_DISPATCH_TIMEOUT_S) || 20;
15958
15983
  var DEFAULT_STALE_RUNNING_SECONDS = Number(process.env.ALOOK_STALE_RUNNING_TIMEOUT_S) || 3600;
@@ -16045,7 +16070,8 @@ class DaemonClient {
16045
16070
  evicted: resp.evicted ?? false,
16046
16071
  pending_update: resp.pending_update,
16047
16072
  pending_rescan: resp.pending_rescan,
16048
- file_requests: resp.file_requests
16073
+ file_requests: resp.file_requests,
16074
+ meetings: resp.meetings
16049
16075
  };
16050
16076
  }
16051
16077
  startTask(token, taskId) {
@@ -16095,15 +16121,6 @@ class DaemonClient {
16095
16121
  reportFileData(token, body) {
16096
16122
  return this.request("POST", "/api/daemon/workspace/report", token, body);
16097
16123
  }
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
16124
  }
16108
16125
 
16109
16126
  // daemon/config.ts
@@ -16276,7 +16293,7 @@ function fromApiTask(api2) {
16276
16293
  type: api2.type,
16277
16294
  contextKey: api2.context_key ?? null,
16278
16295
  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,
16296
+ 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
16297
  sender: api2.sender ? { name: api2.sender.name, email: api2.sender.email, isOwner: api2.sender.is_owner } : undefined,
16281
16298
  repos: undefined,
16282
16299
  createdAt: api2.created_at
@@ -16583,8 +16600,7 @@ function recentFilenames(maxDays) {
16583
16600
  }
16584
16601
  return filenames;
16585
16602
  }
16586
- var DEFAULT_RESUME_MAX_AGE_MS = 3 * 60 * 60 * 1000;
16587
- var EMAIL_RESUME_MAX_AGE_MS = 48 * 60 * 60 * 1000;
16603
+ var RESUME_MAX_AGE_MS = 72 * 60 * 60 * 1000;
16588
16604
  function findRunningPidByTaskId(timelineDir, taskId) {
16589
16605
  for (const filename of recentFilenames(7)) {
16590
16606
  const entries = readJsonl(join4(timelineDir, filename));
@@ -16669,7 +16685,7 @@ function releaseSteeringLock(baseDir, contextKey) {
16669
16685
 
16670
16686
  // daemon/workspace-files.ts
16671
16687
  import { readdir, stat, readFile } from "fs/promises";
16672
- import { join as join6, resolve, extname, relative } from "path";
16688
+ import { join as join6, resolve, extname, relative, sep } from "path";
16673
16689
  var SKIP_DIRS = new Set([".git", "node_modules", ".next", ".wrangler", "__pycache__", ".venv"]);
16674
16690
  var TEXT_EXTENSIONS = new Set([
16675
16691
  ".md",
@@ -16755,24 +16771,61 @@ async function readFileContent(filePath) {
16755
16771
  }
16756
16772
  function validatePath(agentWorkdir, requestedPath) {
16757
16773
  const resolved = resolve(agentWorkdir, requestedPath);
16758
- if (resolved !== agentWorkdir && !resolved.startsWith(agentWorkdir + "/"))
16774
+ if (resolved !== agentWorkdir && !resolved.startsWith(agentWorkdir + sep))
16759
16775
  return null;
16760
16776
  return resolved;
16761
16777
  }
16762
16778
 
16779
+ // lib/shell-env.ts
16780
+ import { execSync as execSync3 } from "child_process";
16781
+
16782
+ // lib/platform.ts
16783
+ import { tmpdir } from "os";
16784
+ import { join as join7, sep as sep2 } from "path";
16785
+ var isWindows = process.platform === "win32";
16786
+ function tempDir(subdir) {
16787
+ return join7(tmpdir(), subdir);
16788
+ }
16789
+
16790
+ // lib/shell-env.ts
16791
+ function resolveLoginShellEnv() {
16792
+ if (isWindows) {
16793
+ return { ...process.env };
16794
+ }
16795
+ const shell = process.env.SHELL || "/bin/zsh";
16796
+ try {
16797
+ const output = execSync3(`${shell} -lc 'env'`, {
16798
+ encoding: "utf-8",
16799
+ timeout: 5000,
16800
+ stdio: ["ignore", "pipe", "ignore"]
16801
+ });
16802
+ const env = {};
16803
+ for (const line of output.split(`
16804
+ `)) {
16805
+ const idx = line.indexOf("=");
16806
+ if (idx > 0) {
16807
+ env[line.slice(0, idx)] = line.slice(idx + 1);
16808
+ }
16809
+ }
16810
+ if (env.PATH)
16811
+ return env;
16812
+ } catch {}
16813
+ return { ...process.env };
16814
+ }
16815
+
16763
16816
  // daemon/daemon.ts
16764
16817
  import { existsSync, mkdirSync as mkdirSync5, openSync, closeSync, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync4 } from "fs";
16765
16818
  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";
16819
+ import { execSync as execSync4, spawn as spawn2 } from "child_process";
16767
16820
  import { fileURLToPath as fileURLToPath2 } from "url";
16768
- import { dirname as dirname3, join as join7 } from "path";
16821
+ import { dirname as dirname3, join as join8 } from "path";
16769
16822
  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");
16823
+ var sessionRunnerPath = existsSync(join8(_dir, "session-runner.js")) ? join8(_dir, "session-runner.js") : join8(_dir, "session-runner.ts");
16824
+ var meetingRunnerPath = existsSync(join8(_dir, "meeting-runner.js")) ? join8(_dir, "meeting-runner.js") : join8(_dir, "meeting-runner.ts");
16772
16825
  function isCommandAvailable2(cmd) {
16773
16826
  try {
16774
16827
  const check2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
16775
- execSync3(check2, { stdio: "ignore" });
16828
+ execSync4(check2, { stdio: "ignore" });
16776
16829
  return true;
16777
16830
  } catch {
16778
16831
  return false;
@@ -16790,7 +16843,7 @@ function pruneSessionRunnerLogs() {
16790
16843
  if (entries.length <= MAX_SESSION_RUNNER_LOGS)
16791
16844
  return;
16792
16845
  const withMtime = entries.map((name) => {
16793
- const full = join7(logDir, name);
16846
+ const full = join8(logDir, name);
16794
16847
  try {
16795
16848
  return { name, mtime: statSync3(full).mtimeMs };
16796
16849
  } catch {
@@ -16800,7 +16853,7 @@ function pruneSessionRunnerLogs() {
16800
16853
  withMtime.sort((a, b) => b.mtime - a.mtime);
16801
16854
  for (const entry of withMtime.slice(MAX_SESSION_RUNNER_LOGS)) {
16802
16855
  try {
16803
- unlinkSync4(join7(logDir, entry.name));
16856
+ unlinkSync4(join8(logDir, entry.name));
16804
16857
  } catch {}
16805
16858
  }
16806
16859
  }
@@ -16841,7 +16894,7 @@ function isValidMarker(data) {
16841
16894
  var MARKER_STALE_MS = 24 * 60 * 60 * 1000;
16842
16895
  var TMP_STALE_MS = 60 * 60 * 1000;
16843
16896
  async function reconcilePendingCompletions(workspacesRoot) {
16844
- const dir = join7(workspacesRoot, ".pending_completions");
16897
+ const dir = join8(workspacesRoot, ".pending_completions");
16845
16898
  let entries;
16846
16899
  try {
16847
16900
  entries = await readdir2(dir);
@@ -16852,15 +16905,15 @@ async function reconcilePendingCompletions(workspacesRoot) {
16852
16905
  if (!name.endsWith(".tmp"))
16853
16906
  continue;
16854
16907
  try {
16855
- const s = await fsStat(join7(dir, name));
16908
+ const s = await fsStat(join8(dir, name));
16856
16909
  if (Date.now() - s.mtimeMs > TMP_STALE_MS) {
16857
- await unlink(join7(dir, name));
16910
+ await unlink(join8(dir, name));
16858
16911
  }
16859
16912
  } catch {}
16860
16913
  }
16861
16914
  const jsonFiles = entries.filter((f) => f.endsWith(".json"));
16862
16915
  for (const name of jsonFiles) {
16863
- const filePath = join7(dir, name);
16916
+ const filePath = join8(dir, name);
16864
16917
  try {
16865
16918
  let raw;
16866
16919
  try {
@@ -16937,9 +16990,13 @@ async function startDaemon(profile, serverUrl) {
16937
16990
  if (serverUrl)
16938
16991
  config2.serverURL = serverUrl;
16939
16992
  const marker = readUpdateMarker(profile);
16940
- if (marker && marker === config2.cliVersion) {
16993
+ if (marker) {
16941
16994
  clearUpdateMarker(profile);
16942
- log.info(`Cleared update marker now running v${config2.cliVersion}`);
16995
+ if (marker === config2.cliVersion) {
16996
+ log.info(`Cleared update marker — now running v${config2.cliVersion}`);
16997
+ } else {
16998
+ log.info(`Cleared stale update marker (was v${marker}, running v${config2.cliVersion}) — update will be retried`);
16999
+ }
16943
17000
  }
16944
17001
  const cliConfig = loadCLIConfigForProfile(profile);
16945
17002
  const workspaces = cliConfig.watched_workspaces || [];
@@ -17070,7 +17127,7 @@ async function startDaemon(profile, serverUrl) {
17070
17127
  await new Promise((r) => setTimeout(r, staggerMs));
17071
17128
  }
17072
17129
  try {
17073
- const { tasks: apiTasks, evicted, pending_update, pending_rescan, file_requests } = await client.poll(ws.token, config2.daemonId, remaining, config2.cliVersion);
17130
+ const { tasks: apiTasks, evicted, pending_update, pending_rescan, file_requests, meetings } = await client.poll(ws.token, config2.daemonId, remaining, config2.cliVersion);
17074
17131
  if (evicted) {
17075
17132
  evictedIds.push(ws.workspaceId);
17076
17133
  continue;
@@ -17097,6 +17154,19 @@ async function startDaemon(profile, serverUrl) {
17097
17154
  handleFileRequest(client, config2, ws.workspaceId, req, ws.token).catch((e) => log.debug("File request error", e));
17098
17155
  }
17099
17156
  }
17157
+ if (meetings) {
17158
+ for (const m of meetings) {
17159
+ spawnMeetingRunner({
17160
+ meetingId: m.id,
17161
+ meetingUrl: m.meeting_url,
17162
+ participants: m.participants,
17163
+ workspaceId: m.workspace_id,
17164
+ callbackUrl: config2.serverURL,
17165
+ authToken: ws.token,
17166
+ agentName: m.agent_name
17167
+ });
17168
+ }
17169
+ }
17100
17170
  } catch (e) {
17101
17171
  if (e instanceof Error && e.message.startsWith("HTTP 401")) {
17102
17172
  log.warn(`Workspace ${ws.workspaceId} poll returned 401 — will retry next cycle`);
@@ -17108,23 +17178,6 @@ async function startDaemon(profile, serverUrl) {
17108
17178
  for (const id of evictedIds) {
17109
17179
  evictWorkspace(id);
17110
17180
  }
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
17181
  try {
17129
17182
  await reconcilePendingCompletions(config2.workspacesRoot);
17130
17183
  } catch (e) {
@@ -17174,7 +17227,8 @@ async function startDaemon(profile, serverUrl) {
17174
17227
  }
17175
17228
  const child = spawn2(process.execPath, args, {
17176
17229
  detached: true,
17177
- stdio: logFd != null ? ["ignore", logFd, logFd] : ["ignore", "ignore", "ignore"]
17230
+ stdio: logFd != null ? ["ignore", logFd, logFd] : ["ignore", "ignore", "ignore"],
17231
+ env: resolveLoginShellEnv()
17178
17232
  });
17179
17233
  child.unref();
17180
17234
  if (logFd != null)
@@ -17192,7 +17246,7 @@ async function startDaemon(profile, serverUrl) {
17192
17246
  function spawnSessionRunner(input) {
17193
17247
  const logDir = sessionRunnerLogDir();
17194
17248
  mkdirSync5(logDir, { recursive: true });
17195
- const logFilePath = join7(logDir, `${input.task.id}.log`);
17249
+ const logFilePath = join8(logDir, `${input.task.id}.log`);
17196
17250
  input.logFilePath = logFilePath;
17197
17251
  const encoded = Buffer.from(JSON.stringify(input)).toString("base64");
17198
17252
  let fd;
@@ -17213,7 +17267,7 @@ function spawnSessionRunner(input) {
17213
17267
  function spawnMeetingRunner(input) {
17214
17268
  const logDir = sessionRunnerLogDir();
17215
17269
  mkdirSync5(logDir, { recursive: true });
17216
- const logFilePath = join7(logDir, `meeting-${input.meetingId}.log`);
17270
+ const logFilePath = join8(logDir, `meeting-${input.meetingId}.log`);
17217
17271
  const encoded = Buffer.from(JSON.stringify(input)).toString("base64");
17218
17272
  let fd;
17219
17273
  try {
@@ -17232,7 +17286,7 @@ function spawnMeetingRunner(input) {
17232
17286
  return child;
17233
17287
  }
17234
17288
  async function handleFileRequest(client, config2, workspaceId, req, token) {
17235
- const agentWorkdir = join7(config2.workspacesRoot, workspaceId, req.agent_id, "workdir");
17289
+ const agentWorkdir = join8(config2.workspacesRoot, workspaceId, req.agent_id, "workdir");
17236
17290
  const resolved = validatePath(agentWorkdir, req.path);
17237
17291
  if (!resolved) {
17238
17292
  await client.reportFileData(token, { request_id: req.id, error: "invalid path", path: req.path });
@@ -17263,9 +17317,18 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
17263
17317
  activeTasks.delete(task.id);
17264
17318
  return;
17265
17319
  }
17266
- const agentBaseDir = join7(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
17267
- const timelineDir = join7(agentBaseDir, ".context_timeline");
17268
- const pid = findRunningPidByTaskId(timelineDir, targetTaskId);
17320
+ const agentBaseDir = join8(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
17321
+ const timelineDir = join8(agentBaseDir, ".context_timeline");
17322
+ const MAX_WAIT_MS = Number(process.env.ALOOK_KILL_TASK_MAX_WAIT_MS) || 15000;
17323
+ const POLL_MS = Number(process.env.ALOOK_KILL_TASK_POLL_MS) || 200;
17324
+ const waitStart = Date.now();
17325
+ let pid = null;
17326
+ while (Date.now() - waitStart < MAX_WAIT_MS) {
17327
+ pid = findRunningPidByTaskId(timelineDir, targetTaskId);
17328
+ if (pid != null)
17329
+ break;
17330
+ await new Promise((r) => setTimeout(r, POLL_MS));
17331
+ }
17269
17332
  if (pid != null) {
17270
17333
  writeKillIntent(agentBaseDir, {
17271
17334
  reason: "cancelled",
@@ -17306,9 +17369,9 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
17306
17369
  }
17307
17370
  const provider = runtimeData.provider;
17308
17371
  if (task.contextKey) {
17309
- const agentBaseDir = join7(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
17372
+ const agentBaseDir = join8(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
17310
17373
  cleanupStaleIntents(agentBaseDir);
17311
- const timelineDir = join7(agentBaseDir, ".context_timeline");
17374
+ const timelineDir = join8(agentBaseDir, ".context_timeline");
17312
17375
  const lockAcquired = acquireSteeringLock(agentBaseDir, task.contextKey);
17313
17376
  if (!lockAcquired) {
17314
17377
  log.warn(`Steering lock contention for context_key=${task.contextKey}, proceeding without steering`);
@@ -17418,7 +17481,8 @@ async function startInBackground(profile, serverUrl) {
17418
17481
  const logFd = openSync2(logPath, "a", 384);
17419
17482
  const child = spawn3(process.execPath, buildChildArgs(profile, serverUrl), {
17420
17483
  detached: true,
17421
- stdio: ["ignore", logFd, logFd]
17484
+ stdio: ["ignore", logFd, logFd],
17485
+ env: resolveLoginShellEnv()
17422
17486
  });
17423
17487
  child.unref();
17424
17488
  closeSync2(logFd);
@@ -17471,10 +17535,12 @@ async function stopCommand(profile) {
17471
17535
  }
17472
17536
  await sleep(STOP_POLL_INTERVAL_MS);
17473
17537
  }
17474
- console.warn(`Daemon did not exit within ${shutdownMs}ms — sending SIGKILL.`);
17475
- try {
17476
- process.kill(pid, "SIGKILL");
17477
- } catch {}
17538
+ console.warn(`Daemon did not exit within ${shutdownMs}ms — force killing.`);
17539
+ if (!isWindows) {
17540
+ try {
17541
+ process.kill(pid, "SIGKILL");
17542
+ } catch {}
17543
+ }
17478
17544
  removePidFileIfMatches(pid, profile);
17479
17545
  console.log("Daemon stopped.");
17480
17546
  }
@@ -17533,10 +17599,11 @@ function configCommand() {
17533
17599
  // commands/email.ts
17534
17600
  import { Command as Command5 } from "commander";
17535
17601
  import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
17536
- import { basename, join as join8 } from "path";
17602
+ import { basename, join as join9 } from "path";
17537
17603
  import PostalMime from "postal-mime";
17538
- var VALID_STATUSES = ["unread", "read", "archived"];
17539
- var EMAIL_BASE = "/tmp/alook-emails";
17604
+ var VALID_STATUSES = ["unread", "read", "archived", "sent"];
17605
+ var VALID_FOLDERS = ["inbox", "sent", "untrust"];
17606
+ var EMAIL_BASE = tempDir("alook-emails");
17540
17607
  var MIME_BY_EXT = {
17541
17608
  ".pdf": "application/pdf",
17542
17609
  ".png": "image/png",
@@ -17604,18 +17671,42 @@ function resolveClientOpts(command, opts) {
17604
17671
  }
17605
17672
  function emailCommand() {
17606
17673
  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) => {
17674
+ 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
17675
  const { serverUrl, token, workspaceId } = resolveClientOpts(command, { workspace: opts.workspace, agentId: opts.agent_id });
17609
17676
  const client = new APIClient(serverUrl, token, workspaceId);
17610
17677
  if (opts.status && !VALID_STATUSES.includes(opts.status)) {
17611
17678
  console.error(`Error: invalid status "${opts.status}", must be one of: ${VALID_STATUSES.join(", ")}`);
17612
17679
  process.exit(1);
17613
17680
  }
17614
- const emailDir_base = join8(EMAIL_BASE, workspaceId, opts.agent_id);
17681
+ if (opts.folder && !VALID_FOLDERS.includes(opts.folder)) {
17682
+ console.error(`Error: invalid folder "${opts.folder}", must be one of: ${VALID_FOLDERS.join(", ")}`);
17683
+ process.exit(1);
17684
+ }
17685
+ if (opts.limit != null) {
17686
+ const n = parseInt(opts.limit, 10);
17687
+ if (isNaN(n) || n < 1 || n > 100) {
17688
+ console.error(`Error: --limit must be an integer between 1 and 100`);
17689
+ process.exit(1);
17690
+ }
17691
+ }
17692
+ if (opts.offset != null) {
17693
+ const n = parseInt(opts.offset, 10);
17694
+ if (isNaN(n) || n < 0) {
17695
+ console.error(`Error: --offset must be a non-negative integer`);
17696
+ process.exit(1);
17697
+ }
17698
+ }
17699
+ const emailDir_base = join9(EMAIL_BASE, workspaceId, opts.agent_id);
17615
17700
  try {
17616
17701
  let query = `/api/email?agentId=${opts.agent_id}`;
17617
17702
  if (opts.status)
17618
17703
  query += `&status=${opts.status}`;
17704
+ if (opts.folder)
17705
+ query += `&folder=${opts.folder}`;
17706
+ if (opts.limit)
17707
+ query += `&limit=${opts.limit}`;
17708
+ if (opts.offset)
17709
+ query += `&offset=${opts.offset}`;
17619
17710
  const emails2 = await client.getJSON(query);
17620
17711
  if (!emails2.length) {
17621
17712
  console.log("No emails found.");
@@ -17628,7 +17719,7 @@ function emailCommand() {
17628
17719
  mkdirSync7(emailDir_base, { recursive: true });
17629
17720
  const downloadedPaths = [];
17630
17721
  for (const email3 of emails2) {
17631
- const emailDir = join8(emailDir_base, email3.id);
17722
+ const emailDir = join9(emailDir_base, email3.id);
17632
17723
  mkdirSync7(emailDir, { recursive: true });
17633
17724
  const metadata = {
17634
17725
  id: email3.id,
@@ -17641,7 +17732,7 @@ function emailCommand() {
17641
17732
  in_reply_to: email3.in_reply_to || "",
17642
17733
  references: email3.references || ""
17643
17734
  };
17644
- const metadataPath = join8(emailDir, "metadata.json");
17735
+ const metadataPath = join9(emailDir, "metadata.json");
17645
17736
  writeFileSync6(metadataPath, JSON.stringify(metadata, null, 2));
17646
17737
  downloadedPaths.push(metadataPath);
17647
17738
  let rawMime;
@@ -17657,17 +17748,17 @@ function emailCommand() {
17657
17748
  }
17658
17749
  const parsed = await new PostalMime().parse(rawMime);
17659
17750
  if (parsed.text) {
17660
- const bodyPath = join8(emailDir, "body.txt");
17751
+ const bodyPath = join9(emailDir, "body.txt");
17661
17752
  writeFileSync6(bodyPath, parsed.text);
17662
17753
  downloadedPaths.push(bodyPath);
17663
17754
  }
17664
17755
  if (parsed.html) {
17665
- const htmlPath = join8(emailDir, "body.html");
17756
+ const htmlPath = join9(emailDir, "body.html");
17666
17757
  writeFileSync6(htmlPath, parsed.html);
17667
17758
  downloadedPaths.push(htmlPath);
17668
17759
  }
17669
17760
  if (parsed.attachments && parsed.attachments.length > 0) {
17670
- const attDir = join8(emailDir, "attachments");
17761
+ const attDir = join9(emailDir, "attachments");
17671
17762
  mkdirSync7(attDir, { recursive: true });
17672
17763
  const usedFilenames = new Set;
17673
17764
  for (let i = 0;i < parsed.attachments.length; i++) {
@@ -17677,7 +17768,7 @@ function emailCommand() {
17677
17768
  filename = `${i}-${filename}`;
17678
17769
  }
17679
17770
  usedFilenames.add(filename);
17680
- const attPath = join8(attDir, filename);
17771
+ const attPath = join9(attDir, filename);
17681
17772
  const content = att.content;
17682
17773
  let buf;
17683
17774
  if (typeof content === "string") {
@@ -17773,6 +17864,7 @@ function emailCommand() {
17773
17864
  console.warn(`Warning: could not fetch parent email ${opts.inReplyTo}, sending without threading`);
17774
17865
  }
17775
17866
  }
17867
+ const conversationId = process.env.ALOOK_CONVERSATION_ID;
17776
17868
  const res = await client.postJSON("/api/email/send", {
17777
17869
  agentId: opts.agent_id,
17778
17870
  to: opts.to,
@@ -17780,7 +17872,8 @@ function emailCommand() {
17780
17872
  htmlBody,
17781
17873
  attachments,
17782
17874
  ...inReplyTo ? { inReplyTo, references } : {},
17783
- ...opts.from ? { from: opts.from } : {}
17875
+ ...opts.from ? { from: opts.from } : {},
17876
+ ...conversationId ? { conversationId } : {}
17784
17877
  });
17785
17878
  console.log(`Sent email to ${res.to_email} (id: ${res.id})`);
17786
17879
  } catch (err) {
@@ -17788,6 +17881,118 @@ function emailCommand() {
17788
17881
  process.exit(1);
17789
17882
  }
17790
17883
  });
17884
+ 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) => {
17885
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, {
17886
+ workspace: opts.workspace,
17887
+ agentId: opts.agent_id
17888
+ });
17889
+ const client = new APIClient(serverUrl, token, workspaceId);
17890
+ try {
17891
+ let original;
17892
+ try {
17893
+ original = await client.getJSON(`/api/email/${opts.email_id}`);
17894
+ } catch (err) {
17895
+ const msg = err instanceof Error ? err.message : String(err);
17896
+ if (msg.includes("404")) {
17897
+ console.error(`Error: email ${opts.email_id} not found`);
17898
+ process.exit(1);
17899
+ }
17900
+ throw err;
17901
+ }
17902
+ let rawMime;
17903
+ try {
17904
+ rawMime = await client.getText(`/api/email/${opts.email_id}/raw`);
17905
+ } catch (err) {
17906
+ const msg = err instanceof Error ? err.message : String(err);
17907
+ if (msg.includes("404")) {
17908
+ console.error(`Error: raw email body not available for ${opts.email_id}`);
17909
+ process.exit(1);
17910
+ }
17911
+ throw err;
17912
+ }
17913
+ const parsed = await new PostalMime().parse(rawMime);
17914
+ const attachments = [];
17915
+ if (parsed.attachments && parsed.attachments.length > 0) {
17916
+ for (const att of parsed.attachments) {
17917
+ const filename = att.filename || "attachment.bin";
17918
+ const contentType = att.mimeType || "application/octet-stream";
17919
+ const content = att.content;
17920
+ let buf;
17921
+ if (typeof content === "string") {
17922
+ buf = Buffer.from(content, "base64");
17923
+ } else if (content instanceof ArrayBuffer) {
17924
+ buf = Buffer.from(new Uint8Array(content));
17925
+ } else {
17926
+ buf = Buffer.from(content);
17927
+ }
17928
+ const form = new FormData;
17929
+ form.append("file", new Blob([new Uint8Array(buf)], { type: contentType }), filename);
17930
+ const uploaded = await client.postMultipart("/api/email/upload", form);
17931
+ attachments.push({
17932
+ key: uploaded.key,
17933
+ filename: uploaded.filename,
17934
+ size: uploaded.size ?? buf.byteLength,
17935
+ contentType: uploaded.contentType ?? contentType
17936
+ });
17937
+ }
17938
+ }
17939
+ const extraPaths = opts.attachment ?? [];
17940
+ for (const path of extraPaths) {
17941
+ let bytes;
17942
+ let size;
17943
+ try {
17944
+ bytes = readFileSync7(path);
17945
+ size = statSync4(path).size;
17946
+ } catch (err) {
17947
+ console.error(`Error: cannot read attachment "${path}": ${err instanceof Error ? err.message : err}`);
17948
+ process.exit(1);
17949
+ }
17950
+ const filename = basename(path);
17951
+ const contentType = guessContentType(filename);
17952
+ const form = new FormData;
17953
+ form.append("file", new Blob([new Uint8Array(bytes)], { type: contentType }), filename);
17954
+ const uploaded = await client.postMultipart("/api/email/upload", form);
17955
+ attachments.push({
17956
+ key: uploaded.key,
17957
+ filename: uploaded.filename,
17958
+ size: uploaded.size ?? size,
17959
+ contentType: uploaded.contentType ?? contentType
17960
+ });
17961
+ }
17962
+ let htmlBody = "";
17963
+ if (opts.note) {
17964
+ htmlBody += `<p>${opts.note}</p>`;
17965
+ }
17966
+ htmlBody += `<br><br>---------- Forwarded message ----------<br>`;
17967
+ htmlBody += `From: ${original.from_email}<br>`;
17968
+ htmlBody += `Date: ${original.created_at}<br>`;
17969
+ htmlBody += `Subject: ${original.subject}<br>`;
17970
+ htmlBody += `To: ${original.to_email}<br><br>`;
17971
+ if (parsed.html) {
17972
+ htmlBody += parsed.html;
17973
+ } else if (parsed.text) {
17974
+ htmlBody += `<pre>${parsed.text}</pre>`;
17975
+ }
17976
+ const subject = /^fwd:/i.test(original.subject) ? original.subject : `Fwd: ${original.subject}`;
17977
+ const conversationId = process.env.ALOOK_CONVERSATION_ID;
17978
+ const res = await client.postJSON("/api/email/send", {
17979
+ agentId: opts.agent_id,
17980
+ to: opts.to,
17981
+ subject,
17982
+ htmlBody,
17983
+ attachments,
17984
+ ...opts.from ? { from: opts.from } : {},
17985
+ ...conversationId ? { conversationId } : {}
17986
+ });
17987
+ console.log(`Forwarded email to ${res.to_email} (id: ${res.id})`);
17988
+ } catch (err) {
17989
+ const msg = err instanceof Error ? err.message : String(err);
17990
+ if (msg === "__exit__")
17991
+ throw err;
17992
+ console.error(`Error: ${msg}`);
17993
+ process.exit(1);
17994
+ }
17995
+ });
17791
17996
  const whitelistCmd = new Command5("whitelist").description("Manage email whitelist (allowed senders)");
17792
17997
  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
17998
  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(),
@@ -15426,13 +15436,13 @@ var agent = sqliteTable("agent", {
15426
15436
  description: text("description").notNull().default(""),
15427
15437
  instructions: text("instructions").notNull().default(""),
15428
15438
  avatarUrl: text("avatar_url"),
15429
- runtimeId: text("runtime_id").references(() => agentRuntime.id),
15439
+ runtimeId: text("runtime_id").notNull().references(() => agentRuntime.id, { onDelete: "cascade" }),
15430
15440
  runtimeMode: text("runtime_mode").notNull().default("local"),
15431
15441
  runtimeConfig: text("runtime_config", { mode: "json" }),
15432
15442
  visibility: text("visibility").notNull().default("private"),
15433
15443
  status: text("status").notNull().default("idle"),
15434
15444
  maxConcurrentTasks: integer2("max_concurrent_tasks").notNull().default(6),
15435
- ownerId: text("owner_id").references(() => user.id),
15445
+ ownerId: text("owner_id").references(() => user.id, { onDelete: "cascade" }),
15436
15446
  tools: text("tools", { mode: "json" }),
15437
15447
  triggers: text("triggers", { mode: "json" }),
15438
15448
  emailHandle: text("email_handle").unique(),
@@ -15491,9 +15501,9 @@ var message = sqliteTable("message", {
15491
15501
  var agentTaskQueue = sqliteTable("agent_task_queue", {
15492
15502
  id: text("id").primaryKey().$defaultFn(() => nanoid3()),
15493
15503
  agentId: text("agent_id").notNull(),
15494
- runtimeId: text("runtime_id").notNull().references(() => agentRuntime.id),
15495
- workspaceId: text("workspace_id").notNull().references(() => workspace.id),
15496
- conversationId: text("conversation_id").notNull().references(() => conversation.id),
15504
+ runtimeId: text("runtime_id").notNull().references(() => agentRuntime.id, { onDelete: "cascade" }),
15505
+ workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
15506
+ conversationId: text("conversation_id").notNull().references(() => conversation.id, { onDelete: "cascade" }),
15497
15507
  prompt: text("prompt").notNull(),
15498
15508
  type: text("type").notNull().default(TASK_TYPES.USER_DM_MESSAGE),
15499
15509
  contextKey: text("context_key"),
@@ -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" }),
@@ -15669,7 +15688,13 @@ var workspaceFileRequest = sqliteTable("workspace_file_request", {
15669
15688
  result: text("result"),
15670
15689
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString()),
15671
15690
  updatedAt: text("updated_at").notNull().$defaultFn(() => new Date().toISOString())
15672
- }, (t) => [index("idx_wfr_workspace_status").on(t.workspaceId, t.status)]);
15691
+ }, (t) => [
15692
+ index("idx_wfr_workspace_status").on(t.workspaceId, t.status),
15693
+ foreignKey({
15694
+ columns: [t.agentId, t.workspaceId],
15695
+ foreignColumns: [agent.id, agent.workspaceId]
15696
+ }).onDelete("cascade")
15697
+ ]);
15673
15698
  // ../shared/src/db/queries/task.ts
15674
15699
  var DEFAULT_STALE_SECONDS = Number(process.env.ALOOK_STALE_DISPATCH_TIMEOUT_S) || 20;
15675
15700
  var DEFAULT_STALE_RUNNING_SECONDS = Number(process.env.ALOOK_STALE_RUNNING_TIMEOUT_S) || 3600;
@@ -15751,7 +15776,8 @@ class DaemonClient {
15751
15776
  evicted: resp.evicted ?? false,
15752
15777
  pending_update: resp.pending_update,
15753
15778
  pending_rescan: resp.pending_rescan,
15754
- file_requests: resp.file_requests
15779
+ file_requests: resp.file_requests,
15780
+ meetings: resp.meetings
15755
15781
  };
15756
15782
  }
15757
15783
  startTask(token, taskId) {
@@ -15801,15 +15827,6 @@ class DaemonClient {
15801
15827
  reportFileData(token, body) {
15802
15828
  return this.request("POST", "/api/daemon/workspace/report", token, body);
15803
15829
  }
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
15830
  }
15814
15831
 
15815
15832
  // daemon/agent/claude.ts
@@ -16735,11 +16752,21 @@ function createBackend(provider, cliPath) {
16735
16752
  }
16736
16753
 
16737
16754
  // daemon/execenv/index.ts
16738
- import { mkdirSync as mkdirSync2 } from "fs";
16755
+ import { mkdirSync } from "fs";
16739
16756
  import { join as join3 } from "path";
16740
16757
 
16741
16758
  // daemon/execenv/context.ts
16742
16759
  import { createHash } from "crypto";
16760
+
16761
+ // lib/platform.ts
16762
+ import { tmpdir } from "os";
16763
+ import { join, sep } from "path";
16764
+ var isWindows = process.platform === "win32";
16765
+ function tempDir(subdir) {
16766
+ return join(tmpdir(), subdir);
16767
+ }
16768
+
16769
+ // daemon/execenv/context.ts
16743
16770
  import {
16744
16771
  writeFileSync,
16745
16772
  readFileSync,
@@ -16749,7 +16776,7 @@ import {
16749
16776
  existsSync,
16750
16777
  readlinkSync
16751
16778
  } from "fs";
16752
- import { join } from "path";
16779
+ import { join as join2 } from "path";
16753
16780
  var CANONICAL_FILE = "AGENTS.md";
16754
16781
  var SYMLINK_ALIASES = ["CLAUDE.md"];
16755
16782
  var SYSTEM_PROMPT_BODY = `## Memory Management
@@ -16805,7 +16832,15 @@ those json are sorted by datetime in asc order.
16805
16832
  `;
16806
16833
  function buildInstructionContent(task) {
16807
16834
  const displayName = task.agent?.name || "Alook Agent";
16808
- let content = `You're ${displayName} in the Alook Platform.
16835
+ const alookAddr = task.agent?.emailHandle ? toAlookAddress(task.agent.emailHandle) : null;
16836
+ const customAddrs = (task.agent?.emailAddresses ?? []).filter((a) => a !== alookAddr);
16837
+ const primaryEmail = alookAddr ?? customAddrs[0] ?? null;
16838
+ let agentLine = `You're ${displayName}${primaryEmail ? ` (${primaryEmail})` : ""} in the Alook Platform.`;
16839
+ if (task.agent?.userName || task.agent?.userEmail) {
16840
+ const ownerParts = [task.agent.userName, task.agent.userEmail ? `(${task.agent.userEmail})` : null].filter(Boolean).join(" ");
16841
+ agentLine += ` Your owner and creator is ${ownerParts}.`;
16842
+ }
16843
+ let content = `${agentLine}
16809
16844
  ${SYSTEM_PROMPT_BODY}`;
16810
16845
  if (task.agent?.instructions) {
16811
16846
  content += `## BIG BOSS Instructions
@@ -16819,29 +16854,34 @@ ${task.agent.instructions}
16819
16854
  You can communicate with the world through Alook CLI.
16820
16855
  Your alook agent id is '${task.agentId}'. remember this, most of alook cli will requires you input your agent id.
16821
16856
  `;
16822
- const alookAddr = task.agent?.emailHandle ? toAlookAddress(task.agent.emailHandle) : null;
16823
- const customAddrs = (task.agent?.emailAddresses ?? []).filter((a) => a !== alookAddr);
16824
16857
  if (alookAddr || customAddrs.length > 0) {
16825
16858
  const lines = [];
16826
16859
  if (alookAddr)
16827
16860
  lines.push(`- '${alookAddr}' (default, Alook platform address)`);
16828
16861
  for (const a of customAddrs)
16829
16862
  lines.push(`- '${a}' (custom IMAP/SMTP mailbox)`);
16830
- content += `Your email addresses:
16863
+ content += `
16864
+ Your email addresses:
16831
16865
  ${lines.join(`
16832
16866
  `)}
16833
- ${task.agent?.userEmail ? `Your owner's email address is '${task.agent.userEmail}'.` : ""}
16867
+
16834
16868
 
16835
16869
  ### Emails
16836
16870
  ---
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:
16871
+ 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}/'.
16872
+ ---
16873
+ To download sent emails, add '--folder sent': 'npx @alook/cli email pull --agent_id ${task.agentId} --folder sent'
16874
+ Valid folders: inbox (default), sent, untrust.
16875
+ To limit the number of emails downloaded, add '--limit <N>' (e.g. '--limit 20'). Use '--offset <N>' to skip emails for pagination.
16876
+ Example: 'npx @alook/cli email pull --agent_id ${task.agentId} --status unread --limit 20 --offset 0'
16877
+ ---
16878
+ Each email is saved to '${tempDir("alook-emails")}/${task.workspaceId}/${task.agentId}/<emailId>/' with:
16839
16879
  - 'metadata.json' — sender, recipient, subject, date, status, message_id, in_reply_to, references
16840
16880
  - 'body.txt' — plain text body
16841
16881
  - 'body.html' — HTML body (if available)
16842
16882
  - 'attachments/' — extracted attachment files (if any)
16843
16883
  ---
16844
- Before starting to process an email, mark it as read:
16884
+ Before starting to process an INBOX email, mark it as read:
16845
16885
  - Run 'npx @alook/cli email set --agent_id ${task.agentId} --email_id <EMAIL_ID> --status read'
16846
16886
  ---
16847
16887
 
@@ -16862,6 +16902,15 @@ Tips:
16862
16902
  - 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
16903
  ---
16864
16904
 
16905
+ #### Forwarding an email
16906
+ Forward any email to a new recipient, with an optional note prepended above the original content. All original attachments are re-attached automatically.
16907
+ - Run 'npx @alook/cli email forward --agent_id ${task.agentId} --email_id <EMAIL_ID> --to <RECIPIENT>'
16908
+ - Add '--note "FYI, see the request below."' to prepend a note above the forwarded body.
16909
+ - Add '--from <YOUR_EMAIL_ADDRESS>' to send from a specific mailbox.
16910
+ - Add '--attachment <PATH>' to attach extra files (repeatable).
16911
+ - Example: 'npx @alook/cli email forward --agent_id ${task.agentId} --email_id em_abc --to boss@company.com --note "FYI" --attachment /tmp/summary.pdf'
16912
+ ---
16913
+
16865
16914
  #### Email Whitelist (Allowed Senders)
16866
16915
  Manage which email addresses are allowed to send you emails.
16867
16916
  - List: 'npx @alook/cli email whitelist list --agent_id ${task.agentId}' (add '--json' for machine-readable output)
@@ -16944,13 +16993,13 @@ function hasContentChanged(filePath, newContent) {
16944
16993
  }
16945
16994
  }
16946
16995
  function ensureSymlinks(workDir) {
16947
- const canonicalPath = join(workDir, CANONICAL_FILE);
16996
+ const canonicalPath = join2(workDir, CANONICAL_FILE);
16948
16997
  if (!existsSync(canonicalPath))
16949
16998
  return;
16950
16999
  for (const alias of SYMLINK_ALIASES) {
16951
17000
  if (alias === CANONICAL_FILE)
16952
17001
  continue;
16953
- const aliasPath = join(workDir, alias);
17002
+ const aliasPath = join2(workDir, alias);
16954
17003
  try {
16955
17004
  const stat = lstatSync(aliasPath);
16956
17005
  if (stat.isSymbolicLink()) {
@@ -16970,7 +17019,7 @@ function ensureSymlinks(workDir) {
16970
17019
  }
16971
17020
  function writeInstructionFileIfChanged(workDir, task) {
16972
17021
  const content = buildInstructionContent(task);
16973
- const filePath = join(workDir, CANONICAL_FILE);
17022
+ const filePath = join2(workDir, CANONICAL_FILE);
16974
17023
  const changed = hasContentChanged(filePath, content);
16975
17024
  if (changed) {
16976
17025
  writeFileSync(filePath, content, "utf-8");
@@ -16978,16 +17027,34 @@ function writeInstructionFileIfChanged(workDir, task) {
16978
17027
  ensureSymlinks(workDir);
16979
17028
  return changed;
16980
17029
  }
17030
+
17031
+ // daemon/execenv/index.ts
17032
+ function prepare(config2, task) {
17033
+ const workDir = join3(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
17034
+ mkdirSync(workDir, { recursive: true });
17035
+ const timelineDir = join3(workDir, ".context_timeline");
17036
+ mkdirSync(timelineDir, { recursive: true });
17037
+ writeInstructionFileIfChanged(workDir, task);
17038
+ const env = {
17039
+ ALOOK_WORKSPACE_ID: task.workspaceId,
17040
+ ALOOK_AGENT_ID: task.agentId,
17041
+ ALOOK_TASK_ID: task.id,
17042
+ ALOOK_CONVERSATION_ID: task.conversationId,
17043
+ ALOOK_HEALTH_PORT: process.env.ALOOK_HEALTH_PORT || "19514"
17044
+ };
17045
+ return { workDir, timelineDir, env };
17046
+ }
17047
+
16981
17048
  // daemon/execenv/timeline.ts
16982
17049
  import { appendFileSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync } from "fs";
16983
- import { join as join2 } from "path";
17050
+ import { join as join4 } from "path";
16984
17051
 
16985
17052
  // daemon/execenv/filelock.ts
16986
- import { mkdirSync, rmdirSync, statSync } from "fs";
17053
+ import { mkdirSync as mkdirSync2, rmdirSync, statSync } from "fs";
16987
17054
  var DEFAULT_STALE_MS = 3600000;
16988
17055
  function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
16989
17056
  try {
16990
- mkdirSync(lockPath);
17057
+ mkdirSync2(lockPath);
16991
17058
  return true;
16992
17059
  } catch {
16993
17060
  try {
@@ -16995,7 +17062,7 @@ function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
16995
17062
  if (Date.now() - stat.mtimeMs > staleMs) {
16996
17063
  rmdirSync(lockPath);
16997
17064
  try {
16998
- mkdirSync(lockPath);
17065
+ mkdirSync2(lockPath);
16999
17066
  return true;
17000
17067
  } catch {
17001
17068
  return false;
@@ -17003,7 +17070,7 @@ function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
17003
17070
  }
17004
17071
  } catch {
17005
17072
  try {
17006
- mkdirSync(lockPath);
17073
+ mkdirSync2(lockPath);
17007
17074
  return true;
17008
17075
  } catch {
17009
17076
  return false;
@@ -17166,14 +17233,14 @@ function localISOString() {
17166
17233
  return `${y}-${mo}-${d}T${h}:${mi}:${s}${sign}${hh}:${mm}`;
17167
17234
  }
17168
17235
  function lockPathFor(timelineDir, filename) {
17169
- return join2(timelineDir, `.${filename}.lock`);
17236
+ return join4(timelineDir, `.${filename}.lock`);
17170
17237
  }
17171
17238
  function sleep(ms) {
17172
17239
  return new Promise((resolve) => setTimeout(resolve, ms));
17173
17240
  }
17174
17241
  async function initEntryAsync(timelineDir, entry) {
17175
17242
  const filename = todayFilename();
17176
- const filePath = join2(timelineDir, filename);
17243
+ const filePath = join4(timelineDir, filename);
17177
17244
  const lockPath = lockPathFor(timelineDir, filename);
17178
17245
  try {
17179
17246
  let acquired = acquireLock(lockPath);
@@ -17197,7 +17264,7 @@ async function initEntryAsync(timelineDir, entry) {
17197
17264
  }
17198
17265
  function updateEntry(timelineDir, taskId, updater) {
17199
17266
  for (const filename of recentFilenames(7)) {
17200
- const filePath = join2(timelineDir, filename);
17267
+ const filePath = join4(timelineDir, filename);
17201
17268
  const lockPath = lockPathFor(timelineDir, filename);
17202
17269
  try {
17203
17270
  const acquired = acquireLock(lockPath);
@@ -17225,7 +17292,7 @@ function updateEntry(timelineDir, taskId, updater) {
17225
17292
  });
17226
17293
  if (!found)
17227
17294
  continue;
17228
- const tmpPath = join2(timelineDir, `.${filename}.tmp`);
17295
+ const tmpPath = join4(timelineDir, `.${filename}.tmp`);
17229
17296
  writeFileSync2(tmpPath, updated.join(`
17230
17297
  `) + `
17231
17298
  `);
@@ -17256,16 +17323,13 @@ function createTimelineEntry(taskId, prompt, type, sessionId, pid, provider, con
17256
17323
  detailed_log: detailedLog ?? null
17257
17324
  };
17258
17325
  }
17259
- var DEFAULT_RESUME_MAX_AGE_MS = 3 * 60 * 60 * 1000;
17260
- var EMAIL_RESUME_MAX_AGE_MS = 48 * 60 * 60 * 1000;
17326
+ var RESUME_MAX_AGE_MS = 72 * 60 * 60 * 1000;
17261
17327
  function findResumableSessionByContextKey(timelineDir, contextKey, provider) {
17262
- const maxAgeMs = contextKey.startsWith("email:") ? EMAIL_RESUME_MAX_AGE_MS : DEFAULT_RESUME_MAX_AGE_MS;
17263
17328
  const now = new Date;
17264
- const cutoff = new Date(now.getTime() - maxAgeMs);
17265
- const daysToScan = Math.ceil(maxAgeMs / 86400000) + 1;
17329
+ const cutoff = new Date(now.getTime() - RESUME_MAX_AGE_MS);
17266
17330
  const entries = [];
17267
- for (const filename of recentFilenames(daysToScan)) {
17268
- entries.push(...readJsonl(join2(timelineDir, filename)));
17331
+ for (const filename of recentFilenames(7)) {
17332
+ entries.push(...readJsonl(join4(timelineDir, filename)));
17269
17333
  }
17270
17334
  entries.sort((a, b) => new Date(b.datetime).getTime() - new Date(a.datetime).getTime());
17271
17335
  for (const entry of entries) {
@@ -17276,30 +17340,13 @@ function findResumableSessionByContextKey(timelineDir, contextKey, provider) {
17276
17340
  return null;
17277
17341
  }
17278
17342
 
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
17343
  // daemon/execenv/steering.ts
17297
17344
  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";
17345
+ import { join as join5 } from "path";
17299
17346
  var INTENT_DIR_NAME = ".kill_intents";
17300
17347
  var INTENT_STALE_MS = 10 * 60 * 1000;
17301
17348
  function intentFilePath(baseDir, taskId) {
17302
- return join4(baseDir, INTENT_DIR_NAME, `${taskId}.json`);
17349
+ return join5(baseDir, INTENT_DIR_NAME, `${taskId}.json`);
17303
17350
  }
17304
17351
  function readKillIntent(baseDir, taskId) {
17305
17352
  const filePath = intentFilePath(baseDir, taskId);
@@ -17319,10 +17366,19 @@ function clearKillIntent(baseDir, taskId) {
17319
17366
 
17320
17367
  // daemon/prompt.ts
17321
17368
  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.";
17369
+ function buildDmNotice(name, email3) {
17370
+ 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.`;
17371
+ }
17322
17372
  function buildPrompt(task, attachments) {
17323
17373
  const obj = { type: task.type, instruction: task.prompt };
17324
17374
  if (task.type === "email_notification") {
17325
- obj.notice = EMAIL_NOTICE;
17375
+ const ctx = task.context;
17376
+ const dmUser = ctx?.dmUser;
17377
+ if (ctx?.conversationType === "user_dm_message" && dmUser) {
17378
+ obj.notice = buildDmNotice(dmUser.name, dmUser.email);
17379
+ } else {
17380
+ obj.notice = EMAIL_NOTICE;
17381
+ }
17326
17382
  }
17327
17383
  if (task.sender) {
17328
17384
  obj.sender = {
@@ -17342,7 +17398,7 @@ function buildPrompt(task, attachments) {
17342
17398
  }
17343
17399
 
17344
17400
  // daemon/session-runner.ts
17345
- var ATTACHMENTS_BASE = "/tmp/alook-attachments";
17401
+ var ATTACHMENTS_BASE = tempDir("alook-attachments");
17346
17402
  async function writeMarkerFile(workspacesRoot, marker) {
17347
17403
  const dir = path.join(workspacesRoot, ".pending_completions");
17348
17404
  await mkdir(dir, { recursive: true, mode: 448 });
@@ -17395,6 +17451,46 @@ async function runSession(input) {
17395
17451
  const client = new DaemonClient(serverURL);
17396
17452
  const backend = createBackend(provider, cliPath);
17397
17453
  const { workDir, timelineDir, env } = prepare({ workspacesRoot }, task);
17454
+ const agentBaseDir = path.dirname(timelineDir);
17455
+ await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, undefined, process.pid, provider, task.contextKey, input.logFilePath));
17456
+ let killed = false;
17457
+ const earlyOnKill = async () => {
17458
+ if (killed)
17459
+ return;
17460
+ killed = true;
17461
+ log.info(`killed by signal (messages=0, tools=0)`);
17462
+ await cleanupAttachments(task.id);
17463
+ const intent = readKillIntent(agentBaseDir, task.id);
17464
+ clearKillIntent(agentBaseDir, task.id);
17465
+ if (intent?.reason === "superseded") {
17466
+ updateEntry(timelineDir, task.id, (entry) => {
17467
+ entry.pid = null;
17468
+ entry.status = "superseded";
17469
+ entry.successor_task_id = intent.successorTaskId ?? null;
17470
+ entry.supersede_reason = "superseded by newer task";
17471
+ });
17472
+ try {
17473
+ await client.supersedeTask(token, task.id);
17474
+ } catch {}
17475
+ } else if (intent?.reason === "cancelled") {
17476
+ updateEntry(timelineDir, task.id, (entry) => {
17477
+ entry.pid = null;
17478
+ entry.status = "cancelled";
17479
+ entry.errmsg = "cancelled by user";
17480
+ });
17481
+ 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);
17482
+ } else {
17483
+ updateEntry(timelineDir, task.id, (entry) => {
17484
+ entry.pid = null;
17485
+ entry.status = "killed";
17486
+ entry.errmsg = "killed by signal";
17487
+ });
17488
+ 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);
17489
+ }
17490
+ process.exit(1);
17491
+ };
17492
+ process.on("SIGTERM", earlyOnKill);
17493
+ process.on("SIGINT", earlyOnKill);
17398
17494
  const attachmentIds = task.context?.attachment_ids ?? [];
17399
17495
  let attachments;
17400
17496
  if (attachmentIds.length > 0) {
@@ -17406,7 +17502,14 @@ async function runSession(input) {
17406
17502
  await cleanupAttachments(task.id);
17407
17503
  const errMsg = `failed to download attachments: ${e}`;
17408
17504
  log.error(errMsg);
17505
+ updateEntry(timelineDir, task.id, (entry) => {
17506
+ entry.pid = null;
17507
+ entry.status = "failed";
17508
+ entry.errmsg = errMsg;
17509
+ });
17409
17510
  await client.failTask(token, task.id, errMsg);
17511
+ process.removeListener("SIGTERM", earlyOnKill);
17512
+ process.removeListener("SIGINT", earlyOnKill);
17410
17513
  return;
17411
17514
  }
17412
17515
  }
@@ -17426,7 +17529,9 @@ async function runSession(input) {
17426
17529
  const earlySessionId = await session2.sessionId;
17427
17530
  log.info(`agent started (pid=${agentPid ?? "unknown"}, session=${earlySessionId})`);
17428
17531
  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));
17532
+ updateEntry(timelineDir, task.id, (entry) => {
17533
+ entry.session_id = earlySessionId || null;
17534
+ });
17430
17535
  const pendingMessages = [];
17431
17536
  let seq = 0;
17432
17537
  let toolCount = 0;
@@ -17443,8 +17548,8 @@ async function runSession(input) {
17443
17548
  }
17444
17549
  };
17445
17550
  const flushTimer = setInterval(flushMessages, FLUSH_INTERVAL_MS);
17446
- let killed = false;
17447
- const agentBaseDir = path.dirname(timelineDir);
17551
+ process.removeListener("SIGTERM", earlyOnKill);
17552
+ process.removeListener("SIGINT", earlyOnKill);
17448
17553
  const onKill = async () => {
17449
17554
  if (killed)
17450
17555
  return;
@@ -17531,7 +17636,11 @@ async function runSession(input) {
17531
17636
  });
17532
17637
  if (msg.type === "tool-use")
17533
17638
  toolCount++;
17534
- log.info(JSON.stringify({ role: "assistant", ...msg }));
17639
+ if (msg.type === "tool-result" && msg.output && msg.output.length > 500) {
17640
+ log.info(JSON.stringify({ role: "assistant", ...msg, output: msg.output.slice(0, 500) + `... (${msg.output.length} chars)` }));
17641
+ } else {
17642
+ log.info(JSON.stringify({ role: "assistant", ...msg }));
17643
+ }
17535
17644
  if (msg.type === "text" && msg.content) {
17536
17645
  updateEntry(timelineDir, task.id, (entry) => {
17537
17646
  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.26",
3
+ "version": "0.0.28",
4
4
  "description": "Alook CLI — Enable Your Person Colleague",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/alookai/alook#readme",