@alook/cli 0.0.59 → 0.0.61
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/README.md +118 -17
- package/dist/index.js +556 -504
- package/dist/meeting-runner.js +219 -23
- package/dist/session-runner.js +71 -26
- package/package.json +4 -1
package/dist/meeting-runner.js
CHANGED
|
@@ -391,7 +391,8 @@ function ensureChrome() {
|
|
|
391
391
|
return installed;
|
|
392
392
|
}
|
|
393
393
|
// daemon/meeting-runner.ts
|
|
394
|
-
import { join as
|
|
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
|
|
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
|
-
|
|
710
|
+
log3.info(`callback ${status} → HTTP ${res.status}`, { meeting: input.meetingId });
|
|
548
711
|
} catch (err) {
|
|
549
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
766
|
+
log3.debug(`final scrape: ${finalCaptions.length} caption(s), total ${transcript.length}`, { meeting: input.meetingId });
|
|
604
767
|
}
|
|
605
|
-
|
|
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
|
-
|
|
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
|
-
|
|
781
|
+
log3.debug(`scrape #${scrapeCount}: no captions yet`, { meeting: input.meetingId });
|
|
619
782
|
}
|
|
620
783
|
} catch (err) {
|
|
621
|
-
|
|
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 =
|
|
794
|
+
const screenshotPath = join3(tempDir("alook-meetings"), `meeting-${input.meetingId}-blocked.png`);
|
|
632
795
|
await page.screenshot({ path: screenshotPath }).catch(() => {});
|
|
633
|
-
|
|
796
|
+
log3.warn(`blocked from joining, screenshot saved: ${screenshotPath}`, { meeting: input.meetingId });
|
|
634
797
|
return { status: "blocked", transcript, error: msg };
|
|
635
798
|
}
|
|
636
|
-
|
|
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
|
-
|
|
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
|
-
|
|
836
|
+
log3.debug(`chrome found: ${chromePath}`);
|
|
649
837
|
} catch (err) {
|
|
650
838
|
const msg = err instanceof Error ? err.message : String(err);
|
|
651
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
886
|
+
log3.error(`fatal: ${err instanceof Error ? err.message : err}`);
|
|
691
887
|
process.exit(1);
|
|
692
888
|
});
|
package/dist/session-runner.js
CHANGED
|
@@ -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") {}
|
|
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
|
-
}
|
|
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
|
-
|
|
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),
|
|
@@ -14480,10 +14482,11 @@ var ActivateTokenRequestSchema = exports_external.object({
|
|
|
14480
14482
|
runtimes: exports_external.array(ActivateTokenRuntimeSchema).min(1)
|
|
14481
14483
|
});
|
|
14482
14484
|
var RegisterDaemonRequestSchema = exports_external.object({
|
|
14483
|
-
workspace_id: exports_external.string().min(1),
|
|
14485
|
+
workspace_id: exports_external.string().min(1).optional(),
|
|
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"),
|
|
@@ -16584,13 +16588,24 @@ var meetingSession = sqliteTable("meeting_session", {
|
|
|
16584
16588
|
var machineToken = sqliteTable("machine_token", {
|
|
16585
16589
|
id: text("id").primaryKey().$defaultFn(() => nanoid3()),
|
|
16586
16590
|
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
16587
|
-
workspaceId: text("workspace_id").
|
|
16591
|
+
workspaceId: text("workspace_id").references(() => workspace.id, { onDelete: "cascade" }),
|
|
16588
16592
|
token: text("token").unique().notNull(),
|
|
16589
16593
|
name: text("name").notNull().default(""),
|
|
16590
16594
|
status: text("status").notNull().default("active"),
|
|
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 '
|
|
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': '
|
|
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: '
|
|
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 '
|
|
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 '
|
|
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: '
|
|
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: '
|
|
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 '
|
|
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: '
|
|
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: '
|
|
17930
|
-
- Add: '
|
|
17931
|
-
- Remove: '
|
|
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 '
|
|
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 '
|
|
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: '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.0.61",
|
|
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
|
}
|