@alook/cli 0.0.60 → 0.0.62

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.
@@ -391,7 +391,8 @@ function ensureChrome() {
391
391
  return installed;
392
392
  }
393
393
  // daemon/meeting-runner.ts
394
- import { join as join2 } from "path";
394
+ import { join as join3 } from "path";
395
+ import { mkdirSync as mkdirSync2 } from "fs";
395
396
 
396
397
  // lib/platform.ts
397
398
  import { tmpdir } from "os";
@@ -521,8 +522,170 @@ function createLogger(opts) {
521
522
  }
522
523
  var log = createLogger();
523
524
 
525
+ // daemon/execenv/timeline.ts
526
+ import { appendFileSync, readFileSync, writeFileSync, renameSync } from "fs";
527
+ import { join as join2 } from "path";
528
+
529
+ // daemon/execenv/filelock.ts
530
+ import { mkdirSync, rmdirSync, statSync } from "fs";
531
+ var DEFAULT_STALE_MS = 3600000;
532
+ function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
533
+ try {
534
+ mkdirSync(lockPath);
535
+ return true;
536
+ } catch {
537
+ try {
538
+ const stat = statSync(lockPath);
539
+ if (Date.now() - stat.mtimeMs > staleMs) {
540
+ rmdirSync(lockPath);
541
+ try {
542
+ mkdirSync(lockPath);
543
+ return true;
544
+ } catch {
545
+ return false;
546
+ }
547
+ }
548
+ } catch {
549
+ try {
550
+ mkdirSync(lockPath);
551
+ return true;
552
+ } catch {
553
+ return false;
554
+ }
555
+ }
556
+ return false;
557
+ }
558
+ }
559
+ function releaseLock(lockPath) {
560
+ try {
561
+ rmdirSync(lockPath);
562
+ } catch {}
563
+ }
564
+
565
+ // daemon/execenv/timeline.ts
566
+ var log2 = createLogger({ module: "timeline" });
567
+ function filenameForDate(date) {
568
+ const y = date.getFullYear();
569
+ const m = String(date.getMonth() + 1).padStart(2, "0");
570
+ const d = String(date.getDate()).padStart(2, "0");
571
+ return `${y}-${m}-${d}.jsonl`;
572
+ }
573
+ function todayFilename() {
574
+ return filenameForDate(new Date);
575
+ }
576
+ function recentFilenames(maxDays) {
577
+ const filenames = [];
578
+ const now = new Date;
579
+ for (let i = 0;i < maxDays; i++) {
580
+ const d = new Date(now);
581
+ d.setDate(d.getDate() - i);
582
+ filenames.push(filenameForDate(d));
583
+ }
584
+ return filenames;
585
+ }
586
+ function localISOString() {
587
+ const now = new Date;
588
+ const tzOffset = -now.getTimezoneOffset();
589
+ const sign = tzOffset >= 0 ? "+" : "-";
590
+ const absOffset = Math.abs(tzOffset);
591
+ const hh = String(Math.floor(absOffset / 60)).padStart(2, "0");
592
+ const mm = String(absOffset % 60).padStart(2, "0");
593
+ const y = now.getFullYear();
594
+ const mo = String(now.getMonth() + 1).padStart(2, "0");
595
+ const d = String(now.getDate()).padStart(2, "0");
596
+ const h = String(now.getHours()).padStart(2, "0");
597
+ const mi = String(now.getMinutes()).padStart(2, "0");
598
+ const s = String(now.getSeconds()).padStart(2, "0");
599
+ return `${y}-${mo}-${d}T${h}:${mi}:${s}${sign}${hh}:${mm}`;
600
+ }
601
+ function lockPathFor(timelineDir, filename) {
602
+ return join2(timelineDir, `.${filename}.lock`);
603
+ }
604
+ function initEntry(timelineDir, entry) {
605
+ const filename = todayFilename();
606
+ const filePath = join2(timelineDir, filename);
607
+ const lockPath = lockPathFor(timelineDir, filename);
608
+ try {
609
+ const acquired = acquireLock(lockPath);
610
+ if (!acquired) {
611
+ log2.debug(`Timeline initEntry: could not acquire lock for ${filename}`);
612
+ return;
613
+ }
614
+ try {
615
+ appendFileSync(filePath, JSON.stringify(entry) + `
616
+ `);
617
+ } finally {
618
+ releaseLock(lockPath);
619
+ }
620
+ } catch (err) {
621
+ log2.debug("Timeline initEntry failed", err);
622
+ }
623
+ }
624
+ function updateEntry(timelineDir, taskId, updater) {
625
+ for (const filename of recentFilenames(7)) {
626
+ const filePath = join2(timelineDir, filename);
627
+ const lockPath = lockPathFor(timelineDir, filename);
628
+ try {
629
+ const acquired = acquireLock(lockPath);
630
+ if (!acquired) {
631
+ log2.debug(`Timeline updateEntry: lock held for ${filename}, skipping`);
632
+ continue;
633
+ }
634
+ try {
635
+ let content;
636
+ try {
637
+ content = readFileSync(filePath, "utf-8");
638
+ } catch {
639
+ continue;
640
+ }
641
+ const lines = content.trimEnd().split(`
642
+ `);
643
+ let found = false;
644
+ const updated = lines.map((line) => {
645
+ const entry = JSON.parse(line);
646
+ if (entry.task_id === taskId) {
647
+ found = true;
648
+ updater(entry);
649
+ }
650
+ return JSON.stringify(entry);
651
+ });
652
+ if (!found)
653
+ continue;
654
+ const tmpPath = join2(timelineDir, `.${filename}.tmp`);
655
+ writeFileSync(tmpPath, updated.join(`
656
+ `) + `
657
+ `);
658
+ renameSync(tmpPath, filePath);
659
+ return;
660
+ } finally {
661
+ releaseLock(lockPath);
662
+ }
663
+ } catch (err) {
664
+ log2.debug(`Timeline updateEntry failed for ${filename}`, err);
665
+ }
666
+ }
667
+ log2.debug(`Timeline updateEntry: task_id ${taskId} not found in last 7 days`);
668
+ }
669
+ function createTimelineEntry(taskId, prompt, type, sessionId, pid, provider, contextKey, detailedLog) {
670
+ return {
671
+ task_id: taskId,
672
+ context_key: contextKey ?? null,
673
+ session_id: sessionId || null,
674
+ pid: pid ?? null,
675
+ status: "running",
676
+ datetime: localISOString(),
677
+ type,
678
+ prompt,
679
+ agent_responses: [],
680
+ errmsg: null,
681
+ provider: provider ?? null,
682
+ detailed_log: detailedLog ?? null
683
+ };
684
+ }
685
+ var RESUME_MAX_AGE_MS = 72 * 60 * 60 * 1000;
686
+
524
687
  // daemon/meeting-runner.ts
525
- var log2 = createLogger({ module: "meeting-runner" });
688
+ var log3 = createLogger({ module: "meeting-runner" });
526
689
  var SCRAPE_INTERVAL_MS = 3000;
527
690
  var DEFAULT_BOT_NAME = "Alook Meeting Bot";
528
691
  var MAX_RETRY_DURATION_MS = 30 * 60 * 1000;
@@ -544,9 +707,9 @@ async function callbackWeb(input, status, transcript, error) {
544
707
  },
545
708
  body: payload
546
709
  });
547
- log2.info(`callback ${status} → HTTP ${res.status}`, { meeting: input.meetingId });
710
+ log3.info(`callback ${status} → HTTP ${res.status}`, { meeting: input.meetingId });
548
711
  } catch (err) {
549
- log2.error(`callback failed: ${err instanceof Error ? err.message : err}`, { meeting: input.meetingId });
712
+ log3.error(`callback failed: ${err instanceof Error ? err.message : err}`, { meeting: input.meetingId });
550
713
  }
551
714
  }
552
715
  function launchBrowser(chromePath) {
@@ -576,7 +739,7 @@ async function tryJoinAndRecord(input, chromePath) {
576
739
  try {
577
740
  const botName = input.agentName ? `${input.agentName} (Alook)` : DEFAULT_BOT_NAME;
578
741
  await joinMeeting(page, input.meetingUrl, botName);
579
- log2.info("joined meeting, waiting for UI ready...", { meeting: input.meetingId });
742
+ log3.info("joined meeting, waiting for UI ready...", { meeting: input.meetingId });
580
743
  await waitForMeetingReady(page);
581
744
  await page.evaluate(() => {
582
745
  for (const btn of document.querySelectorAll("button")) {
@@ -586,11 +749,11 @@ async function tryJoinAndRecord(input, chromePath) {
586
749
  }
587
750
  }
588
751
  });
589
- log2.info("meeting ready, enabling captions...", { meeting: input.meetingId });
752
+ log3.info("meeting ready, enabling captions...", { meeting: input.meetingId });
590
753
  await enableCaptions(page);
591
754
  await page.evaluate(buildCaptionObserverScript());
592
755
  await page.evaluate(buildAloneDetectorScript());
593
- log2.info("captions enabled, scraping loop started", { meeting: input.meetingId });
756
+ log3.info("captions enabled, scraping loop started", { meeting: input.meetingId });
594
757
  let scrapeCount = 0;
595
758
  while (true) {
596
759
  try {
@@ -600,9 +763,9 @@ async function tryJoinAndRecord(input, chromePath) {
600
763
  const finalCaptions = parseCaptionElements(finalRaw);
601
764
  if (finalCaptions.length > 0) {
602
765
  transcript = deduplicateCaptions(transcript, finalCaptions, meetingStartMs, Date.now());
603
- log2.debug(`final scrape: ${finalCaptions.length} caption(s), total ${transcript.length}`, { meeting: input.meetingId });
766
+ log3.debug(`final scrape: ${finalCaptions.length} caption(s), total ${transcript.length}`, { meeting: input.meetingId });
604
767
  }
605
- log2.info("meeting ended (no longer active)", { meeting: input.meetingId });
768
+ log3.info("meeting ended (no longer active)", { meeting: input.meetingId });
606
769
  break;
607
770
  }
608
771
  const rawElements = await page.evaluate(buildCaptionScrapeScript());
@@ -612,13 +775,13 @@ async function tryJoinAndRecord(input, chromePath) {
612
775
  const prevLen = transcript.length;
613
776
  transcript = deduplicateCaptions(transcript, captions, meetingStartMs, Date.now());
614
777
  if (transcript.length > prevLen) {
615
- log2.debug(`caption: ${captions[captions.length - 1].speaker}: "${captions[captions.length - 1].text}" (total ${transcript.length})`, { meeting: input.meetingId });
778
+ log3.debug(`caption: ${captions[captions.length - 1].speaker}: "${captions[captions.length - 1].text}" (total ${transcript.length})`, { meeting: input.meetingId });
616
779
  }
617
780
  } else if (scrapeCount <= 5) {
618
- log2.debug(`scrape #${scrapeCount}: no captions yet`, { meeting: input.meetingId });
781
+ log3.debug(`scrape #${scrapeCount}: no captions yet`, { meeting: input.meetingId });
619
782
  }
620
783
  } catch (err) {
621
- log2.error(`scrape error: ${err instanceof Error ? err.message : err}`, { meeting: input.meetingId });
784
+ log3.error(`scrape error: ${err instanceof Error ? err.message : err}`, { meeting: input.meetingId });
622
785
  break;
623
786
  }
624
787
  await new Promise((resolve) => setTimeout(resolve, SCRAPE_INTERVAL_MS));
@@ -628,27 +791,53 @@ async function tryJoinAndRecord(input, chromePath) {
628
791
  } catch (err) {
629
792
  const msg = err instanceof Error ? err.message : String(err);
630
793
  if (msg.includes("Blocked from joining")) {
631
- const screenshotPath = join2(tempDir("alook-meetings"), `meeting-${input.meetingId}-blocked.png`);
794
+ const screenshotPath = join3(tempDir("alook-meetings"), `meeting-${input.meetingId}-blocked.png`);
632
795
  await page.screenshot({ path: screenshotPath }).catch(() => {});
633
- log2.warn(`blocked from joining, screenshot saved: ${screenshotPath}`, { meeting: input.meetingId });
796
+ log3.warn(`blocked from joining, screenshot saved: ${screenshotPath}`, { meeting: input.meetingId });
634
797
  return { status: "blocked", transcript, error: msg };
635
798
  }
636
- log2.error(`unexpected error: ${msg}`, { meeting: input.meetingId });
799
+ log3.error(`unexpected error: ${msg}`, { meeting: input.meetingId });
637
800
  return { status: "error", transcript, error: msg };
638
801
  } finally {
639
802
  await page.close().catch(() => {});
640
803
  await browser.close().catch(() => {});
641
804
  }
642
805
  }
806
+ function writeTimeline(input, status, responses, errmsg) {
807
+ if (!input.timelineDir)
808
+ return;
809
+ try {
810
+ mkdirSync2(input.timelineDir, { recursive: true });
811
+ const taskId = `meeting-${input.meetingId}`;
812
+ if (status === "running") {
813
+ const meetingLabel = input.title || input.meetingUrl;
814
+ const entry = createTimelineEntry(taskId, `Meeting: ${meetingLabel} (participants: ${input.participants.join(", ")})`, "meeting", undefined, process.pid);
815
+ initEntry(input.timelineDir, entry);
816
+ } else {
817
+ updateEntry(input.timelineDir, taskId, (entry) => {
818
+ entry.status = status;
819
+ entry.pid = null;
820
+ if (responses)
821
+ entry.agent_responses = responses;
822
+ if (errmsg)
823
+ entry.errmsg = errmsg;
824
+ });
825
+ }
826
+ } catch (err) {
827
+ log3.debug(`timeline write failed: ${err instanceof Error ? err.message : err}`);
828
+ }
829
+ }
643
830
  async function run(input) {
644
- log2.info(`starting meeting: url=${input.meetingUrl}`, { meeting: input.meetingId, workspace: input.workspaceId });
831
+ log3.info(`starting meeting: url=${input.meetingUrl}`, { meeting: input.meetingId, workspace: input.workspaceId });
832
+ writeTimeline(input, "running");
645
833
  let chromePath;
646
834
  try {
647
835
  chromePath = ensureChrome();
648
- log2.debug(`chrome found: ${chromePath}`);
836
+ log3.debug(`chrome found: ${chromePath}`);
649
837
  } catch (err) {
650
838
  const msg = err instanceof Error ? err.message : String(err);
651
- log2.error(`chrome setup failed: ${msg}`, { meeting: input.meetingId });
839
+ log3.error(`chrome setup failed: ${msg}`, { meeting: input.meetingId });
840
+ writeTimeline(input, "failed", undefined, `Chrome setup failed: ${msg}`);
652
841
  await callbackWeb(input, "failed", undefined, `Chrome setup failed: ${msg}`);
653
842
  process.exit(1);
654
843
  }
@@ -656,26 +845,33 @@ async function run(input) {
656
845
  let attempt = 0;
657
846
  while (true) {
658
847
  attempt++;
659
- log2.info(attempt > 1 ? `retry attempt #${attempt}` : "launching browser (en-US, stealth)...", { meeting: input.meetingId });
848
+ log3.info(attempt > 1 ? `retry attempt #${attempt}` : "launching browser (en-US, stealth)...", { meeting: input.meetingId });
660
849
  const result = await tryJoinAndRecord(input, chromePath);
661
850
  if (result.status === "completed") {
662
851
  const transcriptText = formatTranscript(result.transcript);
663
- log2.info(`completed: ${result.transcript.length} transcript entries`, { meeting: input.meetingId });
852
+ log3.info(`completed: ${result.transcript.length} transcript entries`, { meeting: input.meetingId });
853
+ const transcriptR2Key = `meetings/${input.meetingId}/transcript`;
854
+ writeTimeline(input, "completed", [
855
+ `Meeting completed. ${result.transcript.length} transcript entries captured.`,
856
+ `Transcript stored at: ${transcriptR2Key}`
857
+ ]);
664
858
  await callbackWeb(input, "completed", transcriptText);
665
859
  return;
666
860
  }
667
861
  if (result.status === "blocked") {
668
862
  const elapsed = Date.now() - startTime;
669
863
  if (elapsed >= MAX_RETRY_DURATION_MS) {
670
- log2.warn(`giving up after ${Math.round(elapsed / 60000)}min of retries`, { meeting: input.meetingId });
864
+ log3.warn(`giving up after ${Math.round(elapsed / 60000)}min of retries`, { meeting: input.meetingId });
865
+ writeTimeline(input, "failed", undefined, result.error);
671
866
  await callbackWeb(input, "failed", undefined, result.error);
672
867
  return;
673
868
  }
674
869
  const backoff = RETRY_BACKOFF[Math.min(attempt - 1, RETRY_BACKOFF.length - 1)];
675
- log2.info(`blocked, retrying in ${backoff / 1000}s (attempt=${attempt}, elapsed=${Math.round(elapsed / 60000)}min)`, { meeting: input.meetingId });
870
+ log3.info(`blocked, retrying in ${backoff / 1000}s (attempt=${attempt}, elapsed=${Math.round(elapsed / 60000)}min)`, { meeting: input.meetingId });
676
871
  await new Promise((resolve) => setTimeout(resolve, backoff));
677
872
  continue;
678
873
  }
874
+ writeTimeline(input, "failed", undefined, result.error);
679
875
  await callbackWeb(input, "failed", undefined, result.error);
680
876
  return;
681
877
  }
@@ -687,6 +883,6 @@ if (!encoded) {
687
883
  }
688
884
  var input = JSON.parse(Buffer.from(encoded, "base64").toString("utf-8"));
689
885
  run(input).then(() => process.exit(0)).catch((err) => {
690
- log2.error(`fatal: ${err instanceof Error ? err.message : err}`);
886
+ log3.error(`fatal: ${err instanceof Error ? err.message : err}`);
691
887
  process.exit(1);
692
888
  });
@@ -11542,7 +11542,7 @@ function finalize(ctx, schema) {
11542
11542
  result.$schema = "http://json-schema.org/draft-07/schema#";
11543
11543
  } else if (ctx.target === "draft-04") {
11544
11544
  result.$schema = "http://json-schema.org/draft-04/schema#";
11545
- } else if (ctx.target === "openapi-3.0") {} else {}
11545
+ } else if (ctx.target === "openapi-3.0") {}
11546
11546
  if (ctx.external?.uri) {
11547
11547
  const id = ctx.external.registry.get(schema)?.id;
11548
11548
  if (!id)
@@ -11786,7 +11786,7 @@ var literalProcessor = (schema, ctx, json, _params) => {
11786
11786
  if (val === undefined) {
11787
11787
  if (ctx.unrepresentable === "throw") {
11788
11788
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
11789
- } else {}
11789
+ }
11790
11790
  } else if (typeof val === "bigint") {
11791
11791
  if (ctx.unrepresentable === "throw") {
11792
11792
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -14449,7 +14449,9 @@ var PollMeetingItemSchema = exports_external.object({
14449
14449
  meeting_url: exports_external.string(),
14450
14450
  participants: exports_external.array(exports_external.string()),
14451
14451
  workspace_id: exports_external.string(),
14452
- agent_name: exports_external.string()
14452
+ agent_id: exports_external.string(),
14453
+ agent_name: exports_external.string(),
14454
+ title: exports_external.string().optional()
14453
14455
  });
14454
14456
  var PollResponseSchema = exports_external.object({
14455
14457
  tasks: exports_external.array(TaskApiSchema),
@@ -14484,6 +14486,7 @@ var RegisterDaemonRequestSchema = exports_external.object({
14484
14486
  daemon_id: exports_external.string().min(1),
14485
14487
  device_name: exports_external.string().optional().default(""),
14486
14488
  cli_version: exports_external.string().optional().default(""),
14489
+ workspaces_root: exports_external.string().optional().default(""),
14487
14490
  runtimes: exports_external.array(DaemonRuntimeItemSchema).min(1)
14488
14491
  });
14489
14492
  var DeregisterRequestSchema = exports_external.object({
@@ -14692,7 +14695,8 @@ var EmailNotifyRequestSchema = exports_external.object({
14692
14695
  meetingInfo: MeetingInfoSchema.nullable().optional(),
14693
14696
  attachments: exports_external.string().optional(),
14694
14697
  traceId: exports_external.string().optional(),
14695
- sourceTaskId: exports_external.string().optional()
14698
+ sourceTaskId: exports_external.string().optional(),
14699
+ isInternal: exports_external.boolean().optional().default(false)
14696
14700
  });
14697
14701
  var CreateEmailAccountSchema = exports_external.object({
14698
14702
  emailAddress: exports_external.string().email("valid email required"),
@@ -16591,6 +16595,17 @@ var machineToken = sqliteTable("machine_token", {
16591
16595
  lastUsedAt: text("last_used_at"),
16592
16596
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
16593
16597
  }, (t) => [index("idx_machine_token").on(t.token)]);
16598
+ var messageFlag = sqliteTable("message_flag", {
16599
+ id: text("id").primaryKey().$defaultFn(() => nanoid3()),
16600
+ messageId: text("message_id").notNull().references(() => message.id, { onDelete: "cascade" }),
16601
+ userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
16602
+ workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
16603
+ createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
16604
+ }, (t) => [
16605
+ unique("message_flag_message_user").on(t.messageId, t.userId),
16606
+ index("idx_message_flag_ws_user_created").on(t.workspaceId, t.userId, t.createdAt),
16607
+ index("idx_message_flag_message_user").on(t.messageId, t.userId)
16608
+ ]);
16594
16609
  var conversationMap = sqliteTable("conversation_map", {
16595
16610
  id: text("id").primaryKey().$defaultFn(() => nanoid3()),
16596
16611
  key: text("key").notNull(),
@@ -16671,6 +16686,28 @@ var RESERVED_HANDLES = new Set([
16671
16686
  function toAlookAddress(h) {
16672
16687
  return `${h}${DOMAIN}`;
16673
16688
  }
16689
+ // ../shared/src/mode.ts
16690
+ function resolveMode(signals) {
16691
+ if (signals.nodeEnv === "development" && !signals.cmdPrefix)
16692
+ return "dev";
16693
+ if (signals.serverUrl && !signals.cmdPrefix)
16694
+ return "dev";
16695
+ if (signals.cmdPrefix)
16696
+ return "app";
16697
+ if (signals.hostname && ["localhost", "127.0.0.1"].includes(signals.hostname))
16698
+ return "app";
16699
+ return "production";
16700
+ }
16701
+ function cliCommand(mode) {
16702
+ switch (mode) {
16703
+ case "dev":
16704
+ return "pnpm dev:cli";
16705
+ case "app":
16706
+ return "npx @alook/app cli";
16707
+ case "production":
16708
+ return "npx @alook/cli";
16709
+ }
16710
+ }
16674
16711
  // daemon/client.ts
16675
16712
  class DaemonClient {
16676
16713
  baseURL;
@@ -17743,6 +17780,14 @@ function tempDir(subdir) {
17743
17780
  return join(tmpdir(), subdir);
17744
17781
  }
17745
17782
 
17783
+ // lib/env.ts
17784
+ function cmdPrefix() {
17785
+ return process.env.ALOOK_CMD_PREFIX || cliCommand(resolveMode({
17786
+ serverUrl: process.env.ALOOK_SERVER_URL,
17787
+ cmdPrefix: process.env.ALOOK_CMD_PREFIX
17788
+ }));
17789
+ }
17790
+
17746
17791
  // daemon/execenv/context.ts
17747
17792
  import {
17748
17793
  writeFileSync,
@@ -17881,12 +17926,12 @@ ${lines.join(`
17881
17926
 
17882
17927
  ### Emails
17883
17928
  ---
17884
- 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}/'.
17929
+ Run '${cmdPrefix()} email pull --agent_id ${task.agentId} --status unread' to download unread emails from inbox to '${tempDir("alook-emails")}/${task.workspaceId}/${task.agentId}/'.
17885
17930
  ---
17886
- To download sent emails, add '--folder sent': 'npx @alook/cli email pull --agent_id ${task.agentId} --folder sent'
17931
+ To download sent emails, add '--folder sent': '${cmdPrefix()} email pull --agent_id ${task.agentId} --folder sent'
17887
17932
  Valid folders: inbox (default), sent, untrust.
17888
17933
  To limit the number of emails downloaded, add '--limit <N>' (e.g. '--limit 20'). Use '--offset <N>' to skip emails for pagination.
17889
- Example: 'npx @alook/cli email pull --agent_id ${task.agentId} --status unread --limit 20 --offset 0'
17934
+ Example: '${cmdPrefix()} email pull --agent_id ${task.agentId} --status unread --limit 20 --offset 0'
17890
17935
  ---
17891
17936
  Each email is saved to '${tempDir("alook-emails")}/${task.workspaceId}/${task.agentId}/<emailId>/' with:
17892
17937
  - 'metadata.json' — sender, recipient, subject, date, status, message_id, in_reply_to, references
@@ -17895,40 +17940,40 @@ Each email is saved to '${tempDir("alook-emails")}/${task.workspaceId}/${task.ag
17895
17940
  - 'attachments/' — extracted attachment files (if any)
17896
17941
  ---
17897
17942
  Before starting to process an INBOX email, mark it as read:
17898
- - Run 'npx @alook/cli email set --agent_id ${task.agentId} --email_id <EMAIL_ID> --status read'
17943
+ - Run '${cmdPrefix()} email set --agent_id ${task.agentId} --email_id <EMAIL_ID> --status read'
17899
17944
  ---
17900
17945
 
17901
17946
  #### Sending a new email
17902
17947
  Write the HTML body to a file first, then send it. The body is forwarded as-is (HTML).
17903
- - Run 'npx @alook/cli email send --agent_id ${task.agentId} --to <ADDRESS> --subject "<SUBJECT>" --body-file <PATH_TO_HTML>'
17948
+ - Run '${cmdPrefix()} email send --agent_id ${task.agentId} --to <ADDRESS> --subject "<SUBJECT>" --body-file <PATH_TO_HTML>'
17904
17949
  - To send from a specific mailbox, add '--from <YOUR_EMAIL_ADDRESS>'. Without '--from', the default Alook address is used.
17905
17950
  - Attach files with '--attachment <PATH>' — repeat the flag for multiple attachments. Each file is uploaded before sending.
17906
- - Example: 'npx @alook/cli email send --agent_id ${task.agentId} --to foo@bar.com --subject "Weekly report" --body-file /tmp/body.html --from alice@company.com --attachment /tmp/report.pdf'
17951
+ - Example: '${cmdPrefix()} email send --agent_id ${task.agentId} --to foo@bar.com --subject "Weekly report" --body-file /tmp/body.html --from alice@company.com --attachment /tmp/report.pdf'
17907
17952
 
17908
17953
  #### Replying to an email
17909
17954
  To reply to an email, add '--in-reply-to <EMAIL_ID>' to the send command. This sets the correct email threading headers so the recipient's email client groups the reply into the same conversation thread.
17910
17955
  - Use 'Re: <original subject>' as the subject.
17911
17956
  - Quote the original email body in your reply (wrap it in a blockquote).
17912
17957
  - The <EMAIL_ID> is the Alook email id from metadata.json (not the message_id header).
17913
- - Example: 'npx @alook/cli email send --agent_id ${task.agentId} --to sender@example.com --subject "Re: Bug report" --body-file /tmp/reply.html --in-reply-to <EMAIL_ID>'
17958
+ - Example: '${cmdPrefix()} email send --agent_id ${task.agentId} --to sender@example.com --subject "Re: Bug report" --body-file /tmp/reply.html --in-reply-to <EMAIL_ID>'
17914
17959
  Tips:
17915
17960
  - 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.
17916
17961
  ---
17917
17962
 
17918
17963
  #### Forwarding an email
17919
17964
  Forward any email to a new recipient, with an optional note prepended above the original content. All original attachments are re-attached automatically.
17920
- - Run 'npx @alook/cli email forward --agent_id ${task.agentId} --email_id <EMAIL_ID> --to <RECIPIENT>'
17965
+ - Run '${cmdPrefix()} email forward --agent_id ${task.agentId} --email_id <EMAIL_ID> --to <RECIPIENT>'
17921
17966
  - Add '--note "FYI, see the request below."' to prepend a note above the forwarded body.
17922
17967
  - Add '--from <YOUR_EMAIL_ADDRESS>' to send from a specific mailbox.
17923
17968
  - Add '--attachment <PATH>' to attach extra files (repeatable).
17924
- - Example: 'npx @alook/cli email forward --agent_id ${task.agentId} --email_id em_abc --to boss@company.com --note "FYI" --attachment /tmp/summary.pdf'
17969
+ - Example: '${cmdPrefix()} email forward --agent_id ${task.agentId} --email_id em_abc --to boss@company.com --note "FYI" --attachment /tmp/summary.pdf'
17925
17970
  ---
17926
17971
 
17927
17972
  #### Email Whitelist (Allowed Senders)
17928
17973
  Manage which email addresses are allowed to send you emails.
17929
- - List: 'npx @alook/cli email whitelist list --agent_id ${task.agentId}' (add '--json' for machine-readable output)
17930
- - Add: 'npx @alook/cli email whitelist add --agent_id ${task.agentId} <EMAIL_ADDRESS>'
17931
- - Remove: 'npx @alook/cli email whitelist delete --agent_id ${task.agentId} <EMAIL_ADDRESS>'
17974
+ - List: '${cmdPrefix()} email whitelist list --agent_id ${task.agentId}' (add '--json' for machine-readable output)
17975
+ - Add: '${cmdPrefix()} email whitelist add --agent_id ${task.agentId} <EMAIL_ADDRESS>'
17976
+ - Remove: '${cmdPrefix()} email whitelist delete --agent_id ${task.agentId} <EMAIL_ADDRESS>'
17932
17977
  ---
17933
17978
  `;
17934
17979
  }
@@ -17936,7 +17981,7 @@ Manage which email addresses are allowed to send you emails.
17936
17981
  ### Artifacts
17937
17982
  Upload files for your owner to review in the app.
17938
17983
  - Your current conversation id is available via env var: $ALOOK_CONVERSATION_ID
17939
- - Run 'npx @alook/cli sync upload-artifact --agent_id ${task.agentId} --conversation_id $ALOOK_CONVERSATION_ID --file <PATH>'
17984
+ - Run '${cmdPrefix()} sync upload-artifact --agent_id ${task.agentId} --conversation_id $ALOOK_CONVERSATION_ID --file <PATH>'
17940
17985
  - Use this after generating plans, reports, or any file the owner should review.
17941
17986
  - You response will be rendered in remote server, so don't output link format with local path in your response (cause user can click it and jump to nowheres)
17942
17987
  - If you think user may need to know any file detail, use upload-artifact tool to send the file to user.
@@ -17958,26 +18003,26 @@ Schedule future tasks for yourself. At the scheduled time, a new task is dispatc
17958
18003
  Keep the event title informative and concise, less than 20 words.
17959
18004
  Place the event details in description.
17960
18005
  Create a one-off event:
17961
- - Run 'npx @alook/cli calendar set --agent_id ${task.agentId} --event_title "<TASK_TITLE>" --description "<TASK_BODY>" --datetime <YYYY-MM-DDTHH:MM>'
18006
+ - Run '${cmdPrefix()} calendar set --agent_id ${task.agentId} --event_title "<TASK_TITLE>" --description "<TASK_BODY>" --datetime <YYYY-MM-DDTHH:MM>'
17962
18007
  - '--datetime' is LOCAL time, format 'YYYY-MM-DDTHH:MM' (e.g. '2026-04-17T09:30'). Do NOT pass UTC / ISO strings with 'Z'.
17963
18008
  - '--event_title' becomes the task prompt when the event fires — write it as the instruction you want future-you to receive.
17964
18009
 
17965
18010
  Create a repeating event:
17966
18011
  - Add '--repeat <interval>' where interval is like '1day', '2hour', '1week', '1month'.
17967
18012
  - Optionally add '--repeat_stop_date <YYYY-MM-DD>' to stop the recurrence (local date).
17968
- - Example: 'npx @alook/cli calendar set --agent_id ${task.agentId} --event_title "<REPEAT_TASK_TITLE>" --description "<REPEAT_TASK_BODY>" --datetime 2026-04-18T09:00 --repeat 1day --repeat_stop_date 2026-05-18'
18013
+ - Example: '${cmdPrefix()} calendar set --agent_id ${task.agentId} --event_title "<REPEAT_TASK_TITLE>" --description "<REPEAT_TASK_BODY>" --datetime 2026-04-18T09:00 --repeat 1day --repeat_stop_date 2026-05-18'
17969
18014
  ---
17970
18015
  List upcoming events:
17971
- - Run 'npx @alook/cli calendar list --agent_id ${task.agentId}' (defaults: next 30 days, past 0 days).
18016
+ - Run '${cmdPrefix()} calendar list --agent_id ${task.agentId}' (defaults: next 30 days, past 0 days).
17972
18017
  - Tune the window with '--future_days <N>' and '--past_days <N>'. Add '--json' for machine-readable output.
17973
18018
  - 'list' shows a '[has description]' badge instead of the full description — use 'show' (below) to read it.
17974
18019
 
17975
18020
  Show full detail of one event (use this to read the description):
17976
- - Run 'npx @alook/cli calendar show --agent_id ${task.agentId} --event_id <EVENT_ID>'
18021
+ - Run '${cmdPrefix()} calendar show --agent_id ${task.agentId} --event_id <EVENT_ID>'
17977
18022
  - Add '--json' for machine-readable output.
17978
18023
 
17979
18024
  Edit an existing event (preserves event id and recurring state):
17980
- - Run 'npx @alook/cli calendar update --agent_id ${task.agentId} --event_id <EVENT_ID> [flags]'
18025
+ - Run '${cmdPrefix()} calendar update --agent_id ${task.agentId} --event_id <EVENT_ID> [flags]'
17981
18026
  - Supply only the fields you want to change. Available flags:
17982
18027
  - '--event_title "<t>"' — rename the event / change the fire-time prompt
17983
18028
  - '--description "<d>"' to set, or '--clear_description' to remove
@@ -17987,7 +18032,7 @@ Edit an existing event (preserves event id and recurring state):
17987
18032
  - Passing no mutating flag is an error. Do NOT use 'delete' + 'set' to edit — that loses the event id and the recurring 'last fired' state.
17988
18033
 
17989
18034
  Delete an event:
17990
- - Run 'npx @alook/cli calendar delete --agent_id ${task.agentId} --event_id <EVENT_ID>'
18035
+ - Run '${cmdPrefix()} calendar delete --agent_id ${task.agentId} --event_id <EVENT_ID>'
17991
18036
  ---
17992
18037
  `;
17993
18038
  return content;
@@ -18410,7 +18455,7 @@ function clearKillIntent(baseDir, taskId) {
18410
18455
  // daemon/prompt.ts
18411
18456
  var DM_RESPONSE_NOTICE = "IMPORTANT: Only your final text response is visible to the user." + " Tool calls, intermediate reasoning, and mid-process outputs are NOT displayed." + " Put all key information, answers, and conclusions in your final response — that is the only thing the user will read.";
18412
18457
  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.";
18413
- var ISSUE_NOTICE = "This task was triggered by an assigned issue. The issue_id is provided in this message." + " Use `alook issue show --agent_id <your_agent_id> --issue_id <issue_id>` to read full context." + " Use `alook issue update --agent_id <your_agent_id> --issue_id <issue_id> --status <status>` to change status." + " Use `alook issue comment --agent_id <your_agent_id> --issue_id <issue_id> --body <text>` to leave a comment." + " CRITICAL — You MUST update the issue status before you finish. This is NOT optional:" + " 1. Set status to 'in_progress' when you start working." + " 2. Set status to 'review' as your LAST action before exiting this means your work is done and ready for the owner to review. You cannot improve the issue further without user feedback." + " If you delegated work to colleagues, wait for their responses, then set 'review' once everything is complete." + " NEVER exit without updating the status. A task left in 'in_progress' is a failed task." + " Always leave a comment summarizing what you did before changing status to 'review'.";
18458
+ var ISSUE_NOTICE = "This task was triggered by an assigned issue. The issue_id is provided in this message." + " Use `alook issue show --agent_id <your_agent_id> --issue_id <issue_id>` to read full context." + " Use `alook issue update --agent_id <your_agent_id> --issue_id <issue_id> --status <status>` to change status." + " Use `alook issue comment --agent_id <your_agent_id> --issue_id <issue_id> --body <text>` to leave a comment." + " CRITICAL — You MUST manage the issue status correctly. This is NOT optional:" + " 1. Set status to 'in_progress' when you start working." + " 2. If you complete the work yourself: leave a summary comment, then set status to 'review' as your last action. 'review' means there is actual completed work (code, artifact, result) ready for the owner to look at." + " 3. If you delegated work to colleagues and are waiting for their response: KEEP status as 'in_progress' and exit. This is expected — you will be woken up when they reply. Set 'review' only after all delegated work is confirmed complete." + " 4. NEVER set 'review' unless there is concrete completed work for the owner to review. Sending a plan to a colleague is NOT completed work." + " NEVER exit without doing at least one of: updating the status, or leaving a comment explaining what you did and what you're waiting for.";
18414
18459
  function buildDmNotice(name, email3) {
18415
18460
  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.`;
18416
18461
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/cli",
3
- "version": "0.0.60",
3
+ "version": "0.0.62",
4
4
  "description": "Alook CLI — Enable Your Person Colleague",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/alookai/alook#readme",
@@ -41,6 +41,7 @@
41
41
  "build": "bun build src/index.ts --outdir dist --target node --format esm --external citty --external commander --external postal-mime --external playwright-core && bun build daemon/session-runner.ts --outdir dist --target node --format esm --external citty --external commander --external postal-mime && bun build daemon/meeting-runner.ts --outdir dist --target node --format esm --external playwright-core && node scripts/prepare-dist.mjs",
42
42
  "prepack": "pnpm run build",
43
43
  "test": "vitest run",
44
+ "lint": "eslint .",
44
45
  "typecheck": "tsc --noEmit -p tsconfig.build.json"
45
46
  },
46
47
  "dependencies": {
@@ -52,7 +53,9 @@
52
53
  "devDependencies": {
53
54
  "@alook/shared": "workspace:*",
54
55
  "@types/bun": "^1.3.13",
56
+ "eslint": "^10.3.0",
55
57
  "typescript": "^6.0.3",
58
+ "typescript-eslint": "^8.59.3",
56
59
  "vitest": "^4.1.5"
57
60
  }
58
61
  }