@alook/cli 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +514 -68
- package/dist/session-runner.js +207 -32
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -297,14 +297,30 @@ function statusCommand() {
|
|
|
297
297
|
// commands/daemon.ts
|
|
298
298
|
import { Command as Command3 } from "commander";
|
|
299
299
|
import { spawn as spawn3 } from "child_process";
|
|
300
|
-
import { openSync as openSync2, closeSync as closeSync2, mkdirSync as
|
|
300
|
+
import { openSync as openSync2, closeSync as closeSync2, mkdirSync as mkdirSync6 } from "fs";
|
|
301
301
|
import { dirname as dirname4 } from "path";
|
|
302
302
|
|
|
303
303
|
// ../shared/src/constants.ts
|
|
304
|
+
var TaskStatus = {
|
|
305
|
+
QUEUED: "queued",
|
|
306
|
+
DISPATCHED: "dispatched",
|
|
307
|
+
RUNNING: "running",
|
|
308
|
+
COMPLETED: "completed",
|
|
309
|
+
FAILED: "failed",
|
|
310
|
+
CANCELLED: "cancelled",
|
|
311
|
+
SUPERSEDED: "superseded"
|
|
312
|
+
};
|
|
313
|
+
var TERMINAL_TASK_STATUSES = [
|
|
314
|
+
TaskStatus.COMPLETED,
|
|
315
|
+
TaskStatus.FAILED,
|
|
316
|
+
TaskStatus.CANCELLED,
|
|
317
|
+
TaskStatus.SUPERSEDED
|
|
318
|
+
];
|
|
304
319
|
var TASK_TYPES = {
|
|
305
320
|
USER_DM_MESSAGE: "user_dm_message",
|
|
306
321
|
EMAIL_NOTIFICATION: "email_notification",
|
|
307
|
-
CALENDAR_EVENT: "calendar_event"
|
|
322
|
+
CALENDAR_EVENT: "calendar_event",
|
|
323
|
+
KILL_TASK: "kill_task"
|
|
308
324
|
};
|
|
309
325
|
var POLL_INTERVAL_MS = Number(process.env.POLL_INTERVAL_MS) || 3000;
|
|
310
326
|
var OFFLINE_THRESHOLD_MS = Number(process.env.OFFLINE_THRESHOLD_MS) || 9000;
|
|
@@ -13851,7 +13867,8 @@ var TaskStatusSchema = exports_external.enum([
|
|
|
13851
13867
|
"running",
|
|
13852
13868
|
"completed",
|
|
13853
13869
|
"failed",
|
|
13854
|
-
"cancelled"
|
|
13870
|
+
"cancelled",
|
|
13871
|
+
"superseded"
|
|
13855
13872
|
]);
|
|
13856
13873
|
var ClaimedTaskRowSchema = exports_external.object({
|
|
13857
13874
|
id: exports_external.string(),
|
|
@@ -14032,8 +14049,9 @@ var UpdateAgentRequestSchema = exports_external.object({
|
|
|
14032
14049
|
description: exports_external.string().optional(),
|
|
14033
14050
|
instructions: exports_external.string().optional(),
|
|
14034
14051
|
runtime_id: exports_external.string().min(1).optional(),
|
|
14035
|
-
runtime_config: RuntimeConfigSchema
|
|
14036
|
-
|
|
14052
|
+
runtime_config: RuntimeConfigSchema,
|
|
14053
|
+
visibility: exports_external.enum(["public", "private"]).optional()
|
|
14054
|
+
}).refine((v) => v.name !== undefined || v.description !== undefined || v.instructions !== undefined || v.runtime_id !== undefined || v.runtime_config !== undefined || v.visibility !== undefined, { message: "at least one field is required" });
|
|
14037
14055
|
var CreateConversationRequestSchema = exports_external.object({
|
|
14038
14056
|
agent_id: exports_external.string().min(1, "agent_id is required")
|
|
14039
14057
|
});
|
|
@@ -14125,6 +14143,16 @@ var CreateWorkspaceRequestSchema = exports_external.object({
|
|
|
14125
14143
|
name: exports_external.string().min(1, "name is required"),
|
|
14126
14144
|
slug: exports_external.string().min(1, "slug is required")
|
|
14127
14145
|
});
|
|
14146
|
+
var UpdateWorkspaceRequestSchema = exports_external.object({
|
|
14147
|
+
name: exports_external.string().min(1, "name is required").max(100).trim().optional(),
|
|
14148
|
+
slug: exports_external.string().min(1, "slug is required").max(100).trim().toLowerCase().optional()
|
|
14149
|
+
});
|
|
14150
|
+
var DeleteWorkspaceRequestSchema = exports_external.object({
|
|
14151
|
+
confirm_name: exports_external.string().min(1, "confirm_name is required")
|
|
14152
|
+
});
|
|
14153
|
+
var GrantAgentAccessRequestSchema = exports_external.object({
|
|
14154
|
+
user_id: exports_external.string().min(1, "user_id is required")
|
|
14155
|
+
});
|
|
14128
14156
|
// ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@4.20260418.1_@opentelemetry+api@1.9.1_bun-types@1.3.12_kysely@0.28.16/node_modules/drizzle-orm/entity.js
|
|
14129
14157
|
var entityKind = Symbol.for("drizzle:entityKind");
|
|
14130
14158
|
var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
|
|
@@ -15556,6 +15584,48 @@ var member = sqliteTable("member", {
|
|
|
15556
15584
|
globalInstruction: text("global_instruction").notNull().default(""),
|
|
15557
15585
|
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15558
15586
|
}, (t) => [unique("member_workspace_user").on(t.workspaceId, t.userId)]);
|
|
15587
|
+
var workspaceInvite = sqliteTable("workspace_invite", {
|
|
15588
|
+
id: text("id").primaryKey().$defaultFn(() => "inv_" + nanoid3()),
|
|
15589
|
+
workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
|
|
15590
|
+
token: text("token").unique().notNull().$defaultFn(() => nanoid3(32)),
|
|
15591
|
+
createdBy: text("created_by").notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
15592
|
+
usedBy: text("used_by").references(() => user.id, { onDelete: "set null" }),
|
|
15593
|
+
usedAt: text("used_at"),
|
|
15594
|
+
expiresAt: text("expires_at").notNull(),
|
|
15595
|
+
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15596
|
+
}, (t) => [
|
|
15597
|
+
index("idx_workspace_invite_token").on(t.token),
|
|
15598
|
+
index("idx_workspace_invite_workspace").on(t.workspaceId)
|
|
15599
|
+
]);
|
|
15600
|
+
var agentAccess = sqliteTable("agent_access", {
|
|
15601
|
+
id: text("id").primaryKey().$defaultFn(() => nanoid3()),
|
|
15602
|
+
agentId: text("agent_id").notNull(),
|
|
15603
|
+
workspaceId: text("workspace_id").notNull(),
|
|
15604
|
+
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
15605
|
+
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15606
|
+
}, (t) => [
|
|
15607
|
+
unique("agent_access_agent_ws_user").on(t.agentId, t.workspaceId, t.userId),
|
|
15608
|
+
index("idx_agent_access_agent_ws").on(t.agentId, t.workspaceId),
|
|
15609
|
+
index("idx_agent_access_user").on(t.userId),
|
|
15610
|
+
foreignKey({
|
|
15611
|
+
columns: [t.agentId, t.workspaceId],
|
|
15612
|
+
foreignColumns: [agent.id, agent.workspaceId]
|
|
15613
|
+
}).onDelete("cascade")
|
|
15614
|
+
]);
|
|
15615
|
+
var agentPin = sqliteTable("agent_pin", {
|
|
15616
|
+
id: text("id").primaryKey().$defaultFn(() => nanoid3()),
|
|
15617
|
+
agentId: text("agent_id").notNull(),
|
|
15618
|
+
workspaceId: text("workspace_id").notNull(),
|
|
15619
|
+
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
15620
|
+
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15621
|
+
}, (t) => [
|
|
15622
|
+
unique("agent_pin_agent_ws_user").on(t.agentId, t.workspaceId, t.userId),
|
|
15623
|
+
index("idx_agent_pin_ws_user").on(t.workspaceId, t.userId),
|
|
15624
|
+
foreignKey({
|
|
15625
|
+
columns: [t.agentId, t.workspaceId],
|
|
15626
|
+
foreignColumns: [agent.id, agent.workspaceId]
|
|
15627
|
+
}).onDelete("cascade")
|
|
15628
|
+
]);
|
|
15559
15629
|
var machine = sqliteTable("machine", {
|
|
15560
15630
|
daemonId: text("daemon_id").notNull(),
|
|
15561
15631
|
workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
|
|
@@ -15693,6 +15763,7 @@ var emails = sqliteTable("emails", {
|
|
|
15693
15763
|
htmlBody: text("html_body").notNull().default(""),
|
|
15694
15764
|
attachments: text("attachments").notNull().default("[]"),
|
|
15695
15765
|
status: text("status").notNull().default("unread"),
|
|
15766
|
+
direction: text("direction").notNull().default("inbound"),
|
|
15696
15767
|
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15697
15768
|
}, (t) => [
|
|
15698
15769
|
foreignKey({
|
|
@@ -15826,16 +15897,33 @@ class DaemonClient {
|
|
|
15826
15897
|
"Content-Type": "application/json",
|
|
15827
15898
|
Authorization: `Bearer ${token}`
|
|
15828
15899
|
};
|
|
15829
|
-
const
|
|
15830
|
-
|
|
15831
|
-
|
|
15832
|
-
|
|
15833
|
-
|
|
15834
|
-
|
|
15835
|
-
|
|
15836
|
-
|
|
15837
|
-
|
|
15838
|
-
|
|
15900
|
+
const MAX_RETRIES = 3;
|
|
15901
|
+
const BASE_DELAY_MS = 500;
|
|
15902
|
+
let lastError;
|
|
15903
|
+
for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
|
|
15904
|
+
try {
|
|
15905
|
+
const res = await fetch(this.baseURL + path, {
|
|
15906
|
+
method,
|
|
15907
|
+
headers,
|
|
15908
|
+
body: body ? JSON.stringify(body) : undefined
|
|
15909
|
+
});
|
|
15910
|
+
if (!res.ok)
|
|
15911
|
+
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
|
15912
|
+
if (res.status === 204)
|
|
15913
|
+
return;
|
|
15914
|
+
return res.json();
|
|
15915
|
+
} catch (e) {
|
|
15916
|
+
if (e instanceof TypeError) {
|
|
15917
|
+
lastError = e;
|
|
15918
|
+
if (attempt < MAX_RETRIES) {
|
|
15919
|
+
await new Promise((r) => setTimeout(r, BASE_DELAY_MS * 2 ** attempt));
|
|
15920
|
+
continue;
|
|
15921
|
+
}
|
|
15922
|
+
}
|
|
15923
|
+
throw e;
|
|
15924
|
+
}
|
|
15925
|
+
}
|
|
15926
|
+
throw lastError;
|
|
15839
15927
|
}
|
|
15840
15928
|
async register(token, body) {
|
|
15841
15929
|
const raw = await this.request("POST", "/api/daemon/register", token, body);
|
|
@@ -15866,15 +15954,35 @@ class DaemonClient {
|
|
|
15866
15954
|
error: error48
|
|
15867
15955
|
});
|
|
15868
15956
|
}
|
|
15957
|
+
supersedeTask(token, taskId) {
|
|
15958
|
+
return this.request("POST", `/api/daemon/tasks/${taskId}/supersede`, token);
|
|
15959
|
+
}
|
|
15869
15960
|
async getArtifactMeta(token, artifactId, workspaceId) {
|
|
15870
15961
|
return this.request("GET", `/api/artifacts/${artifactId}?workspace_id=${encodeURIComponent(workspaceId)}`, token);
|
|
15871
15962
|
}
|
|
15872
15963
|
async downloadArtifact(token, artifactId, workspaceId) {
|
|
15873
|
-
const
|
|
15874
|
-
|
|
15875
|
-
|
|
15964
|
+
const MAX_RETRIES = 3;
|
|
15965
|
+
const BASE_DELAY_MS = 500;
|
|
15966
|
+
let lastError;
|
|
15967
|
+
for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
|
|
15968
|
+
try {
|
|
15969
|
+
const res = await fetch(`${this.baseURL}/api/artifacts/${artifactId}/content?workspace_id=${encodeURIComponent(workspaceId)}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
15970
|
+
if (!res.ok) {
|
|
15971
|
+
throw new Error(`artifact download failed: HTTP ${res.status}`);
|
|
15972
|
+
}
|
|
15973
|
+
return res.arrayBuffer();
|
|
15974
|
+
} catch (e) {
|
|
15975
|
+
if (e instanceof TypeError) {
|
|
15976
|
+
lastError = e;
|
|
15977
|
+
if (attempt < MAX_RETRIES) {
|
|
15978
|
+
await new Promise((r) => setTimeout(r, BASE_DELAY_MS * 2 ** attempt));
|
|
15979
|
+
continue;
|
|
15980
|
+
}
|
|
15981
|
+
}
|
|
15982
|
+
throw e;
|
|
15983
|
+
}
|
|
15876
15984
|
}
|
|
15877
|
-
|
|
15985
|
+
throw lastError;
|
|
15878
15986
|
}
|
|
15879
15987
|
reportMessages(token, taskId, messages) {
|
|
15880
15988
|
return this.request("POST", `/api/daemon/tasks/${taskId}/messages`, token, { messages });
|
|
@@ -16282,13 +16390,172 @@ async function handleCliUpdate(version3, onSuccess, profile) {
|
|
|
16282
16390
|
}
|
|
16283
16391
|
}
|
|
16284
16392
|
|
|
16393
|
+
// daemon/execenv/timeline.ts
|
|
16394
|
+
import { appendFileSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync } from "fs";
|
|
16395
|
+
import { join as join4 } from "path";
|
|
16396
|
+
|
|
16397
|
+
// daemon/execenv/filelock.ts
|
|
16398
|
+
import { mkdirSync as mkdirSync3, rmdirSync, statSync } from "fs";
|
|
16399
|
+
var DEFAULT_STALE_MS = 3600000;
|
|
16400
|
+
function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
|
|
16401
|
+
try {
|
|
16402
|
+
mkdirSync3(lockPath);
|
|
16403
|
+
return true;
|
|
16404
|
+
} catch {
|
|
16405
|
+
try {
|
|
16406
|
+
const stat = statSync(lockPath);
|
|
16407
|
+
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
16408
|
+
rmdirSync(lockPath);
|
|
16409
|
+
try {
|
|
16410
|
+
mkdirSync3(lockPath);
|
|
16411
|
+
return true;
|
|
16412
|
+
} catch {
|
|
16413
|
+
return false;
|
|
16414
|
+
}
|
|
16415
|
+
}
|
|
16416
|
+
} catch {
|
|
16417
|
+
try {
|
|
16418
|
+
mkdirSync3(lockPath);
|
|
16419
|
+
return true;
|
|
16420
|
+
} catch {
|
|
16421
|
+
return false;
|
|
16422
|
+
}
|
|
16423
|
+
}
|
|
16424
|
+
return false;
|
|
16425
|
+
}
|
|
16426
|
+
}
|
|
16427
|
+
function releaseLock(lockPath) {
|
|
16428
|
+
try {
|
|
16429
|
+
rmdirSync(lockPath);
|
|
16430
|
+
} catch {}
|
|
16431
|
+
}
|
|
16432
|
+
|
|
16433
|
+
// daemon/execenv/timeline.ts
|
|
16434
|
+
function readJsonl(filePath) {
|
|
16435
|
+
let content;
|
|
16436
|
+
try {
|
|
16437
|
+
content = readFileSync5(filePath, "utf-8");
|
|
16438
|
+
} catch {
|
|
16439
|
+
return [];
|
|
16440
|
+
}
|
|
16441
|
+
const entries = [];
|
|
16442
|
+
for (const line of content.trimEnd().split(`
|
|
16443
|
+
`)) {
|
|
16444
|
+
if (!line)
|
|
16445
|
+
continue;
|
|
16446
|
+
try {
|
|
16447
|
+
entries.push(JSON.parse(line));
|
|
16448
|
+
} catch {}
|
|
16449
|
+
}
|
|
16450
|
+
return entries;
|
|
16451
|
+
}
|
|
16452
|
+
function filenameForDate(date5) {
|
|
16453
|
+
const y = date5.getFullYear();
|
|
16454
|
+
const m = String(date5.getMonth() + 1).padStart(2, "0");
|
|
16455
|
+
const d = String(date5.getDate()).padStart(2, "0");
|
|
16456
|
+
return `${y}-${m}-${d}.jsonl`;
|
|
16457
|
+
}
|
|
16458
|
+
function recentFilenames(maxDays) {
|
|
16459
|
+
const filenames = [];
|
|
16460
|
+
const now = new Date;
|
|
16461
|
+
for (let i = 0;i < maxDays; i++) {
|
|
16462
|
+
const d = new Date(now);
|
|
16463
|
+
d.setDate(d.getDate() - i);
|
|
16464
|
+
filenames.push(filenameForDate(d));
|
|
16465
|
+
}
|
|
16466
|
+
return filenames;
|
|
16467
|
+
}
|
|
16468
|
+
var DEFAULT_RESUME_MAX_AGE_MS = 3 * 60 * 60 * 1000;
|
|
16469
|
+
var EMAIL_RESUME_MAX_AGE_MS = 48 * 60 * 60 * 1000;
|
|
16470
|
+
function findRunningPidByTaskId(timelineDir, taskId) {
|
|
16471
|
+
for (const filename of recentFilenames(7)) {
|
|
16472
|
+
const entries = readJsonl(join4(timelineDir, filename));
|
|
16473
|
+
for (const entry of entries) {
|
|
16474
|
+
if (entry.task_id === taskId && entry.status === "running" && entry.pid != null) {
|
|
16475
|
+
return entry.pid;
|
|
16476
|
+
}
|
|
16477
|
+
}
|
|
16478
|
+
}
|
|
16479
|
+
return null;
|
|
16480
|
+
}
|
|
16481
|
+
function findRunningEntryByContextKey(timelineDir, contextKey, provider) {
|
|
16482
|
+
for (const filename of recentFilenames(7)) {
|
|
16483
|
+
const dayEntries = readJsonl(join4(timelineDir, filename));
|
|
16484
|
+
for (let i = dayEntries.length - 1;i >= 0; i--) {
|
|
16485
|
+
const entry = dayEntries[i];
|
|
16486
|
+
if (entry.status === "running" && entry.context_key === contextKey && entry.provider === provider) {
|
|
16487
|
+
return entry;
|
|
16488
|
+
}
|
|
16489
|
+
}
|
|
16490
|
+
}
|
|
16491
|
+
return null;
|
|
16492
|
+
}
|
|
16493
|
+
|
|
16494
|
+
// daemon/execenv/steering.ts
|
|
16495
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync3, readdirSync, statSync as statSync2 } from "fs";
|
|
16496
|
+
import { join as join5 } from "path";
|
|
16497
|
+
var INTENT_DIR_NAME = ".kill_intents";
|
|
16498
|
+
var STEERING_LOCK_DIR = ".steering_locks";
|
|
16499
|
+
var INTENT_STALE_MS = 10 * 60 * 1000;
|
|
16500
|
+
function intentFilePath(baseDir, taskId) {
|
|
16501
|
+
return join5(baseDir, INTENT_DIR_NAME, `${taskId}.json`);
|
|
16502
|
+
}
|
|
16503
|
+
function intentDirPath(baseDir) {
|
|
16504
|
+
return join5(baseDir, INTENT_DIR_NAME);
|
|
16505
|
+
}
|
|
16506
|
+
function steeringLockPath(baseDir, contextKey) {
|
|
16507
|
+
const safeKey = contextKey.replace(/[^a-zA-Z0-9_:-]/g, "_");
|
|
16508
|
+
return join5(baseDir, STEERING_LOCK_DIR, safeKey);
|
|
16509
|
+
}
|
|
16510
|
+
function writeKillIntent(baseDir, intent) {
|
|
16511
|
+
const dir = intentDirPath(baseDir);
|
|
16512
|
+
try {
|
|
16513
|
+
mkdirSync4(dir, { recursive: true });
|
|
16514
|
+
} catch {}
|
|
16515
|
+
const filePath = intentFilePath(baseDir, intent.targetTaskId);
|
|
16516
|
+
writeFileSync5(filePath, JSON.stringify(intent));
|
|
16517
|
+
}
|
|
16518
|
+
function cleanupStaleIntents(baseDir) {
|
|
16519
|
+
const dir = intentDirPath(baseDir);
|
|
16520
|
+
let files;
|
|
16521
|
+
try {
|
|
16522
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
16523
|
+
} catch {
|
|
16524
|
+
return;
|
|
16525
|
+
}
|
|
16526
|
+
const now = Date.now();
|
|
16527
|
+
for (const file2 of files) {
|
|
16528
|
+
const filePath = join5(dir, file2);
|
|
16529
|
+
try {
|
|
16530
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
16531
|
+
const intent = JSON.parse(content);
|
|
16532
|
+
const stat = statSync2(filePath);
|
|
16533
|
+
if (now - stat.mtimeMs > INTENT_STALE_MS) {
|
|
16534
|
+
unlinkSync3(filePath);
|
|
16535
|
+
log.debug(`Cleaned up stale kill intent for task ${intent.targetTaskId}`);
|
|
16536
|
+
}
|
|
16537
|
+
} catch {}
|
|
16538
|
+
}
|
|
16539
|
+
}
|
|
16540
|
+
function acquireSteeringLock(baseDir, contextKey) {
|
|
16541
|
+
const lockPath = steeringLockPath(baseDir, contextKey);
|
|
16542
|
+
try {
|
|
16543
|
+
mkdirSync4(join5(baseDir, STEERING_LOCK_DIR), { recursive: true });
|
|
16544
|
+
} catch {}
|
|
16545
|
+
return acquireLock(lockPath, 60000);
|
|
16546
|
+
}
|
|
16547
|
+
function releaseSteeringLock(baseDir, contextKey) {
|
|
16548
|
+
const lockPath = steeringLockPath(baseDir, contextKey);
|
|
16549
|
+
releaseLock(lockPath);
|
|
16550
|
+
}
|
|
16551
|
+
|
|
16285
16552
|
// daemon/daemon.ts
|
|
16286
|
-
import { existsSync, mkdirSync as
|
|
16553
|
+
import { existsSync, mkdirSync as mkdirSync5, openSync, closeSync, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
16287
16554
|
import { execSync as execSync3, spawn as spawn2 } from "child_process";
|
|
16288
16555
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
16289
|
-
import { dirname as dirname3, join as
|
|
16556
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
16290
16557
|
var _dir = dirname3(fileURLToPath2(import.meta.url));
|
|
16291
|
-
var sessionRunnerPath = existsSync(
|
|
16558
|
+
var sessionRunnerPath = existsSync(join6(_dir, "session-runner.js")) ? join6(_dir, "session-runner.js") : join6(_dir, "session-runner.ts");
|
|
16292
16559
|
function isCommandAvailable2(cmd) {
|
|
16293
16560
|
try {
|
|
16294
16561
|
const check2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
|
|
@@ -16298,21 +16565,21 @@ function isCommandAvailable2(cmd) {
|
|
|
16298
16565
|
return false;
|
|
16299
16566
|
}
|
|
16300
16567
|
}
|
|
16301
|
-
var MAX_SESSION_RUNNER_LOGS =
|
|
16568
|
+
var MAX_SESSION_RUNNER_LOGS = 500;
|
|
16302
16569
|
function pruneSessionRunnerLogs() {
|
|
16303
16570
|
const logDir = sessionRunnerLogDir();
|
|
16304
16571
|
let entries;
|
|
16305
16572
|
try {
|
|
16306
|
-
entries =
|
|
16573
|
+
entries = readdirSync2(logDir).filter((f) => f.endsWith(".log"));
|
|
16307
16574
|
} catch {
|
|
16308
16575
|
return;
|
|
16309
16576
|
}
|
|
16310
16577
|
if (entries.length <= MAX_SESSION_RUNNER_LOGS)
|
|
16311
16578
|
return;
|
|
16312
16579
|
const withMtime = entries.map((name) => {
|
|
16313
|
-
const full =
|
|
16580
|
+
const full = join6(logDir, name);
|
|
16314
16581
|
try {
|
|
16315
|
-
return { name, mtime:
|
|
16582
|
+
return { name, mtime: statSync3(full).mtimeMs };
|
|
16316
16583
|
} catch {
|
|
16317
16584
|
return { name, mtime: 0 };
|
|
16318
16585
|
}
|
|
@@ -16320,7 +16587,7 @@ function pruneSessionRunnerLogs() {
|
|
|
16320
16587
|
withMtime.sort((a, b) => b.mtime - a.mtime);
|
|
16321
16588
|
for (const entry of withMtime.slice(MAX_SESSION_RUNNER_LOGS)) {
|
|
16322
16589
|
try {
|
|
16323
|
-
|
|
16590
|
+
unlinkSync4(join6(logDir, entry.name));
|
|
16324
16591
|
} catch {}
|
|
16325
16592
|
}
|
|
16326
16593
|
}
|
|
@@ -16399,12 +16666,7 @@ async function startDaemon(profile, serverUrl) {
|
|
|
16399
16666
|
});
|
|
16400
16667
|
} catch (e) {
|
|
16401
16668
|
if (e instanceof Error && e.message.startsWith("HTTP 401")) {
|
|
16402
|
-
log.warn(`Workspace ${ws.id} token invalid —
|
|
16403
|
-
try {
|
|
16404
|
-
const cfg = loadCLIConfigForProfile(profile);
|
|
16405
|
-
cfg.watched_workspaces = (cfg.watched_workspaces || []).filter((w) => w.id !== ws.id);
|
|
16406
|
-
saveCLIConfigForProfile(profile, cfg);
|
|
16407
|
-
} catch {}
|
|
16669
|
+
log.warn(`Workspace ${ws.id} token invalid — skipping (run '${cmdPrefix()} register --token <token>' to fix)`);
|
|
16408
16670
|
} else {
|
|
16409
16671
|
log.error(`Failed to register workspace ${ws.id}, skipping`, e);
|
|
16410
16672
|
}
|
|
@@ -16463,7 +16725,7 @@ async function startDaemon(profile, serverUrl) {
|
|
|
16463
16725
|
cfg.watched_workspaces = (cfg.watched_workspaces || []).filter((w) => w.id !== workspaceId);
|
|
16464
16726
|
saveCLIConfigForProfile(profile, cfg);
|
|
16465
16727
|
} catch {}
|
|
16466
|
-
log.info(`Workspace ${workspaceId}
|
|
16728
|
+
log.info(`Workspace ${workspaceId} deleted server-side — removed from config`);
|
|
16467
16729
|
}
|
|
16468
16730
|
const pollCycle = async () => {
|
|
16469
16731
|
let remaining = config2.maxConcurrentTasks - activeTasks.size;
|
|
@@ -16500,7 +16762,7 @@ async function startDaemon(profile, serverUrl) {
|
|
|
16500
16762
|
}
|
|
16501
16763
|
} catch (e) {
|
|
16502
16764
|
if (e instanceof Error && e.message.startsWith("HTTP 401")) {
|
|
16503
|
-
|
|
16765
|
+
log.warn(`Workspace ${ws.workspaceId} poll returned 401 — will retry next cycle`);
|
|
16504
16766
|
} else {
|
|
16505
16767
|
log.debug("Poll error", e);
|
|
16506
16768
|
}
|
|
@@ -16537,17 +16799,28 @@ async function startDaemon(profile, serverUrl) {
|
|
|
16537
16799
|
releaseDaemonPid(profile);
|
|
16538
16800
|
health.server.close(() => {
|
|
16539
16801
|
if (restartRequested) {
|
|
16540
|
-
const
|
|
16802
|
+
const entry = process.argv[1];
|
|
16803
|
+
const args = [entry, "daemon", "start", "--foreground"];
|
|
16541
16804
|
if (profile)
|
|
16542
16805
|
args.push("--profile", profile);
|
|
16543
16806
|
if (serverUrl)
|
|
16544
16807
|
args.push("--server", serverUrl);
|
|
16545
|
-
const
|
|
16808
|
+
const logPath = daemonLogFilePath();
|
|
16809
|
+
let logFd;
|
|
16810
|
+
try {
|
|
16811
|
+
mkdirSync5(dirname3(logPath), { recursive: true, mode: 448 });
|
|
16812
|
+
logFd = openSync(logPath, "a", 384);
|
|
16813
|
+
} catch (e) {
|
|
16814
|
+
log.error(`Failed to open daemon log file ${logPath}`, e);
|
|
16815
|
+
}
|
|
16816
|
+
const child = spawn2(process.execPath, args, {
|
|
16546
16817
|
detached: true,
|
|
16547
|
-
stdio: ["ignore", "ignore", "ignore"]
|
|
16818
|
+
stdio: logFd != null ? ["ignore", logFd, logFd] : ["ignore", "ignore", "ignore"]
|
|
16548
16819
|
});
|
|
16549
16820
|
child.unref();
|
|
16550
|
-
|
|
16821
|
+
if (logFd != null)
|
|
16822
|
+
closeSync(logFd);
|
|
16823
|
+
log.info(`Spawned new daemon (pid=${child.pid}), logs: ${logPath}`);
|
|
16551
16824
|
}
|
|
16552
16825
|
clearTimeout(timeout);
|
|
16553
16826
|
process.exit(0);
|
|
@@ -16559,21 +16832,62 @@ async function startDaemon(profile, serverUrl) {
|
|
|
16559
16832
|
}
|
|
16560
16833
|
function spawnSessionRunner(input) {
|
|
16561
16834
|
const logDir = sessionRunnerLogDir();
|
|
16562
|
-
|
|
16563
|
-
const logFilePath =
|
|
16835
|
+
mkdirSync5(logDir, { recursive: true });
|
|
16836
|
+
const logFilePath = join6(logDir, `${input.task.id}.log`);
|
|
16564
16837
|
input.logFilePath = logFilePath;
|
|
16565
16838
|
const encoded = Buffer.from(JSON.stringify(input)).toString("base64");
|
|
16566
|
-
|
|
16839
|
+
let fd;
|
|
16840
|
+
try {
|
|
16841
|
+
fd = openSync(logFilePath, "a");
|
|
16842
|
+
} catch (e) {
|
|
16843
|
+
log.error(`Failed to open log file ${logFilePath}`, e);
|
|
16844
|
+
}
|
|
16567
16845
|
const child = spawn2(process.execPath, [sessionRunnerPath, encoded], {
|
|
16568
16846
|
detached: true,
|
|
16569
|
-
stdio: ["ignore", fd, fd]
|
|
16847
|
+
stdio: fd != null ? ["ignore", fd, fd] : ["ignore", "ignore", "ignore"]
|
|
16570
16848
|
});
|
|
16571
16849
|
child.unref();
|
|
16572
|
-
|
|
16850
|
+
if (fd != null)
|
|
16851
|
+
closeSync(fd);
|
|
16573
16852
|
return child;
|
|
16574
16853
|
}
|
|
16575
16854
|
async function handleTask(client, config2, runtimeIndex, task, token, activeTasks) {
|
|
16576
16855
|
log.info(`Task ${task.id} claimed agent=${task.agentId}`);
|
|
16856
|
+
if (task.type === TASK_TYPES.KILL_TASK) {
|
|
16857
|
+
const targetTaskId = task.context?.target_task_id;
|
|
16858
|
+
if (!targetTaskId) {
|
|
16859
|
+
await client.failTask(token, task.id, "missing target_task_id in context");
|
|
16860
|
+
activeTasks.delete(task.id);
|
|
16861
|
+
return;
|
|
16862
|
+
}
|
|
16863
|
+
const agentBaseDir = join6(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
|
|
16864
|
+
const timelineDir = join6(agentBaseDir, ".context_timeline");
|
|
16865
|
+
const pid = findRunningPidByTaskId(timelineDir, targetTaskId);
|
|
16866
|
+
if (pid != null) {
|
|
16867
|
+
writeKillIntent(agentBaseDir, {
|
|
16868
|
+
reason: "cancelled",
|
|
16869
|
+
targetTaskId,
|
|
16870
|
+
expectedPid: pid
|
|
16871
|
+
});
|
|
16872
|
+
try {
|
|
16873
|
+
process.kill(pid, "SIGTERM");
|
|
16874
|
+
await client.failTask(token, task.id, "killed");
|
|
16875
|
+
log.info(`Kill task ${task.id}: sent SIGTERM to pid=${pid} for target=${targetTaskId}`);
|
|
16876
|
+
} catch (e) {
|
|
16877
|
+
if (e?.code === "ESRCH") {
|
|
16878
|
+
await client.failTask(token, task.id, "target process already exited");
|
|
16879
|
+
log.info(`Kill task ${task.id}: target pid=${pid} already exited`);
|
|
16880
|
+
} else {
|
|
16881
|
+
await client.failTask(token, task.id, `kill failed: ${e}`);
|
|
16882
|
+
}
|
|
16883
|
+
}
|
|
16884
|
+
} else {
|
|
16885
|
+
await client.failTask(token, task.id, "target not found in timeline");
|
|
16886
|
+
log.info(`Kill task ${task.id}: target ${targetTaskId} not found in timeline`);
|
|
16887
|
+
}
|
|
16888
|
+
activeTasks.delete(task.id);
|
|
16889
|
+
return;
|
|
16890
|
+
}
|
|
16577
16891
|
try {
|
|
16578
16892
|
await client.startTask(token, task.id);
|
|
16579
16893
|
} catch (e) {
|
|
@@ -16588,6 +16902,60 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
|
|
|
16588
16902
|
return;
|
|
16589
16903
|
}
|
|
16590
16904
|
const provider = runtimeData.provider;
|
|
16905
|
+
if (task.contextKey) {
|
|
16906
|
+
const agentBaseDir = join6(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
|
|
16907
|
+
cleanupStaleIntents(agentBaseDir);
|
|
16908
|
+
const timelineDir = join6(agentBaseDir, ".context_timeline");
|
|
16909
|
+
const lockAcquired = acquireSteeringLock(agentBaseDir, task.contextKey);
|
|
16910
|
+
if (!lockAcquired) {
|
|
16911
|
+
log.warn(`Steering lock contention for context_key=${task.contextKey}, proceeding without steering`);
|
|
16912
|
+
} else {
|
|
16913
|
+
try {
|
|
16914
|
+
const predecessor = findRunningEntryByContextKey(timelineDir, task.contextKey, provider);
|
|
16915
|
+
if (predecessor && predecessor.task_id !== task.id) {
|
|
16916
|
+
log.info(`Steering: task ${task.id} supersedes predecessor ${predecessor.task_id} (context_key=${task.contextKey})`);
|
|
16917
|
+
if (predecessor.pid != null) {
|
|
16918
|
+
writeKillIntent(agentBaseDir, {
|
|
16919
|
+
reason: "superseded",
|
|
16920
|
+
targetTaskId: predecessor.task_id,
|
|
16921
|
+
expectedPid: predecessor.pid,
|
|
16922
|
+
successorTaskId: task.id
|
|
16923
|
+
});
|
|
16924
|
+
try {
|
|
16925
|
+
process.kill(predecessor.pid, "SIGTERM");
|
|
16926
|
+
log.info(`Steering: sent SIGTERM to predecessor pid=${predecessor.pid}`);
|
|
16927
|
+
} catch (e) {
|
|
16928
|
+
if (e?.code === "ESRCH") {
|
|
16929
|
+
log.info(`Steering: predecessor pid=${predecessor.pid} already exited`);
|
|
16930
|
+
} else {
|
|
16931
|
+
log.warn(`Steering: kill failed for pid=${predecessor.pid}`, e);
|
|
16932
|
+
}
|
|
16933
|
+
}
|
|
16934
|
+
const waitStart = Date.now();
|
|
16935
|
+
const MAX_WAIT_MS = 15000;
|
|
16936
|
+
const POLL_MS = 200;
|
|
16937
|
+
while (Date.now() - waitStart < MAX_WAIT_MS) {
|
|
16938
|
+
const stillRunning = findRunningPidByTaskId(timelineDir, predecessor.task_id);
|
|
16939
|
+
if (stillRunning == null)
|
|
16940
|
+
break;
|
|
16941
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
16942
|
+
}
|
|
16943
|
+
if (findRunningPidByTaskId(timelineDir, predecessor.task_id) != null) {
|
|
16944
|
+
log.warn(`Steering: predecessor pid=${predecessor.pid} did not exit within ${MAX_WAIT_MS}ms, proceeding anyway`);
|
|
16945
|
+
}
|
|
16946
|
+
}
|
|
16947
|
+
try {
|
|
16948
|
+
await client.supersedeTask(token, predecessor.task_id);
|
|
16949
|
+
log.info(`Steering: predecessor ${predecessor.task_id} marked superseded`);
|
|
16950
|
+
} catch (e) {
|
|
16951
|
+
log.warn(`Steering: failed to mark predecessor superseded server-side`, e);
|
|
16952
|
+
}
|
|
16953
|
+
}
|
|
16954
|
+
} finally {
|
|
16955
|
+
releaseSteeringLock(agentBaseDir, task.contextKey);
|
|
16956
|
+
}
|
|
16957
|
+
}
|
|
16958
|
+
}
|
|
16591
16959
|
const cliPath = provider === "claude" ? config2.claudePath : provider === "codex" ? config2.codexPath : config2.opencodePath;
|
|
16592
16960
|
const configModel = provider === "claude" ? config2.claudeModel : provider === "codex" ? config2.codexModel : config2.opencodeModel;
|
|
16593
16961
|
const agentModel = task.agent?.runtimeConfig?.model;
|
|
@@ -16642,7 +17010,7 @@ async function startInBackground(profile, serverUrl) {
|
|
|
16642
17010
|
return;
|
|
16643
17011
|
}
|
|
16644
17012
|
const logPath = daemonLogFilePath();
|
|
16645
|
-
|
|
17013
|
+
mkdirSync6(dirname4(logPath), { recursive: true, mode: 448 });
|
|
16646
17014
|
const logFd = openSync2(logPath, "a", 384);
|
|
16647
17015
|
const child = spawn3(process.execPath, buildChildArgs(profile, serverUrl), {
|
|
16648
17016
|
detached: true,
|
|
@@ -16733,6 +17101,14 @@ function daemonCommand() {
|
|
|
16733
17101
|
import { Command as Command4 } from "commander";
|
|
16734
17102
|
|
|
16735
17103
|
// lib/output.ts
|
|
17104
|
+
function printTable(headers, rows) {
|
|
17105
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || "").length)));
|
|
17106
|
+
const header = headers.map((h, i) => h.padEnd(widths[i])).join(" ");
|
|
17107
|
+
const separator = widths.map((w) => "-".repeat(w)).join(" ");
|
|
17108
|
+
console.log(header);
|
|
17109
|
+
console.log(separator);
|
|
17110
|
+
rows.forEach((r) => console.log(r.map((c, i) => (c || "").padEnd(widths[i])).join(" ")));
|
|
17111
|
+
}
|
|
16736
17112
|
function printJSON(data) {
|
|
16737
17113
|
console.log(JSON.stringify(data, null, 2));
|
|
16738
17114
|
}
|
|
@@ -16752,8 +17128,8 @@ function configCommand() {
|
|
|
16752
17128
|
|
|
16753
17129
|
// commands/email.ts
|
|
16754
17130
|
import { Command as Command5 } from "commander";
|
|
16755
|
-
import { writeFileSync as
|
|
16756
|
-
import { basename, join as
|
|
17131
|
+
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
|
|
17132
|
+
import { basename, join as join7 } from "path";
|
|
16757
17133
|
import PostalMime from "postal-mime";
|
|
16758
17134
|
var VALID_STATUSES = ["unread", "read", "archived"];
|
|
16759
17135
|
var EMAIL_BASE = "/tmp/alook-emails";
|
|
@@ -16784,7 +17160,10 @@ function collectRepeated(value, previous) {
|
|
|
16784
17160
|
return previous.concat([value]);
|
|
16785
17161
|
}
|
|
16786
17162
|
function resolveClientOpts(command, opts) {
|
|
16787
|
-
|
|
17163
|
+
let root = command;
|
|
17164
|
+
while (root.parent)
|
|
17165
|
+
root = root.parent;
|
|
17166
|
+
const parentOpts = root.opts() || {};
|
|
16788
17167
|
const profile = parentOpts.profile;
|
|
16789
17168
|
const cfg = loadCLIConfigForProfile(profile);
|
|
16790
17169
|
const serverUrl = parentOpts.server || cfg.server_url;
|
|
@@ -16813,7 +17192,7 @@ function emailCommand() {
|
|
|
16813
17192
|
console.error(`Error: invalid status "${opts.status}", must be one of: ${VALID_STATUSES.join(", ")}`);
|
|
16814
17193
|
process.exit(1);
|
|
16815
17194
|
}
|
|
16816
|
-
const emailDir_base =
|
|
17195
|
+
const emailDir_base = join7(EMAIL_BASE, workspaceId, opts.agent_id);
|
|
16817
17196
|
try {
|
|
16818
17197
|
let query = `/api/email?agentId=${opts.agent_id}`;
|
|
16819
17198
|
if (opts.status)
|
|
@@ -16827,11 +17206,11 @@ function emailCommand() {
|
|
|
16827
17206
|
printJSON(emails2);
|
|
16828
17207
|
return;
|
|
16829
17208
|
}
|
|
16830
|
-
|
|
17209
|
+
mkdirSync7(emailDir_base, { recursive: true });
|
|
16831
17210
|
const downloadedPaths = [];
|
|
16832
17211
|
for (const email3 of emails2) {
|
|
16833
|
-
const emailDir =
|
|
16834
|
-
|
|
17212
|
+
const emailDir = join7(emailDir_base, email3.id);
|
|
17213
|
+
mkdirSync7(emailDir, { recursive: true });
|
|
16835
17214
|
const metadata = {
|
|
16836
17215
|
id: email3.id,
|
|
16837
17216
|
from: email3.from_email,
|
|
@@ -16843,8 +17222,8 @@ function emailCommand() {
|
|
|
16843
17222
|
in_reply_to: email3.in_reply_to || "",
|
|
16844
17223
|
references: email3.references || ""
|
|
16845
17224
|
};
|
|
16846
|
-
const metadataPath =
|
|
16847
|
-
|
|
17225
|
+
const metadataPath = join7(emailDir, "metadata.json");
|
|
17226
|
+
writeFileSync6(metadataPath, JSON.stringify(metadata, null, 2));
|
|
16848
17227
|
downloadedPaths.push(metadataPath);
|
|
16849
17228
|
let rawMime;
|
|
16850
17229
|
try {
|
|
@@ -16859,18 +17238,18 @@ function emailCommand() {
|
|
|
16859
17238
|
}
|
|
16860
17239
|
const parsed = await new PostalMime().parse(rawMime);
|
|
16861
17240
|
if (parsed.text) {
|
|
16862
|
-
const bodyPath =
|
|
16863
|
-
|
|
17241
|
+
const bodyPath = join7(emailDir, "body.txt");
|
|
17242
|
+
writeFileSync6(bodyPath, parsed.text);
|
|
16864
17243
|
downloadedPaths.push(bodyPath);
|
|
16865
17244
|
}
|
|
16866
17245
|
if (parsed.html) {
|
|
16867
|
-
const htmlPath =
|
|
16868
|
-
|
|
17246
|
+
const htmlPath = join7(emailDir, "body.html");
|
|
17247
|
+
writeFileSync6(htmlPath, parsed.html);
|
|
16869
17248
|
downloadedPaths.push(htmlPath);
|
|
16870
17249
|
}
|
|
16871
17250
|
if (parsed.attachments && parsed.attachments.length > 0) {
|
|
16872
|
-
const attDir =
|
|
16873
|
-
|
|
17251
|
+
const attDir = join7(emailDir, "attachments");
|
|
17252
|
+
mkdirSync7(attDir, { recursive: true });
|
|
16874
17253
|
const usedFilenames = new Set;
|
|
16875
17254
|
for (let i = 0;i < parsed.attachments.length; i++) {
|
|
16876
17255
|
const att = parsed.attachments[i];
|
|
@@ -16879,7 +17258,7 @@ function emailCommand() {
|
|
|
16879
17258
|
filename = `${i}-${filename}`;
|
|
16880
17259
|
}
|
|
16881
17260
|
usedFilenames.add(filename);
|
|
16882
|
-
const attPath =
|
|
17261
|
+
const attPath = join7(attDir, filename);
|
|
16883
17262
|
const content = att.content;
|
|
16884
17263
|
let buf;
|
|
16885
17264
|
if (typeof content === "string") {
|
|
@@ -16889,7 +17268,7 @@ function emailCommand() {
|
|
|
16889
17268
|
} else {
|
|
16890
17269
|
buf = Buffer.from(content);
|
|
16891
17270
|
}
|
|
16892
|
-
|
|
17271
|
+
writeFileSync6(attPath, buf);
|
|
16893
17272
|
downloadedPaths.push(attPath);
|
|
16894
17273
|
}
|
|
16895
17274
|
}
|
|
@@ -16928,7 +17307,7 @@ function emailCommand() {
|
|
|
16928
17307
|
const client = new APIClient(serverUrl, token, workspaceId);
|
|
16929
17308
|
let htmlBody;
|
|
16930
17309
|
try {
|
|
16931
|
-
htmlBody =
|
|
17310
|
+
htmlBody = readFileSync7(opts.bodyFile, "utf-8");
|
|
16932
17311
|
} catch (err) {
|
|
16933
17312
|
console.error(`Error: cannot read body file "${opts.bodyFile}": ${err instanceof Error ? err.message : err}`);
|
|
16934
17313
|
process.exit(1);
|
|
@@ -16944,8 +17323,8 @@ function emailCommand() {
|
|
|
16944
17323
|
let bytes;
|
|
16945
17324
|
let size;
|
|
16946
17325
|
try {
|
|
16947
|
-
bytes =
|
|
16948
|
-
size =
|
|
17326
|
+
bytes = readFileSync7(path);
|
|
17327
|
+
size = statSync4(path).size;
|
|
16949
17328
|
} catch (err) {
|
|
16950
17329
|
console.error(`Error: cannot read attachment "${path}": ${err instanceof Error ? err.message : err}`);
|
|
16951
17330
|
process.exit(1);
|
|
@@ -16990,6 +17369,73 @@ function emailCommand() {
|
|
|
16990
17369
|
process.exit(1);
|
|
16991
17370
|
}
|
|
16992
17371
|
});
|
|
17372
|
+
const whitelistCmd = new Command5("whitelist").description("Manage email whitelist (allowed senders)");
|
|
17373
|
+
whitelistCmd.command("list").description("List all whitelisted emails for an agent").requiredOption("--agent_id <id>", "Agent ID").option("--workspace <id>", "Workspace ID").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
17374
|
+
const { serverUrl, token, workspaceId } = resolveClientOpts(command, {
|
|
17375
|
+
workspace: opts.workspace,
|
|
17376
|
+
agentId: opts.agent_id
|
|
17377
|
+
});
|
|
17378
|
+
const client = new APIClient(serverUrl, token, workspaceId);
|
|
17379
|
+
try {
|
|
17380
|
+
const entries = await client.getJSON(`/api/agents/${opts.agent_id}/whitelist`);
|
|
17381
|
+
if (!entries.length) {
|
|
17382
|
+
console.log("No whitelisted emails.");
|
|
17383
|
+
return;
|
|
17384
|
+
}
|
|
17385
|
+
if (opts.json) {
|
|
17386
|
+
printJSON(entries);
|
|
17387
|
+
return;
|
|
17388
|
+
}
|
|
17389
|
+
printTable(["ID", "EMAIL", "CREATED AT"], entries.map((e) => [e.id, e.email, e.created_at]));
|
|
17390
|
+
} catch (err) {
|
|
17391
|
+
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
|
17392
|
+
process.exit(1);
|
|
17393
|
+
}
|
|
17394
|
+
});
|
|
17395
|
+
whitelistCmd.command("add").description("Add an email to the whitelist").requiredOption("--agent_id <id>", "Agent ID").option("--workspace <id>", "Workspace ID").argument("<email>", "Email address to whitelist").action(async (email3, opts, command) => {
|
|
17396
|
+
const { serverUrl, token, workspaceId } = resolveClientOpts(command, {
|
|
17397
|
+
workspace: opts.workspace,
|
|
17398
|
+
agentId: opts.agent_id
|
|
17399
|
+
});
|
|
17400
|
+
const client = new APIClient(serverUrl, token, workspaceId);
|
|
17401
|
+
try {
|
|
17402
|
+
const entry = await client.postJSON(`/api/agents/${opts.agent_id}/whitelist`, { email: email3.toLowerCase() });
|
|
17403
|
+
console.log(`Added ${entry.email} to whitelist (id: ${entry.id})`);
|
|
17404
|
+
} catch (err) {
|
|
17405
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17406
|
+
if (msg.includes("409")) {
|
|
17407
|
+
console.error(`Error: ${email3.toLowerCase()} is already whitelisted`);
|
|
17408
|
+
} else {
|
|
17409
|
+
console.error(`Error: ${msg}`);
|
|
17410
|
+
}
|
|
17411
|
+
process.exit(1);
|
|
17412
|
+
}
|
|
17413
|
+
});
|
|
17414
|
+
whitelistCmd.command("delete").description("Remove an email from the whitelist").requiredOption("--agent_id <id>", "Agent ID").option("--workspace <id>", "Workspace ID").argument("<email>", "Email address to remove").action(async (email3, opts, command) => {
|
|
17415
|
+
const { serverUrl, token, workspaceId } = resolveClientOpts(command, {
|
|
17416
|
+
workspace: opts.workspace,
|
|
17417
|
+
agentId: opts.agent_id
|
|
17418
|
+
});
|
|
17419
|
+
const client = new APIClient(serverUrl, token, workspaceId);
|
|
17420
|
+
const normalizedEmail = email3.toLowerCase();
|
|
17421
|
+
try {
|
|
17422
|
+
const entries = await client.getJSON(`/api/agents/${opts.agent_id}/whitelist`);
|
|
17423
|
+
const entry = entries.find((e) => e.email === normalizedEmail);
|
|
17424
|
+
if (!entry) {
|
|
17425
|
+
console.error(`Error: ${normalizedEmail} is not in the whitelist`);
|
|
17426
|
+
process.exit(1);
|
|
17427
|
+
}
|
|
17428
|
+
await client.deleteJSON(`/api/agents/${opts.agent_id}/whitelist/${entry.id}`);
|
|
17429
|
+
console.log(`Removed ${normalizedEmail} from whitelist`);
|
|
17430
|
+
} catch (err) {
|
|
17431
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17432
|
+
if (msg === "__exit__")
|
|
17433
|
+
throw err;
|
|
17434
|
+
console.error(`Error: ${msg}`);
|
|
17435
|
+
process.exit(1);
|
|
17436
|
+
}
|
|
17437
|
+
});
|
|
17438
|
+
cmd.addCommand(whitelistCmd);
|
|
16993
17439
|
return cmd;
|
|
16994
17440
|
}
|
|
16995
17441
|
|
|
@@ -17249,7 +17695,7 @@ ${result.output}`);
|
|
|
17249
17695
|
|
|
17250
17696
|
// commands/sync.ts
|
|
17251
17697
|
import { Command as Command9 } from "commander";
|
|
17252
|
-
import { readFileSync as
|
|
17698
|
+
import { readFileSync as readFileSync8, statSync as statSync5 } from "fs";
|
|
17253
17699
|
import { basename as basename2 } from "path";
|
|
17254
17700
|
var MIME_BY_EXT2 = {
|
|
17255
17701
|
".pdf": "application/pdf",
|
|
@@ -17298,9 +17744,9 @@ function syncCommand() {
|
|
|
17298
17744
|
let bytes;
|
|
17299
17745
|
let size;
|
|
17300
17746
|
try {
|
|
17301
|
-
const stat =
|
|
17747
|
+
const stat = statSync5(opts.file);
|
|
17302
17748
|
size = stat.size;
|
|
17303
|
-
bytes =
|
|
17749
|
+
bytes = readFileSync8(opts.file);
|
|
17304
17750
|
} catch (err) {
|
|
17305
17751
|
console.error(`Error: cannot read file "${opts.file}": ${err.message}`);
|
|
17306
17752
|
process.exit(1);
|
package/dist/session-runner.js
CHANGED
|
@@ -18,10 +18,26 @@ import { mkdir, writeFile, rm } from "fs/promises";
|
|
|
18
18
|
import path from "path";
|
|
19
19
|
|
|
20
20
|
// ../shared/src/constants.ts
|
|
21
|
+
var TaskStatus = {
|
|
22
|
+
QUEUED: "queued",
|
|
23
|
+
DISPATCHED: "dispatched",
|
|
24
|
+
RUNNING: "running",
|
|
25
|
+
COMPLETED: "completed",
|
|
26
|
+
FAILED: "failed",
|
|
27
|
+
CANCELLED: "cancelled",
|
|
28
|
+
SUPERSEDED: "superseded"
|
|
29
|
+
};
|
|
30
|
+
var TERMINAL_TASK_STATUSES = [
|
|
31
|
+
TaskStatus.COMPLETED,
|
|
32
|
+
TaskStatus.FAILED,
|
|
33
|
+
TaskStatus.CANCELLED,
|
|
34
|
+
TaskStatus.SUPERSEDED
|
|
35
|
+
];
|
|
21
36
|
var TASK_TYPES = {
|
|
22
37
|
USER_DM_MESSAGE: "user_dm_message",
|
|
23
38
|
EMAIL_NOTIFICATION: "email_notification",
|
|
24
|
-
CALENDAR_EVENT: "calendar_event"
|
|
39
|
+
CALENDAR_EVENT: "calendar_event",
|
|
40
|
+
KILL_TASK: "kill_task"
|
|
25
41
|
};
|
|
26
42
|
var POLL_INTERVAL_MS = Number(process.env.POLL_INTERVAL_MS) || 3000;
|
|
27
43
|
var OFFLINE_THRESHOLD_MS = Number(process.env.OFFLINE_THRESHOLD_MS) || 9000;
|
|
@@ -13568,7 +13584,8 @@ var TaskStatusSchema = exports_external.enum([
|
|
|
13568
13584
|
"running",
|
|
13569
13585
|
"completed",
|
|
13570
13586
|
"failed",
|
|
13571
|
-
"cancelled"
|
|
13587
|
+
"cancelled",
|
|
13588
|
+
"superseded"
|
|
13572
13589
|
]);
|
|
13573
13590
|
var ClaimedTaskRowSchema = exports_external.object({
|
|
13574
13591
|
id: exports_external.string(),
|
|
@@ -13749,8 +13766,9 @@ var UpdateAgentRequestSchema = exports_external.object({
|
|
|
13749
13766
|
description: exports_external.string().optional(),
|
|
13750
13767
|
instructions: exports_external.string().optional(),
|
|
13751
13768
|
runtime_id: exports_external.string().min(1).optional(),
|
|
13752
|
-
runtime_config: RuntimeConfigSchema
|
|
13753
|
-
|
|
13769
|
+
runtime_config: RuntimeConfigSchema,
|
|
13770
|
+
visibility: exports_external.enum(["public", "private"]).optional()
|
|
13771
|
+
}).refine((v) => v.name !== undefined || v.description !== undefined || v.instructions !== undefined || v.runtime_id !== undefined || v.runtime_config !== undefined || v.visibility !== undefined, { message: "at least one field is required" });
|
|
13754
13772
|
var CreateConversationRequestSchema = exports_external.object({
|
|
13755
13773
|
agent_id: exports_external.string().min(1, "agent_id is required")
|
|
13756
13774
|
});
|
|
@@ -13842,6 +13860,16 @@ var CreateWorkspaceRequestSchema = exports_external.object({
|
|
|
13842
13860
|
name: exports_external.string().min(1, "name is required"),
|
|
13843
13861
|
slug: exports_external.string().min(1, "slug is required")
|
|
13844
13862
|
});
|
|
13863
|
+
var UpdateWorkspaceRequestSchema = exports_external.object({
|
|
13864
|
+
name: exports_external.string().min(1, "name is required").max(100).trim().optional(),
|
|
13865
|
+
slug: exports_external.string().min(1, "slug is required").max(100).trim().toLowerCase().optional()
|
|
13866
|
+
});
|
|
13867
|
+
var DeleteWorkspaceRequestSchema = exports_external.object({
|
|
13868
|
+
confirm_name: exports_external.string().min(1, "confirm_name is required")
|
|
13869
|
+
});
|
|
13870
|
+
var GrantAgentAccessRequestSchema = exports_external.object({
|
|
13871
|
+
user_id: exports_external.string().min(1, "user_id is required")
|
|
13872
|
+
});
|
|
13845
13873
|
// ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@4.20260418.1_@opentelemetry+api@1.9.1_bun-types@1.3.12_kysely@0.28.16/node_modules/drizzle-orm/entity.js
|
|
13846
13874
|
var entityKind = Symbol.for("drizzle:entityKind");
|
|
13847
13875
|
var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
|
|
@@ -15273,6 +15301,48 @@ var member = sqliteTable("member", {
|
|
|
15273
15301
|
globalInstruction: text("global_instruction").notNull().default(""),
|
|
15274
15302
|
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15275
15303
|
}, (t) => [unique("member_workspace_user").on(t.workspaceId, t.userId)]);
|
|
15304
|
+
var workspaceInvite = sqliteTable("workspace_invite", {
|
|
15305
|
+
id: text("id").primaryKey().$defaultFn(() => "inv_" + nanoid3()),
|
|
15306
|
+
workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
|
|
15307
|
+
token: text("token").unique().notNull().$defaultFn(() => nanoid3(32)),
|
|
15308
|
+
createdBy: text("created_by").notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
15309
|
+
usedBy: text("used_by").references(() => user.id, { onDelete: "set null" }),
|
|
15310
|
+
usedAt: text("used_at"),
|
|
15311
|
+
expiresAt: text("expires_at").notNull(),
|
|
15312
|
+
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15313
|
+
}, (t) => [
|
|
15314
|
+
index("idx_workspace_invite_token").on(t.token),
|
|
15315
|
+
index("idx_workspace_invite_workspace").on(t.workspaceId)
|
|
15316
|
+
]);
|
|
15317
|
+
var agentAccess = sqliteTable("agent_access", {
|
|
15318
|
+
id: text("id").primaryKey().$defaultFn(() => nanoid3()),
|
|
15319
|
+
agentId: text("agent_id").notNull(),
|
|
15320
|
+
workspaceId: text("workspace_id").notNull(),
|
|
15321
|
+
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
15322
|
+
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15323
|
+
}, (t) => [
|
|
15324
|
+
unique("agent_access_agent_ws_user").on(t.agentId, t.workspaceId, t.userId),
|
|
15325
|
+
index("idx_agent_access_agent_ws").on(t.agentId, t.workspaceId),
|
|
15326
|
+
index("idx_agent_access_user").on(t.userId),
|
|
15327
|
+
foreignKey({
|
|
15328
|
+
columns: [t.agentId, t.workspaceId],
|
|
15329
|
+
foreignColumns: [agent.id, agent.workspaceId]
|
|
15330
|
+
}).onDelete("cascade")
|
|
15331
|
+
]);
|
|
15332
|
+
var agentPin = sqliteTable("agent_pin", {
|
|
15333
|
+
id: text("id").primaryKey().$defaultFn(() => nanoid3()),
|
|
15334
|
+
agentId: text("agent_id").notNull(),
|
|
15335
|
+
workspaceId: text("workspace_id").notNull(),
|
|
15336
|
+
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
15337
|
+
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15338
|
+
}, (t) => [
|
|
15339
|
+
unique("agent_pin_agent_ws_user").on(t.agentId, t.workspaceId, t.userId),
|
|
15340
|
+
index("idx_agent_pin_ws_user").on(t.workspaceId, t.userId),
|
|
15341
|
+
foreignKey({
|
|
15342
|
+
columns: [t.agentId, t.workspaceId],
|
|
15343
|
+
foreignColumns: [agent.id, agent.workspaceId]
|
|
15344
|
+
}).onDelete("cascade")
|
|
15345
|
+
]);
|
|
15276
15346
|
var machine = sqliteTable("machine", {
|
|
15277
15347
|
daemonId: text("daemon_id").notNull(),
|
|
15278
15348
|
workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
|
|
@@ -15410,6 +15480,7 @@ var emails = sqliteTable("emails", {
|
|
|
15410
15480
|
htmlBody: text("html_body").notNull().default(""),
|
|
15411
15481
|
attachments: text("attachments").notNull().default("[]"),
|
|
15412
15482
|
status: text("status").notNull().default("unread"),
|
|
15483
|
+
direction: text("direction").notNull().default("inbound"),
|
|
15413
15484
|
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
15414
15485
|
}, (t) => [
|
|
15415
15486
|
foreignKey({
|
|
@@ -15532,16 +15603,33 @@ class DaemonClient {
|
|
|
15532
15603
|
"Content-Type": "application/json",
|
|
15533
15604
|
Authorization: `Bearer ${token}`
|
|
15534
15605
|
};
|
|
15535
|
-
const
|
|
15536
|
-
|
|
15537
|
-
|
|
15538
|
-
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15543
|
-
|
|
15544
|
-
|
|
15606
|
+
const MAX_RETRIES = 3;
|
|
15607
|
+
const BASE_DELAY_MS = 500;
|
|
15608
|
+
let lastError;
|
|
15609
|
+
for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
|
|
15610
|
+
try {
|
|
15611
|
+
const res = await fetch(this.baseURL + path, {
|
|
15612
|
+
method,
|
|
15613
|
+
headers,
|
|
15614
|
+
body: body ? JSON.stringify(body) : undefined
|
|
15615
|
+
});
|
|
15616
|
+
if (!res.ok)
|
|
15617
|
+
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
|
15618
|
+
if (res.status === 204)
|
|
15619
|
+
return;
|
|
15620
|
+
return res.json();
|
|
15621
|
+
} catch (e) {
|
|
15622
|
+
if (e instanceof TypeError) {
|
|
15623
|
+
lastError = e;
|
|
15624
|
+
if (attempt < MAX_RETRIES) {
|
|
15625
|
+
await new Promise((r) => setTimeout(r, BASE_DELAY_MS * 2 ** attempt));
|
|
15626
|
+
continue;
|
|
15627
|
+
}
|
|
15628
|
+
}
|
|
15629
|
+
throw e;
|
|
15630
|
+
}
|
|
15631
|
+
}
|
|
15632
|
+
throw lastError;
|
|
15545
15633
|
}
|
|
15546
15634
|
async register(token, body) {
|
|
15547
15635
|
const raw = await this.request("POST", "/api/daemon/register", token, body);
|
|
@@ -15572,15 +15660,35 @@ class DaemonClient {
|
|
|
15572
15660
|
error: error48
|
|
15573
15661
|
});
|
|
15574
15662
|
}
|
|
15663
|
+
supersedeTask(token, taskId) {
|
|
15664
|
+
return this.request("POST", `/api/daemon/tasks/${taskId}/supersede`, token);
|
|
15665
|
+
}
|
|
15575
15666
|
async getArtifactMeta(token, artifactId, workspaceId) {
|
|
15576
15667
|
return this.request("GET", `/api/artifacts/${artifactId}?workspace_id=${encodeURIComponent(workspaceId)}`, token);
|
|
15577
15668
|
}
|
|
15578
15669
|
async downloadArtifact(token, artifactId, workspaceId) {
|
|
15579
|
-
const
|
|
15580
|
-
|
|
15581
|
-
|
|
15670
|
+
const MAX_RETRIES = 3;
|
|
15671
|
+
const BASE_DELAY_MS = 500;
|
|
15672
|
+
let lastError;
|
|
15673
|
+
for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
|
|
15674
|
+
try {
|
|
15675
|
+
const res = await fetch(`${this.baseURL}/api/artifacts/${artifactId}/content?workspace_id=${encodeURIComponent(workspaceId)}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
15676
|
+
if (!res.ok) {
|
|
15677
|
+
throw new Error(`artifact download failed: HTTP ${res.status}`);
|
|
15678
|
+
}
|
|
15679
|
+
return res.arrayBuffer();
|
|
15680
|
+
} catch (e) {
|
|
15681
|
+
if (e instanceof TypeError) {
|
|
15682
|
+
lastError = e;
|
|
15683
|
+
if (attempt < MAX_RETRIES) {
|
|
15684
|
+
await new Promise((r) => setTimeout(r, BASE_DELAY_MS * 2 ** attempt));
|
|
15685
|
+
continue;
|
|
15686
|
+
}
|
|
15687
|
+
}
|
|
15688
|
+
throw e;
|
|
15689
|
+
}
|
|
15582
15690
|
}
|
|
15583
|
-
|
|
15691
|
+
throw lastError;
|
|
15584
15692
|
}
|
|
15585
15693
|
reportMessages(token, taskId, messages) {
|
|
15586
15694
|
return this.request("POST", `/api/daemon/tasks/${taskId}/messages`, token, { messages });
|
|
@@ -16547,6 +16655,7 @@ Your current context is only a fraction of the full timeline of what's your have
|
|
|
16547
16655
|
The full context timeline is inside './.context_timeline/YYYY-MM-DD.jsonl'.
|
|
16548
16656
|
Each line of a timeline JSONL is a JSON object with these fields:
|
|
16549
16657
|
- "task_id" — unique task identifier
|
|
16658
|
+
- "context_key" — thread identifier, the same context key meaning those tasks are in the same thread.
|
|
16550
16659
|
- "session_id" — agent session identifier (null until completion)
|
|
16551
16660
|
- "pid" — daemon process ID (present while running, null when done)
|
|
16552
16661
|
- "status" — "running", "completed", or "failed"
|
|
@@ -16555,18 +16664,21 @@ Each line of a timeline JSONL is a JSON object with these fields:
|
|
|
16555
16664
|
- "prompt" — what the user asked
|
|
16556
16665
|
- "agent_responses" — assistant text outputs during execution
|
|
16557
16666
|
- "errmsg" — error message (null unless status is "failed")
|
|
16667
|
+
- "detailed_log" — the running log for this task, including the actually task agent messages with tool-calls and failed logs.
|
|
16558
16668
|
those json are sorted by datetime in asc order.
|
|
16559
16669
|
|
|
16560
16670
|
## BE HUMAN
|
|
16561
16671
|
- When you recall the timelines, don't say "I find ... in my timeline jsonl". Just say "I can recall..."
|
|
16562
16672
|
- User don't know about the context timeline jsonl, this is your secret weapon.
|
|
16563
16673
|
- Act and response like you're human and actually have memory, not a file reading machine.
|
|
16674
|
+
- Don't report your agent id and workspace id when not asked, it will break the image that you're a human colleague.
|
|
16564
16675
|
|
|
16565
16676
|
## RULES
|
|
16566
16677
|
- Read @memory.md(if exists) before your action.
|
|
16567
16678
|
- When you start a new task, read the last ~10 lines of today's timeline to understand what has been asked and done recently.
|
|
16568
16679
|
- if you don't know the current datetime, obtain the current datetime first.
|
|
16569
16680
|
- When user ask you something you don't have in your current context, try to read the timeline jsonl files for answer (today or previous days).
|
|
16681
|
+
- Use grep tool to search in the context timeline jsonls if you have clean and focus keywords to recall.
|
|
16570
16682
|
- When access other local projects, make sure you read the CLAUDE.md/AGENTS.md file under the project root dir to understand the requirements.
|
|
16571
16683
|
`;
|
|
16572
16684
|
function buildInstructionContent(task) {
|
|
@@ -16600,7 +16712,7 @@ ${task.agent?.userEmail ? `Your owner's email address is '${task.agent.userEmail
|
|
|
16600
16712
|
|
|
16601
16713
|
### Emails
|
|
16602
16714
|
---
|
|
16603
|
-
Run 'npx @alook/cli email pull --agent_id ${task.agentId} --
|
|
16715
|
+
Run 'npx @alook/cli email pull --agent_id ${task.agentId} --status unread' to download unread emails to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/'.
|
|
16604
16716
|
Each email is saved to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/<emailId>/' with:
|
|
16605
16717
|
- 'metadata.json' — sender, recipient, subject, date, status, message_id, in_reply_to, references
|
|
16606
16718
|
- 'body.txt' — plain text body
|
|
@@ -16608,22 +16720,31 @@ Each email is saved to '/tmp/alook-emails/${task.workspaceId}/${task.agentId}/<e
|
|
|
16608
16720
|
- 'attachments/' — extracted attachment files (if any)
|
|
16609
16721
|
---
|
|
16610
16722
|
Before starting to process an email, mark it as read:
|
|
16611
|
-
- Run 'npx @alook/cli email set --agent_id ${task.agentId} --
|
|
16723
|
+
- Run 'npx @alook/cli email set --agent_id ${task.agentId} --email_id <EMAIL_ID> --status read'
|
|
16612
16724
|
---
|
|
16613
16725
|
|
|
16614
16726
|
#### Sending a new email
|
|
16615
16727
|
Write the HTML body to a file first, then send it. The body is forwarded as-is (HTML).
|
|
16616
|
-
- Run 'npx @alook/cli email send --agent_id ${task.agentId} --
|
|
16728
|
+
- Run 'npx @alook/cli email send --agent_id ${task.agentId} --to <ADDRESS> --subject "<SUBJECT>" --body-file <PATH_TO_HTML>'
|
|
16617
16729
|
- To send from a specific mailbox, add '--from <YOUR_EMAIL_ADDRESS>'. Without '--from', the default Alook address is used.
|
|
16618
16730
|
- Attach files with '--attachment <PATH>' — repeat the flag for multiple attachments. Each file is uploaded before sending.
|
|
16619
|
-
- Example: 'npx @alook/cli email send --agent_id ${task.agentId} --
|
|
16731
|
+
- Example: 'npx @alook/cli email send --agent_id ${task.agentId} --to foo@bar.com --subject "Weekly report" --body-file /tmp/body.html --from alice@company.com --attachment /tmp/report.pdf'
|
|
16620
16732
|
|
|
16621
16733
|
#### Replying to an email
|
|
16622
16734
|
To reply to an email, add '--in-reply-to <EMAIL_ID>' to the send command. This sets the correct email threading headers so the recipient's email client groups the reply into the same conversation thread.
|
|
16623
16735
|
- Use 'Re: <original subject>' as the subject.
|
|
16624
16736
|
- Quote the original email body in your reply (wrap it in a blockquote).
|
|
16625
16737
|
- The <EMAIL_ID> is the Alook email id from metadata.json (not the message_id header).
|
|
16626
|
-
- Example: 'npx @alook/cli email send --agent_id ${task.agentId} --
|
|
16738
|
+
- Example: 'npx @alook/cli email send --agent_id ${task.agentId} --to sender@example.com --subject "Re: Bug report" --body-file /tmp/reply.html --in-reply-to <EMAIL_ID>'
|
|
16739
|
+
Tips:
|
|
16740
|
+
- If you think the task will take a while, consider sending a short "I'm on it" style email reply first to reassure the sender.
|
|
16741
|
+
---
|
|
16742
|
+
|
|
16743
|
+
#### Email Whitelist (Allowed Senders)
|
|
16744
|
+
Manage which email addresses are allowed to send you emails.
|
|
16745
|
+
- List: 'npx @alook/cli email whitelist list --agent_id ${task.agentId}' (add '--json' for machine-readable output)
|
|
16746
|
+
- Add: 'npx @alook/cli email whitelist add --agent_id ${task.agentId} <EMAIL_ADDRESS>'
|
|
16747
|
+
- Remove: 'npx @alook/cli email whitelist delete --agent_id ${task.agentId} <EMAIL_ADDRESS>'
|
|
16627
16748
|
---
|
|
16628
16749
|
`;
|
|
16629
16750
|
}
|
|
@@ -16633,6 +16754,8 @@ Upload files for your owner to review in the app.
|
|
|
16633
16754
|
- Your current conversation id is available via env var: $ALOOK_CONVERSATION_ID
|
|
16634
16755
|
- Run 'npx @alook/cli sync upload-artifact --agent_id ${task.agentId} --conversation_id $ALOOK_CONVERSATION_ID --file <PATH>'
|
|
16635
16756
|
- Use this after generating plans, reports, or any file the owner should review.
|
|
16757
|
+
- You response will be rendered in remote server, so don't output link format with local path in your response (cause user can click it and jump to nowheres)
|
|
16758
|
+
- If you think user may need to know any file detail, use upload-artifact tool to send the file to user.
|
|
16636
16759
|
---
|
|
16637
16760
|
|
|
16638
16761
|
### Attachments
|
|
@@ -17024,7 +17147,7 @@ function findResumableSessionByContextKey(timelineDir, contextKey, provider) {
|
|
|
17024
17147
|
}
|
|
17025
17148
|
entries.sort((a, b) => new Date(b.datetime).getTime() - new Date(a.datetime).getTime());
|
|
17026
17149
|
for (const entry of entries) {
|
|
17027
|
-
if (entry.status
|
|
17150
|
+
if (entry.status !== "running" && entry.context_key === contextKey && entry.provider === provider && entry.session_id && new Date(entry.datetime) >= cutoff) {
|
|
17028
17151
|
return entry.session_id;
|
|
17029
17152
|
}
|
|
17030
17153
|
}
|
|
@@ -17048,9 +17171,37 @@ function prepare(config2, task) {
|
|
|
17048
17171
|
return { workDir, timelineDir, env };
|
|
17049
17172
|
}
|
|
17050
17173
|
|
|
17174
|
+
// daemon/execenv/steering.ts
|
|
17175
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync3, unlinkSync as unlinkSync2, readdirSync, statSync as statSync2 } from "fs";
|
|
17176
|
+
import { join as join4 } from "path";
|
|
17177
|
+
var INTENT_DIR_NAME = ".kill_intents";
|
|
17178
|
+
var INTENT_STALE_MS = 10 * 60 * 1000;
|
|
17179
|
+
function intentFilePath(baseDir, taskId) {
|
|
17180
|
+
return join4(baseDir, INTENT_DIR_NAME, `${taskId}.json`);
|
|
17181
|
+
}
|
|
17182
|
+
function readKillIntent(baseDir, taskId) {
|
|
17183
|
+
const filePath = intentFilePath(baseDir, taskId);
|
|
17184
|
+
try {
|
|
17185
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
17186
|
+
return JSON.parse(content);
|
|
17187
|
+
} catch {
|
|
17188
|
+
return null;
|
|
17189
|
+
}
|
|
17190
|
+
}
|
|
17191
|
+
function clearKillIntent(baseDir, taskId) {
|
|
17192
|
+
const filePath = intentFilePath(baseDir, taskId);
|
|
17193
|
+
try {
|
|
17194
|
+
unlinkSync2(filePath);
|
|
17195
|
+
} catch {}
|
|
17196
|
+
}
|
|
17197
|
+
|
|
17051
17198
|
// daemon/prompt.ts
|
|
17199
|
+
var EMAIL_NOTICE = "This task was triggered automatically by an incoming email. There is no human in this session." + " If you need to communicate with a human, you MUST send an email using the email sending tool." + " If you need more information or confirmation from the human, send them an email asking for it and then exit." + " Do not wait — when the human replies, a new task will be triggered automatically and you will be woken up with their response.";
|
|
17052
17200
|
function buildPrompt(task, attachments) {
|
|
17053
17201
|
const obj = { type: task.type, instruction: task.prompt };
|
|
17202
|
+
if (task.type === "email_notification") {
|
|
17203
|
+
obj.notice = EMAIL_NOTICE;
|
|
17204
|
+
}
|
|
17054
17205
|
if (attachments && attachments.length > 0) {
|
|
17055
17206
|
obj.attachments = attachments.map((a) => ({
|
|
17056
17207
|
path: a.path,
|
|
@@ -17144,6 +17295,7 @@ async function runSession(input) {
|
|
|
17144
17295
|
};
|
|
17145
17296
|
const flushTimer = setInterval(flushMessages, FLUSH_INTERVAL_MS);
|
|
17146
17297
|
let killed = false;
|
|
17298
|
+
const agentBaseDir = path.dirname(timelineDir);
|
|
17147
17299
|
const onKill = async () => {
|
|
17148
17300
|
if (killed)
|
|
17149
17301
|
return;
|
|
@@ -17159,14 +17311,37 @@ async function runSession(input) {
|
|
|
17159
17311
|
await flushMessages();
|
|
17160
17312
|
} catch {}
|
|
17161
17313
|
await cleanupAttachments(task.id);
|
|
17162
|
-
|
|
17163
|
-
|
|
17164
|
-
|
|
17165
|
-
|
|
17166
|
-
|
|
17167
|
-
|
|
17168
|
-
|
|
17169
|
-
|
|
17314
|
+
const intent = readKillIntent(agentBaseDir, task.id);
|
|
17315
|
+
clearKillIntent(agentBaseDir, task.id);
|
|
17316
|
+
if (intent?.reason === "superseded") {
|
|
17317
|
+
updateEntry(timelineDir, task.id, (entry) => {
|
|
17318
|
+
entry.pid = null;
|
|
17319
|
+
entry.status = "superseded";
|
|
17320
|
+
entry.successor_task_id = intent.successorTaskId ?? null;
|
|
17321
|
+
entry.supersede_reason = "superseded by newer task";
|
|
17322
|
+
});
|
|
17323
|
+
try {
|
|
17324
|
+
await client.supersedeTask(token, task.id);
|
|
17325
|
+
} catch {}
|
|
17326
|
+
} else if (intent?.reason === "cancelled") {
|
|
17327
|
+
updateEntry(timelineDir, task.id, (entry) => {
|
|
17328
|
+
entry.pid = null;
|
|
17329
|
+
entry.status = "cancelled";
|
|
17330
|
+
entry.errmsg = "cancelled by user";
|
|
17331
|
+
});
|
|
17332
|
+
try {
|
|
17333
|
+
await client.failTask(token, task.id, "cancelled by user");
|
|
17334
|
+
} catch {}
|
|
17335
|
+
} else {
|
|
17336
|
+
updateEntry(timelineDir, task.id, (entry) => {
|
|
17337
|
+
entry.pid = null;
|
|
17338
|
+
entry.status = "killed";
|
|
17339
|
+
entry.errmsg = "killed by signal";
|
|
17340
|
+
});
|
|
17341
|
+
try {
|
|
17342
|
+
await client.failTask(token, task.id, "killed by signal");
|
|
17343
|
+
} catch {}
|
|
17344
|
+
}
|
|
17170
17345
|
process.exit(1);
|
|
17171
17346
|
};
|
|
17172
17347
|
process.on("SIGTERM", onKill);
|