@alook/cli 0.0.15 → 0.0.17
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 +592 -55
- package/dist/session-runner.js +221 -31
- 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({
|
|
@@ -15782,6 +15853,7 @@ var machineToken = sqliteTable("machine_token", {
|
|
|
15782
15853
|
}, (t) => [index("idx_machine_token").on(t.token)]);
|
|
15783
15854
|
// ../shared/src/db/queries/task.ts
|
|
15784
15855
|
var DEFAULT_STALE_SECONDS = Number(process.env.ALOOK_STALE_DISPATCH_TIMEOUT_S) || 20;
|
|
15856
|
+
var DEFAULT_STALE_RUNNING_SECONDS = Number(process.env.ALOOK_STALE_RUNNING_TIMEOUT_S) || 3600;
|
|
15785
15857
|
// ../shared/src/utils/email.ts
|
|
15786
15858
|
var DOMAIN = `@${process.env.ALOOK_DOMAIN || "alook.ai"}`;
|
|
15787
15859
|
var RESERVED_HANDLES = new Set([
|
|
@@ -15883,6 +15955,9 @@ class DaemonClient {
|
|
|
15883
15955
|
error: error48
|
|
15884
15956
|
});
|
|
15885
15957
|
}
|
|
15958
|
+
supersedeTask(token, taskId) {
|
|
15959
|
+
return this.request("POST", `/api/daemon/tasks/${taskId}/supersede`, token);
|
|
15960
|
+
}
|
|
15886
15961
|
async getArtifactMeta(token, artifactId, workspaceId) {
|
|
15887
15962
|
return this.request("GET", `/api/artifacts/${artifactId}?workspace_id=${encodeURIComponent(workspaceId)}`, token);
|
|
15888
15963
|
}
|
|
@@ -16010,6 +16085,7 @@ function loadDaemonConfig(profile) {
|
|
|
16010
16085
|
opencodeModel: process.env.ALOOK_OPENCODE_MODEL || "",
|
|
16011
16086
|
pollInterval: parseDuration(process.env.ALOOK_DAEMON_POLL_INTERVAL || "3s"),
|
|
16012
16087
|
agentTimeout: parseDuration(process.env.ALOOK_AGENT_TIMEOUT || "12h"),
|
|
16088
|
+
messageInactivityTimeout: parseDuration(process.env.ALOOK_MESSAGE_INACTIVITY_TIMEOUT || "5m"),
|
|
16013
16089
|
maxConcurrentTasks: parseInt(process.env.ALOOK_DAEMON_MAX_CONCURRENT_TASKS || "20"),
|
|
16014
16090
|
daemonId,
|
|
16015
16091
|
deviceName: process.env.ALOOK_DAEMON_DEVICE_NAME || h,
|
|
@@ -16316,13 +16392,173 @@ async function handleCliUpdate(version3, onSuccess, profile) {
|
|
|
16316
16392
|
}
|
|
16317
16393
|
}
|
|
16318
16394
|
|
|
16395
|
+
// daemon/execenv/timeline.ts
|
|
16396
|
+
import { appendFileSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync } from "fs";
|
|
16397
|
+
import { join as join4 } from "path";
|
|
16398
|
+
|
|
16399
|
+
// daemon/execenv/filelock.ts
|
|
16400
|
+
import { mkdirSync as mkdirSync3, rmdirSync, statSync } from "fs";
|
|
16401
|
+
var DEFAULT_STALE_MS = 3600000;
|
|
16402
|
+
function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
|
|
16403
|
+
try {
|
|
16404
|
+
mkdirSync3(lockPath);
|
|
16405
|
+
return true;
|
|
16406
|
+
} catch {
|
|
16407
|
+
try {
|
|
16408
|
+
const stat = statSync(lockPath);
|
|
16409
|
+
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
16410
|
+
rmdirSync(lockPath);
|
|
16411
|
+
try {
|
|
16412
|
+
mkdirSync3(lockPath);
|
|
16413
|
+
return true;
|
|
16414
|
+
} catch {
|
|
16415
|
+
return false;
|
|
16416
|
+
}
|
|
16417
|
+
}
|
|
16418
|
+
} catch {
|
|
16419
|
+
try {
|
|
16420
|
+
mkdirSync3(lockPath);
|
|
16421
|
+
return true;
|
|
16422
|
+
} catch {
|
|
16423
|
+
return false;
|
|
16424
|
+
}
|
|
16425
|
+
}
|
|
16426
|
+
return false;
|
|
16427
|
+
}
|
|
16428
|
+
}
|
|
16429
|
+
function releaseLock(lockPath) {
|
|
16430
|
+
try {
|
|
16431
|
+
rmdirSync(lockPath);
|
|
16432
|
+
} catch {}
|
|
16433
|
+
}
|
|
16434
|
+
|
|
16435
|
+
// daemon/execenv/timeline.ts
|
|
16436
|
+
function readJsonl(filePath) {
|
|
16437
|
+
let content;
|
|
16438
|
+
try {
|
|
16439
|
+
content = readFileSync5(filePath, "utf-8");
|
|
16440
|
+
} catch {
|
|
16441
|
+
return [];
|
|
16442
|
+
}
|
|
16443
|
+
const entries = [];
|
|
16444
|
+
for (const line of content.trimEnd().split(`
|
|
16445
|
+
`)) {
|
|
16446
|
+
if (!line)
|
|
16447
|
+
continue;
|
|
16448
|
+
try {
|
|
16449
|
+
entries.push(JSON.parse(line));
|
|
16450
|
+
} catch {}
|
|
16451
|
+
}
|
|
16452
|
+
return entries;
|
|
16453
|
+
}
|
|
16454
|
+
function filenameForDate(date5) {
|
|
16455
|
+
const y = date5.getFullYear();
|
|
16456
|
+
const m = String(date5.getMonth() + 1).padStart(2, "0");
|
|
16457
|
+
const d = String(date5.getDate()).padStart(2, "0");
|
|
16458
|
+
return `${y}-${m}-${d}.jsonl`;
|
|
16459
|
+
}
|
|
16460
|
+
function recentFilenames(maxDays) {
|
|
16461
|
+
const filenames = [];
|
|
16462
|
+
const now = new Date;
|
|
16463
|
+
for (let i = 0;i < maxDays; i++) {
|
|
16464
|
+
const d = new Date(now);
|
|
16465
|
+
d.setDate(d.getDate() - i);
|
|
16466
|
+
filenames.push(filenameForDate(d));
|
|
16467
|
+
}
|
|
16468
|
+
return filenames;
|
|
16469
|
+
}
|
|
16470
|
+
var DEFAULT_RESUME_MAX_AGE_MS = 3 * 60 * 60 * 1000;
|
|
16471
|
+
var EMAIL_RESUME_MAX_AGE_MS = 48 * 60 * 60 * 1000;
|
|
16472
|
+
function findRunningPidByTaskId(timelineDir, taskId) {
|
|
16473
|
+
for (const filename of recentFilenames(7)) {
|
|
16474
|
+
const entries = readJsonl(join4(timelineDir, filename));
|
|
16475
|
+
for (const entry of entries) {
|
|
16476
|
+
if (entry.task_id === taskId && entry.status === "running" && entry.pid != null) {
|
|
16477
|
+
return entry.pid;
|
|
16478
|
+
}
|
|
16479
|
+
}
|
|
16480
|
+
}
|
|
16481
|
+
return null;
|
|
16482
|
+
}
|
|
16483
|
+
function findRunningEntryByContextKey(timelineDir, contextKey, provider) {
|
|
16484
|
+
for (const filename of recentFilenames(7)) {
|
|
16485
|
+
const dayEntries = readJsonl(join4(timelineDir, filename));
|
|
16486
|
+
for (let i = dayEntries.length - 1;i >= 0; i--) {
|
|
16487
|
+
const entry = dayEntries[i];
|
|
16488
|
+
if (entry.status === "running" && entry.context_key === contextKey && entry.provider === provider) {
|
|
16489
|
+
return entry;
|
|
16490
|
+
}
|
|
16491
|
+
}
|
|
16492
|
+
}
|
|
16493
|
+
return null;
|
|
16494
|
+
}
|
|
16495
|
+
|
|
16496
|
+
// daemon/execenv/steering.ts
|
|
16497
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync3, readdirSync, statSync as statSync2 } from "fs";
|
|
16498
|
+
import { join as join5 } from "path";
|
|
16499
|
+
var INTENT_DIR_NAME = ".kill_intents";
|
|
16500
|
+
var STEERING_LOCK_DIR = ".steering_locks";
|
|
16501
|
+
var INTENT_STALE_MS = 10 * 60 * 1000;
|
|
16502
|
+
function intentFilePath(baseDir, taskId) {
|
|
16503
|
+
return join5(baseDir, INTENT_DIR_NAME, `${taskId}.json`);
|
|
16504
|
+
}
|
|
16505
|
+
function intentDirPath(baseDir) {
|
|
16506
|
+
return join5(baseDir, INTENT_DIR_NAME);
|
|
16507
|
+
}
|
|
16508
|
+
function steeringLockPath(baseDir, contextKey) {
|
|
16509
|
+
const safeKey = contextKey.replace(/[^a-zA-Z0-9_:-]/g, "_");
|
|
16510
|
+
return join5(baseDir, STEERING_LOCK_DIR, safeKey);
|
|
16511
|
+
}
|
|
16512
|
+
function writeKillIntent(baseDir, intent) {
|
|
16513
|
+
const dir = intentDirPath(baseDir);
|
|
16514
|
+
try {
|
|
16515
|
+
mkdirSync4(dir, { recursive: true });
|
|
16516
|
+
} catch {}
|
|
16517
|
+
const filePath = intentFilePath(baseDir, intent.targetTaskId);
|
|
16518
|
+
writeFileSync5(filePath, JSON.stringify(intent));
|
|
16519
|
+
}
|
|
16520
|
+
function cleanupStaleIntents(baseDir) {
|
|
16521
|
+
const dir = intentDirPath(baseDir);
|
|
16522
|
+
let files;
|
|
16523
|
+
try {
|
|
16524
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
16525
|
+
} catch {
|
|
16526
|
+
return;
|
|
16527
|
+
}
|
|
16528
|
+
const now = Date.now();
|
|
16529
|
+
for (const file2 of files) {
|
|
16530
|
+
const filePath = join5(dir, file2);
|
|
16531
|
+
try {
|
|
16532
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
16533
|
+
const intent = JSON.parse(content);
|
|
16534
|
+
const stat = statSync2(filePath);
|
|
16535
|
+
if (now - stat.mtimeMs > INTENT_STALE_MS) {
|
|
16536
|
+
unlinkSync3(filePath);
|
|
16537
|
+
log.debug(`Cleaned up stale kill intent for task ${intent.targetTaskId}`);
|
|
16538
|
+
}
|
|
16539
|
+
} catch {}
|
|
16540
|
+
}
|
|
16541
|
+
}
|
|
16542
|
+
function acquireSteeringLock(baseDir, contextKey) {
|
|
16543
|
+
const lockPath = steeringLockPath(baseDir, contextKey);
|
|
16544
|
+
try {
|
|
16545
|
+
mkdirSync4(join5(baseDir, STEERING_LOCK_DIR), { recursive: true });
|
|
16546
|
+
} catch {}
|
|
16547
|
+
return acquireLock(lockPath, 60000);
|
|
16548
|
+
}
|
|
16549
|
+
function releaseSteeringLock(baseDir, contextKey) {
|
|
16550
|
+
const lockPath = steeringLockPath(baseDir, contextKey);
|
|
16551
|
+
releaseLock(lockPath);
|
|
16552
|
+
}
|
|
16553
|
+
|
|
16319
16554
|
// daemon/daemon.ts
|
|
16320
|
-
import { existsSync, mkdirSync as
|
|
16555
|
+
import { existsSync, mkdirSync as mkdirSync5, openSync, closeSync, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
16556
|
+
import { readdir, readFile, unlink, stat as fsStat } from "fs/promises";
|
|
16321
16557
|
import { execSync as execSync3, spawn as spawn2 } from "child_process";
|
|
16322
16558
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
16323
|
-
import { dirname as dirname3, join as
|
|
16559
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
16324
16560
|
var _dir = dirname3(fileURLToPath2(import.meta.url));
|
|
16325
|
-
var sessionRunnerPath = existsSync(
|
|
16561
|
+
var sessionRunnerPath = existsSync(join6(_dir, "session-runner.js")) ? join6(_dir, "session-runner.js") : join6(_dir, "session-runner.ts");
|
|
16326
16562
|
function isCommandAvailable2(cmd) {
|
|
16327
16563
|
try {
|
|
16328
16564
|
const check2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
|
|
@@ -16332,21 +16568,21 @@ function isCommandAvailable2(cmd) {
|
|
|
16332
16568
|
return false;
|
|
16333
16569
|
}
|
|
16334
16570
|
}
|
|
16335
|
-
var MAX_SESSION_RUNNER_LOGS =
|
|
16571
|
+
var MAX_SESSION_RUNNER_LOGS = 500;
|
|
16336
16572
|
function pruneSessionRunnerLogs() {
|
|
16337
16573
|
const logDir = sessionRunnerLogDir();
|
|
16338
16574
|
let entries;
|
|
16339
16575
|
try {
|
|
16340
|
-
entries =
|
|
16576
|
+
entries = readdirSync2(logDir).filter((f) => f.endsWith(".log"));
|
|
16341
16577
|
} catch {
|
|
16342
16578
|
return;
|
|
16343
16579
|
}
|
|
16344
16580
|
if (entries.length <= MAX_SESSION_RUNNER_LOGS)
|
|
16345
16581
|
return;
|
|
16346
16582
|
const withMtime = entries.map((name) => {
|
|
16347
|
-
const full =
|
|
16583
|
+
const full = join6(logDir, name);
|
|
16348
16584
|
try {
|
|
16349
|
-
return { name, mtime:
|
|
16585
|
+
return { name, mtime: statSync3(full).mtimeMs };
|
|
16350
16586
|
} catch {
|
|
16351
16587
|
return { name, mtime: 0 };
|
|
16352
16588
|
}
|
|
@@ -16354,10 +16590,126 @@ function pruneSessionRunnerLogs() {
|
|
|
16354
16590
|
withMtime.sort((a, b) => b.mtime - a.mtime);
|
|
16355
16591
|
for (const entry of withMtime.slice(MAX_SESSION_RUNNER_LOGS)) {
|
|
16356
16592
|
try {
|
|
16357
|
-
|
|
16593
|
+
unlinkSync4(join6(logDir, entry.name));
|
|
16358
16594
|
} catch {}
|
|
16359
16595
|
}
|
|
16360
16596
|
}
|
|
16597
|
+
function isClientError(error48) {
|
|
16598
|
+
if (!(error48 instanceof Error))
|
|
16599
|
+
return false;
|
|
16600
|
+
const match = error48.message.match(/^HTTP (\d+):/);
|
|
16601
|
+
if (!match)
|
|
16602
|
+
return false;
|
|
16603
|
+
const status = Number(match[1]);
|
|
16604
|
+
if (status === 408 || status === 429)
|
|
16605
|
+
return false;
|
|
16606
|
+
return status >= 400 && status < 500;
|
|
16607
|
+
}
|
|
16608
|
+
function isValidMarker(data) {
|
|
16609
|
+
if (!data || typeof data !== "object")
|
|
16610
|
+
return false;
|
|
16611
|
+
const d = data;
|
|
16612
|
+
if (typeof d.taskId !== "string")
|
|
16613
|
+
return false;
|
|
16614
|
+
if (typeof d.token !== "string")
|
|
16615
|
+
return false;
|
|
16616
|
+
if (typeof d.serverURL !== "string")
|
|
16617
|
+
return false;
|
|
16618
|
+
if (typeof d.createdAt !== "string" || isNaN(new Date(d.createdAt).getTime()))
|
|
16619
|
+
return false;
|
|
16620
|
+
if (!d.payload || typeof d.payload !== "object")
|
|
16621
|
+
return false;
|
|
16622
|
+
const payload = d.payload;
|
|
16623
|
+
if (d.type === "complete") {
|
|
16624
|
+
return typeof payload.output === "string";
|
|
16625
|
+
}
|
|
16626
|
+
if (d.type === "fail") {
|
|
16627
|
+
return typeof payload.error === "string";
|
|
16628
|
+
}
|
|
16629
|
+
return false;
|
|
16630
|
+
}
|
|
16631
|
+
var MARKER_STALE_MS = 24 * 60 * 60 * 1000;
|
|
16632
|
+
var TMP_STALE_MS = 60 * 60 * 1000;
|
|
16633
|
+
async function reconcilePendingCompletions(workspacesRoot) {
|
|
16634
|
+
const dir = join6(workspacesRoot, ".pending_completions");
|
|
16635
|
+
let entries;
|
|
16636
|
+
try {
|
|
16637
|
+
entries = await readdir(dir);
|
|
16638
|
+
} catch {
|
|
16639
|
+
return;
|
|
16640
|
+
}
|
|
16641
|
+
for (const name of entries) {
|
|
16642
|
+
if (!name.endsWith(".tmp"))
|
|
16643
|
+
continue;
|
|
16644
|
+
try {
|
|
16645
|
+
const s = await fsStat(join6(dir, name));
|
|
16646
|
+
if (Date.now() - s.mtimeMs > TMP_STALE_MS) {
|
|
16647
|
+
await unlink(join6(dir, name));
|
|
16648
|
+
}
|
|
16649
|
+
} catch {}
|
|
16650
|
+
}
|
|
16651
|
+
const jsonFiles = entries.filter((f) => f.endsWith(".json"));
|
|
16652
|
+
for (const name of jsonFiles) {
|
|
16653
|
+
const filePath = join6(dir, name);
|
|
16654
|
+
try {
|
|
16655
|
+
let raw;
|
|
16656
|
+
try {
|
|
16657
|
+
raw = await readFile(filePath, "utf-8");
|
|
16658
|
+
} catch {
|
|
16659
|
+
continue;
|
|
16660
|
+
}
|
|
16661
|
+
let parsed;
|
|
16662
|
+
try {
|
|
16663
|
+
parsed = JSON.parse(raw);
|
|
16664
|
+
} catch {
|
|
16665
|
+
log.warn(`reconcile: malformed marker ${name}, deleting`);
|
|
16666
|
+
try {
|
|
16667
|
+
await unlink(filePath);
|
|
16668
|
+
} catch {}
|
|
16669
|
+
continue;
|
|
16670
|
+
}
|
|
16671
|
+
if (!isValidMarker(parsed)) {
|
|
16672
|
+
log.warn(`reconcile: invalid marker structure ${name}, deleting`);
|
|
16673
|
+
try {
|
|
16674
|
+
await unlink(filePath);
|
|
16675
|
+
} catch {}
|
|
16676
|
+
continue;
|
|
16677
|
+
}
|
|
16678
|
+
const marker = parsed;
|
|
16679
|
+
const age = Date.now() - new Date(marker.createdAt).getTime();
|
|
16680
|
+
if (age > MARKER_STALE_MS) {
|
|
16681
|
+
log.warn(`reconcile: stale marker ${name} (${Math.round(age / 3600000)}h old), deleting`);
|
|
16682
|
+
try {
|
|
16683
|
+
await unlink(filePath);
|
|
16684
|
+
} catch {}
|
|
16685
|
+
continue;
|
|
16686
|
+
}
|
|
16687
|
+
const client = new DaemonClient(marker.serverURL);
|
|
16688
|
+
try {
|
|
16689
|
+
if (marker.type === "complete") {
|
|
16690
|
+
await client.completeTask(marker.token, marker.taskId, marker.payload);
|
|
16691
|
+
} else {
|
|
16692
|
+
await client.failTask(marker.token, marker.taskId, marker.payload.error);
|
|
16693
|
+
}
|
|
16694
|
+
try {
|
|
16695
|
+
await unlink(filePath);
|
|
16696
|
+
} catch (delErr) {
|
|
16697
|
+
log.warn(`reconcile: delivered marker ${name} but failed to delete: ${delErr}`);
|
|
16698
|
+
}
|
|
16699
|
+
} catch (deliverErr) {
|
|
16700
|
+
if (isClientError(deliverErr)) {
|
|
16701
|
+
try {
|
|
16702
|
+
await unlink(filePath);
|
|
16703
|
+
} catch {}
|
|
16704
|
+
} else {
|
|
16705
|
+
log.debug(`reconcile: delivery failed for ${name}, will retry next cycle`);
|
|
16706
|
+
}
|
|
16707
|
+
}
|
|
16708
|
+
} catch (e) {
|
|
16709
|
+
log.debug(`reconcile: error processing ${name}`, e);
|
|
16710
|
+
}
|
|
16711
|
+
}
|
|
16712
|
+
}
|
|
16361
16713
|
async function startDaemon(profile, serverUrl) {
|
|
16362
16714
|
pruneSessionRunnerLogs();
|
|
16363
16715
|
if (!acquireDaemonPid(profile)) {
|
|
@@ -16433,12 +16785,7 @@ async function startDaemon(profile, serverUrl) {
|
|
|
16433
16785
|
});
|
|
16434
16786
|
} catch (e) {
|
|
16435
16787
|
if (e instanceof Error && e.message.startsWith("HTTP 401")) {
|
|
16436
|
-
log.warn(`Workspace ${ws.id} token invalid —
|
|
16437
|
-
try {
|
|
16438
|
-
const cfg = loadCLIConfigForProfile(profile);
|
|
16439
|
-
cfg.watched_workspaces = (cfg.watched_workspaces || []).filter((w) => w.id !== ws.id);
|
|
16440
|
-
saveCLIConfigForProfile(profile, cfg);
|
|
16441
|
-
} catch {}
|
|
16788
|
+
log.warn(`Workspace ${ws.id} token invalid — skipping (run '${cmdPrefix()} register --token <token>' to fix)`);
|
|
16442
16789
|
} else {
|
|
16443
16790
|
log.error(`Failed to register workspace ${ws.id}, skipping`, e);
|
|
16444
16791
|
}
|
|
@@ -16497,7 +16844,7 @@ async function startDaemon(profile, serverUrl) {
|
|
|
16497
16844
|
cfg.watched_workspaces = (cfg.watched_workspaces || []).filter((w) => w.id !== workspaceId);
|
|
16498
16845
|
saveCLIConfigForProfile(profile, cfg);
|
|
16499
16846
|
} catch {}
|
|
16500
|
-
log.info(`Workspace ${workspaceId}
|
|
16847
|
+
log.info(`Workspace ${workspaceId} deleted server-side — removed from config`);
|
|
16501
16848
|
}
|
|
16502
16849
|
const pollCycle = async () => {
|
|
16503
16850
|
let remaining = config2.maxConcurrentTasks - activeTasks.size;
|
|
@@ -16534,7 +16881,7 @@ async function startDaemon(profile, serverUrl) {
|
|
|
16534
16881
|
}
|
|
16535
16882
|
} catch (e) {
|
|
16536
16883
|
if (e instanceof Error && e.message.startsWith("HTTP 401")) {
|
|
16537
|
-
|
|
16884
|
+
log.warn(`Workspace ${ws.workspaceId} poll returned 401 — will retry next cycle`);
|
|
16538
16885
|
} else {
|
|
16539
16886
|
log.debug("Poll error", e);
|
|
16540
16887
|
}
|
|
@@ -16543,6 +16890,11 @@ async function startDaemon(profile, serverUrl) {
|
|
|
16543
16890
|
for (const id of evictedIds) {
|
|
16544
16891
|
evictWorkspace(id);
|
|
16545
16892
|
}
|
|
16893
|
+
try {
|
|
16894
|
+
await reconcilePendingCompletions(config2.workspacesRoot);
|
|
16895
|
+
} catch (e) {
|
|
16896
|
+
log.debug("reconciliation error", e);
|
|
16897
|
+
}
|
|
16546
16898
|
if (workspaceStates.length === 0) {
|
|
16547
16899
|
log.info("All workspaces evicted — shutting down");
|
|
16548
16900
|
shutdown();
|
|
@@ -16571,17 +16923,28 @@ async function startDaemon(profile, serverUrl) {
|
|
|
16571
16923
|
releaseDaemonPid(profile);
|
|
16572
16924
|
health.server.close(() => {
|
|
16573
16925
|
if (restartRequested) {
|
|
16574
|
-
const
|
|
16926
|
+
const entry = process.argv[1];
|
|
16927
|
+
const args = [entry, "daemon", "start", "--foreground"];
|
|
16575
16928
|
if (profile)
|
|
16576
16929
|
args.push("--profile", profile);
|
|
16577
16930
|
if (serverUrl)
|
|
16578
16931
|
args.push("--server", serverUrl);
|
|
16579
|
-
const
|
|
16932
|
+
const logPath = daemonLogFilePath();
|
|
16933
|
+
let logFd;
|
|
16934
|
+
try {
|
|
16935
|
+
mkdirSync5(dirname3(logPath), { recursive: true, mode: 448 });
|
|
16936
|
+
logFd = openSync(logPath, "a", 384);
|
|
16937
|
+
} catch (e) {
|
|
16938
|
+
log.error(`Failed to open daemon log file ${logPath}`, e);
|
|
16939
|
+
}
|
|
16940
|
+
const child = spawn2(process.execPath, args, {
|
|
16580
16941
|
detached: true,
|
|
16581
|
-
stdio: ["ignore", "ignore", "ignore"]
|
|
16942
|
+
stdio: logFd != null ? ["ignore", logFd, logFd] : ["ignore", "ignore", "ignore"]
|
|
16582
16943
|
});
|
|
16583
16944
|
child.unref();
|
|
16584
|
-
|
|
16945
|
+
if (logFd != null)
|
|
16946
|
+
closeSync(logFd);
|
|
16947
|
+
log.info(`Spawned new daemon (pid=${child.pid}), logs: ${logPath}`);
|
|
16585
16948
|
}
|
|
16586
16949
|
clearTimeout(timeout);
|
|
16587
16950
|
process.exit(0);
|
|
@@ -16593,21 +16956,62 @@ async function startDaemon(profile, serverUrl) {
|
|
|
16593
16956
|
}
|
|
16594
16957
|
function spawnSessionRunner(input) {
|
|
16595
16958
|
const logDir = sessionRunnerLogDir();
|
|
16596
|
-
|
|
16597
|
-
const logFilePath =
|
|
16959
|
+
mkdirSync5(logDir, { recursive: true });
|
|
16960
|
+
const logFilePath = join6(logDir, `${input.task.id}.log`);
|
|
16598
16961
|
input.logFilePath = logFilePath;
|
|
16599
16962
|
const encoded = Buffer.from(JSON.stringify(input)).toString("base64");
|
|
16600
|
-
|
|
16963
|
+
let fd;
|
|
16964
|
+
try {
|
|
16965
|
+
fd = openSync(logFilePath, "a");
|
|
16966
|
+
} catch (e) {
|
|
16967
|
+
log.error(`Failed to open log file ${logFilePath}`, e);
|
|
16968
|
+
}
|
|
16601
16969
|
const child = spawn2(process.execPath, [sessionRunnerPath, encoded], {
|
|
16602
16970
|
detached: true,
|
|
16603
|
-
stdio: ["ignore", fd, fd]
|
|
16971
|
+
stdio: fd != null ? ["ignore", fd, fd] : ["ignore", "ignore", "ignore"]
|
|
16604
16972
|
});
|
|
16605
16973
|
child.unref();
|
|
16606
|
-
|
|
16974
|
+
if (fd != null)
|
|
16975
|
+
closeSync(fd);
|
|
16607
16976
|
return child;
|
|
16608
16977
|
}
|
|
16609
16978
|
async function handleTask(client, config2, runtimeIndex, task, token, activeTasks) {
|
|
16610
16979
|
log.info(`Task ${task.id} claimed agent=${task.agentId}`);
|
|
16980
|
+
if (task.type === TASK_TYPES.KILL_TASK) {
|
|
16981
|
+
const targetTaskId = task.context?.target_task_id;
|
|
16982
|
+
if (!targetTaskId) {
|
|
16983
|
+
await client.failTask(token, task.id, "missing target_task_id in context");
|
|
16984
|
+
activeTasks.delete(task.id);
|
|
16985
|
+
return;
|
|
16986
|
+
}
|
|
16987
|
+
const agentBaseDir = join6(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
|
|
16988
|
+
const timelineDir = join6(agentBaseDir, ".context_timeline");
|
|
16989
|
+
const pid = findRunningPidByTaskId(timelineDir, targetTaskId);
|
|
16990
|
+
if (pid != null) {
|
|
16991
|
+
writeKillIntent(agentBaseDir, {
|
|
16992
|
+
reason: "cancelled",
|
|
16993
|
+
targetTaskId,
|
|
16994
|
+
expectedPid: pid
|
|
16995
|
+
});
|
|
16996
|
+
try {
|
|
16997
|
+
process.kill(pid, "SIGTERM");
|
|
16998
|
+
await client.failTask(token, task.id, "killed");
|
|
16999
|
+
log.info(`Kill task ${task.id}: sent SIGTERM to pid=${pid} for target=${targetTaskId}`);
|
|
17000
|
+
} catch (e) {
|
|
17001
|
+
if (e?.code === "ESRCH") {
|
|
17002
|
+
await client.failTask(token, task.id, "target process already exited");
|
|
17003
|
+
log.info(`Kill task ${task.id}: target pid=${pid} already exited`);
|
|
17004
|
+
} else {
|
|
17005
|
+
await client.failTask(token, task.id, `kill failed: ${e}`);
|
|
17006
|
+
}
|
|
17007
|
+
}
|
|
17008
|
+
} else {
|
|
17009
|
+
await client.failTask(token, task.id, "target not found in timeline");
|
|
17010
|
+
log.info(`Kill task ${task.id}: target ${targetTaskId} not found in timeline`);
|
|
17011
|
+
}
|
|
17012
|
+
activeTasks.delete(task.id);
|
|
17013
|
+
return;
|
|
17014
|
+
}
|
|
16611
17015
|
try {
|
|
16612
17016
|
await client.startTask(token, task.id);
|
|
16613
17017
|
} catch (e) {
|
|
@@ -16622,6 +17026,60 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
|
|
|
16622
17026
|
return;
|
|
16623
17027
|
}
|
|
16624
17028
|
const provider = runtimeData.provider;
|
|
17029
|
+
if (task.contextKey) {
|
|
17030
|
+
const agentBaseDir = join6(config2.workspacesRoot, task.workspaceId, task.agentId, "workdir");
|
|
17031
|
+
cleanupStaleIntents(agentBaseDir);
|
|
17032
|
+
const timelineDir = join6(agentBaseDir, ".context_timeline");
|
|
17033
|
+
const lockAcquired = acquireSteeringLock(agentBaseDir, task.contextKey);
|
|
17034
|
+
if (!lockAcquired) {
|
|
17035
|
+
log.warn(`Steering lock contention for context_key=${task.contextKey}, proceeding without steering`);
|
|
17036
|
+
} else {
|
|
17037
|
+
try {
|
|
17038
|
+
const predecessor = findRunningEntryByContextKey(timelineDir, task.contextKey, provider);
|
|
17039
|
+
if (predecessor && predecessor.task_id !== task.id) {
|
|
17040
|
+
log.info(`Steering: task ${task.id} supersedes predecessor ${predecessor.task_id} (context_key=${task.contextKey})`);
|
|
17041
|
+
if (predecessor.pid != null) {
|
|
17042
|
+
writeKillIntent(agentBaseDir, {
|
|
17043
|
+
reason: "superseded",
|
|
17044
|
+
targetTaskId: predecessor.task_id,
|
|
17045
|
+
expectedPid: predecessor.pid,
|
|
17046
|
+
successorTaskId: task.id
|
|
17047
|
+
});
|
|
17048
|
+
try {
|
|
17049
|
+
process.kill(predecessor.pid, "SIGTERM");
|
|
17050
|
+
log.info(`Steering: sent SIGTERM to predecessor pid=${predecessor.pid}`);
|
|
17051
|
+
} catch (e) {
|
|
17052
|
+
if (e?.code === "ESRCH") {
|
|
17053
|
+
log.info(`Steering: predecessor pid=${predecessor.pid} already exited`);
|
|
17054
|
+
} else {
|
|
17055
|
+
log.warn(`Steering: kill failed for pid=${predecessor.pid}`, e);
|
|
17056
|
+
}
|
|
17057
|
+
}
|
|
17058
|
+
const waitStart = Date.now();
|
|
17059
|
+
const MAX_WAIT_MS = 15000;
|
|
17060
|
+
const POLL_MS = 200;
|
|
17061
|
+
while (Date.now() - waitStart < MAX_WAIT_MS) {
|
|
17062
|
+
const stillRunning = findRunningPidByTaskId(timelineDir, predecessor.task_id);
|
|
17063
|
+
if (stillRunning == null)
|
|
17064
|
+
break;
|
|
17065
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
17066
|
+
}
|
|
17067
|
+
if (findRunningPidByTaskId(timelineDir, predecessor.task_id) != null) {
|
|
17068
|
+
log.warn(`Steering: predecessor pid=${predecessor.pid} did not exit within ${MAX_WAIT_MS}ms, proceeding anyway`);
|
|
17069
|
+
}
|
|
17070
|
+
}
|
|
17071
|
+
try {
|
|
17072
|
+
await client.supersedeTask(token, predecessor.task_id);
|
|
17073
|
+
log.info(`Steering: predecessor ${predecessor.task_id} marked superseded`);
|
|
17074
|
+
} catch (e) {
|
|
17075
|
+
log.warn(`Steering: failed to mark predecessor superseded server-side`, e);
|
|
17076
|
+
}
|
|
17077
|
+
}
|
|
17078
|
+
} finally {
|
|
17079
|
+
releaseSteeringLock(agentBaseDir, task.contextKey);
|
|
17080
|
+
}
|
|
17081
|
+
}
|
|
17082
|
+
}
|
|
16625
17083
|
const cliPath = provider === "claude" ? config2.claudePath : provider === "codex" ? config2.codexPath : config2.opencodePath;
|
|
16626
17084
|
const configModel = provider === "claude" ? config2.claudeModel : provider === "codex" ? config2.codexModel : config2.opencodeModel;
|
|
16627
17085
|
const agentModel = task.agent?.runtimeConfig?.model;
|
|
@@ -16634,7 +17092,8 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
|
|
|
16634
17092
|
serverURL: config2.serverURL,
|
|
16635
17093
|
token,
|
|
16636
17094
|
workspacesRoot: config2.workspacesRoot,
|
|
16637
|
-
agentTimeout: config2.agentTimeout
|
|
17095
|
+
agentTimeout: config2.agentTimeout,
|
|
17096
|
+
messageInactivityTimeout: config2.messageInactivityTimeout
|
|
16638
17097
|
};
|
|
16639
17098
|
const child = spawnSessionRunner(input);
|
|
16640
17099
|
child.on("close", () => activeTasks.delete(task.id));
|
|
@@ -16676,7 +17135,7 @@ async function startInBackground(profile, serverUrl) {
|
|
|
16676
17135
|
return;
|
|
16677
17136
|
}
|
|
16678
17137
|
const logPath = daemonLogFilePath();
|
|
16679
|
-
|
|
17138
|
+
mkdirSync6(dirname4(logPath), { recursive: true, mode: 448 });
|
|
16680
17139
|
const logFd = openSync2(logPath, "a", 384);
|
|
16681
17140
|
const child = spawn3(process.execPath, buildChildArgs(profile, serverUrl), {
|
|
16682
17141
|
detached: true,
|
|
@@ -16767,6 +17226,14 @@ function daemonCommand() {
|
|
|
16767
17226
|
import { Command as Command4 } from "commander";
|
|
16768
17227
|
|
|
16769
17228
|
// lib/output.ts
|
|
17229
|
+
function printTable(headers, rows) {
|
|
17230
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || "").length)));
|
|
17231
|
+
const header = headers.map((h, i) => h.padEnd(widths[i])).join(" ");
|
|
17232
|
+
const separator = widths.map((w) => "-".repeat(w)).join(" ");
|
|
17233
|
+
console.log(header);
|
|
17234
|
+
console.log(separator);
|
|
17235
|
+
rows.forEach((r) => console.log(r.map((c, i) => (c || "").padEnd(widths[i])).join(" ")));
|
|
17236
|
+
}
|
|
16770
17237
|
function printJSON(data) {
|
|
16771
17238
|
console.log(JSON.stringify(data, null, 2));
|
|
16772
17239
|
}
|
|
@@ -16786,8 +17253,8 @@ function configCommand() {
|
|
|
16786
17253
|
|
|
16787
17254
|
// commands/email.ts
|
|
16788
17255
|
import { Command as Command5 } from "commander";
|
|
16789
|
-
import { writeFileSync as
|
|
16790
|
-
import { basename, join as
|
|
17256
|
+
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
|
|
17257
|
+
import { basename, join as join7 } from "path";
|
|
16791
17258
|
import PostalMime from "postal-mime";
|
|
16792
17259
|
var VALID_STATUSES = ["unread", "read", "archived"];
|
|
16793
17260
|
var EMAIL_BASE = "/tmp/alook-emails";
|
|
@@ -16818,7 +17285,10 @@ function collectRepeated(value, previous) {
|
|
|
16818
17285
|
return previous.concat([value]);
|
|
16819
17286
|
}
|
|
16820
17287
|
function resolveClientOpts(command, opts) {
|
|
16821
|
-
|
|
17288
|
+
let root = command;
|
|
17289
|
+
while (root.parent)
|
|
17290
|
+
root = root.parent;
|
|
17291
|
+
const parentOpts = root.opts() || {};
|
|
16822
17292
|
const profile = parentOpts.profile;
|
|
16823
17293
|
const cfg = loadCLIConfigForProfile(profile);
|
|
16824
17294
|
const serverUrl = parentOpts.server || cfg.server_url;
|
|
@@ -16847,7 +17317,7 @@ function emailCommand() {
|
|
|
16847
17317
|
console.error(`Error: invalid status "${opts.status}", must be one of: ${VALID_STATUSES.join(", ")}`);
|
|
16848
17318
|
process.exit(1);
|
|
16849
17319
|
}
|
|
16850
|
-
const emailDir_base =
|
|
17320
|
+
const emailDir_base = join7(EMAIL_BASE, workspaceId, opts.agent_id);
|
|
16851
17321
|
try {
|
|
16852
17322
|
let query = `/api/email?agentId=${opts.agent_id}`;
|
|
16853
17323
|
if (opts.status)
|
|
@@ -16861,11 +17331,11 @@ function emailCommand() {
|
|
|
16861
17331
|
printJSON(emails2);
|
|
16862
17332
|
return;
|
|
16863
17333
|
}
|
|
16864
|
-
|
|
17334
|
+
mkdirSync7(emailDir_base, { recursive: true });
|
|
16865
17335
|
const downloadedPaths = [];
|
|
16866
17336
|
for (const email3 of emails2) {
|
|
16867
|
-
const emailDir =
|
|
16868
|
-
|
|
17337
|
+
const emailDir = join7(emailDir_base, email3.id);
|
|
17338
|
+
mkdirSync7(emailDir, { recursive: true });
|
|
16869
17339
|
const metadata = {
|
|
16870
17340
|
id: email3.id,
|
|
16871
17341
|
from: email3.from_email,
|
|
@@ -16877,8 +17347,8 @@ function emailCommand() {
|
|
|
16877
17347
|
in_reply_to: email3.in_reply_to || "",
|
|
16878
17348
|
references: email3.references || ""
|
|
16879
17349
|
};
|
|
16880
|
-
const metadataPath =
|
|
16881
|
-
|
|
17350
|
+
const metadataPath = join7(emailDir, "metadata.json");
|
|
17351
|
+
writeFileSync6(metadataPath, JSON.stringify(metadata, null, 2));
|
|
16882
17352
|
downloadedPaths.push(metadataPath);
|
|
16883
17353
|
let rawMime;
|
|
16884
17354
|
try {
|
|
@@ -16893,18 +17363,18 @@ function emailCommand() {
|
|
|
16893
17363
|
}
|
|
16894
17364
|
const parsed = await new PostalMime().parse(rawMime);
|
|
16895
17365
|
if (parsed.text) {
|
|
16896
|
-
const bodyPath =
|
|
16897
|
-
|
|
17366
|
+
const bodyPath = join7(emailDir, "body.txt");
|
|
17367
|
+
writeFileSync6(bodyPath, parsed.text);
|
|
16898
17368
|
downloadedPaths.push(bodyPath);
|
|
16899
17369
|
}
|
|
16900
17370
|
if (parsed.html) {
|
|
16901
|
-
const htmlPath =
|
|
16902
|
-
|
|
17371
|
+
const htmlPath = join7(emailDir, "body.html");
|
|
17372
|
+
writeFileSync6(htmlPath, parsed.html);
|
|
16903
17373
|
downloadedPaths.push(htmlPath);
|
|
16904
17374
|
}
|
|
16905
17375
|
if (parsed.attachments && parsed.attachments.length > 0) {
|
|
16906
|
-
const attDir =
|
|
16907
|
-
|
|
17376
|
+
const attDir = join7(emailDir, "attachments");
|
|
17377
|
+
mkdirSync7(attDir, { recursive: true });
|
|
16908
17378
|
const usedFilenames = new Set;
|
|
16909
17379
|
for (let i = 0;i < parsed.attachments.length; i++) {
|
|
16910
17380
|
const att = parsed.attachments[i];
|
|
@@ -16913,7 +17383,7 @@ function emailCommand() {
|
|
|
16913
17383
|
filename = `${i}-${filename}`;
|
|
16914
17384
|
}
|
|
16915
17385
|
usedFilenames.add(filename);
|
|
16916
|
-
const attPath =
|
|
17386
|
+
const attPath = join7(attDir, filename);
|
|
16917
17387
|
const content = att.content;
|
|
16918
17388
|
let buf;
|
|
16919
17389
|
if (typeof content === "string") {
|
|
@@ -16923,7 +17393,7 @@ function emailCommand() {
|
|
|
16923
17393
|
} else {
|
|
16924
17394
|
buf = Buffer.from(content);
|
|
16925
17395
|
}
|
|
16926
|
-
|
|
17396
|
+
writeFileSync6(attPath, buf);
|
|
16927
17397
|
downloadedPaths.push(attPath);
|
|
16928
17398
|
}
|
|
16929
17399
|
}
|
|
@@ -16962,7 +17432,7 @@ function emailCommand() {
|
|
|
16962
17432
|
const client = new APIClient(serverUrl, token, workspaceId);
|
|
16963
17433
|
let htmlBody;
|
|
16964
17434
|
try {
|
|
16965
|
-
htmlBody =
|
|
17435
|
+
htmlBody = readFileSync7(opts.bodyFile, "utf-8");
|
|
16966
17436
|
} catch (err) {
|
|
16967
17437
|
console.error(`Error: cannot read body file "${opts.bodyFile}": ${err instanceof Error ? err.message : err}`);
|
|
16968
17438
|
process.exit(1);
|
|
@@ -16978,8 +17448,8 @@ function emailCommand() {
|
|
|
16978
17448
|
let bytes;
|
|
16979
17449
|
let size;
|
|
16980
17450
|
try {
|
|
16981
|
-
bytes =
|
|
16982
|
-
size =
|
|
17451
|
+
bytes = readFileSync7(path);
|
|
17452
|
+
size = statSync4(path).size;
|
|
16983
17453
|
} catch (err) {
|
|
16984
17454
|
console.error(`Error: cannot read attachment "${path}": ${err instanceof Error ? err.message : err}`);
|
|
16985
17455
|
process.exit(1);
|
|
@@ -17024,6 +17494,73 @@ function emailCommand() {
|
|
|
17024
17494
|
process.exit(1);
|
|
17025
17495
|
}
|
|
17026
17496
|
});
|
|
17497
|
+
const whitelistCmd = new Command5("whitelist").description("Manage email whitelist (allowed senders)");
|
|
17498
|
+
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) => {
|
|
17499
|
+
const { serverUrl, token, workspaceId } = resolveClientOpts(command, {
|
|
17500
|
+
workspace: opts.workspace,
|
|
17501
|
+
agentId: opts.agent_id
|
|
17502
|
+
});
|
|
17503
|
+
const client = new APIClient(serverUrl, token, workspaceId);
|
|
17504
|
+
try {
|
|
17505
|
+
const entries = await client.getJSON(`/api/agents/${opts.agent_id}/whitelist`);
|
|
17506
|
+
if (!entries.length) {
|
|
17507
|
+
console.log("No whitelisted emails.");
|
|
17508
|
+
return;
|
|
17509
|
+
}
|
|
17510
|
+
if (opts.json) {
|
|
17511
|
+
printJSON(entries);
|
|
17512
|
+
return;
|
|
17513
|
+
}
|
|
17514
|
+
printTable(["ID", "EMAIL", "CREATED AT"], entries.map((e) => [e.id, e.email, e.created_at]));
|
|
17515
|
+
} catch (err) {
|
|
17516
|
+
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
|
17517
|
+
process.exit(1);
|
|
17518
|
+
}
|
|
17519
|
+
});
|
|
17520
|
+
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) => {
|
|
17521
|
+
const { serverUrl, token, workspaceId } = resolveClientOpts(command, {
|
|
17522
|
+
workspace: opts.workspace,
|
|
17523
|
+
agentId: opts.agent_id
|
|
17524
|
+
});
|
|
17525
|
+
const client = new APIClient(serverUrl, token, workspaceId);
|
|
17526
|
+
try {
|
|
17527
|
+
const entry = await client.postJSON(`/api/agents/${opts.agent_id}/whitelist`, { email: email3.toLowerCase() });
|
|
17528
|
+
console.log(`Added ${entry.email} to whitelist (id: ${entry.id})`);
|
|
17529
|
+
} catch (err) {
|
|
17530
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17531
|
+
if (msg.includes("409")) {
|
|
17532
|
+
console.error(`Error: ${email3.toLowerCase()} is already whitelisted`);
|
|
17533
|
+
} else {
|
|
17534
|
+
console.error(`Error: ${msg}`);
|
|
17535
|
+
}
|
|
17536
|
+
process.exit(1);
|
|
17537
|
+
}
|
|
17538
|
+
});
|
|
17539
|
+
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) => {
|
|
17540
|
+
const { serverUrl, token, workspaceId } = resolveClientOpts(command, {
|
|
17541
|
+
workspace: opts.workspace,
|
|
17542
|
+
agentId: opts.agent_id
|
|
17543
|
+
});
|
|
17544
|
+
const client = new APIClient(serverUrl, token, workspaceId);
|
|
17545
|
+
const normalizedEmail = email3.toLowerCase();
|
|
17546
|
+
try {
|
|
17547
|
+
const entries = await client.getJSON(`/api/agents/${opts.agent_id}/whitelist`);
|
|
17548
|
+
const entry = entries.find((e) => e.email === normalizedEmail);
|
|
17549
|
+
if (!entry) {
|
|
17550
|
+
console.error(`Error: ${normalizedEmail} is not in the whitelist`);
|
|
17551
|
+
process.exit(1);
|
|
17552
|
+
}
|
|
17553
|
+
await client.deleteJSON(`/api/agents/${opts.agent_id}/whitelist/${entry.id}`);
|
|
17554
|
+
console.log(`Removed ${normalizedEmail} from whitelist`);
|
|
17555
|
+
} catch (err) {
|
|
17556
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17557
|
+
if (msg === "__exit__")
|
|
17558
|
+
throw err;
|
|
17559
|
+
console.error(`Error: ${msg}`);
|
|
17560
|
+
process.exit(1);
|
|
17561
|
+
}
|
|
17562
|
+
});
|
|
17563
|
+
cmd.addCommand(whitelistCmd);
|
|
17027
17564
|
return cmd;
|
|
17028
17565
|
}
|
|
17029
17566
|
|
|
@@ -17283,7 +17820,7 @@ ${result.output}`);
|
|
|
17283
17820
|
|
|
17284
17821
|
// commands/sync.ts
|
|
17285
17822
|
import { Command as Command9 } from "commander";
|
|
17286
|
-
import { readFileSync as
|
|
17823
|
+
import { readFileSync as readFileSync8, statSync as statSync5 } from "fs";
|
|
17287
17824
|
import { basename as basename2 } from "path";
|
|
17288
17825
|
var MIME_BY_EXT2 = {
|
|
17289
17826
|
".pdf": "application/pdf",
|
|
@@ -17332,9 +17869,9 @@ function syncCommand() {
|
|
|
17332
17869
|
let bytes;
|
|
17333
17870
|
let size;
|
|
17334
17871
|
try {
|
|
17335
|
-
const stat =
|
|
17872
|
+
const stat = statSync5(opts.file);
|
|
17336
17873
|
size = stat.size;
|
|
17337
|
-
bytes =
|
|
17874
|
+
bytes = readFileSync8(opts.file);
|
|
17338
17875
|
} catch (err) {
|
|
17339
17876
|
console.error(`Error: cannot read file "${opts.file}": ${err.message}`);
|
|
17340
17877
|
process.exit(1);
|