@alook/cli 0.0.98 → 0.0.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -14833,7 +14833,7 @@ var EmailNotifyRequestSchema = exports_external.object({
14833
14833
  r2Key: exports_external.string().min(1),
14834
14834
  from: exports_external.string().min(1),
14835
14835
  to: exports_external.string().optional(),
14836
- subject: exports_external.string().min(1),
14836
+ subject: exports_external.string(),
14837
14837
  isWhitelisted: exports_external.boolean(),
14838
14838
  forwarded: exports_external.boolean().optional().default(false),
14839
14839
  messageId: exports_external.string().optional().default(""),
@@ -14935,6 +14935,7 @@ var SkillItemSchema = exports_external.object({
14935
14935
  var SkillSyncRequestSchema = exports_external.object({
14936
14936
  scope: exports_external.enum(["global", "agent"]),
14937
14937
  agent_id: exports_external.string().min(1).optional(),
14938
+ daemon_id: exports_external.string().min(1).optional(),
14938
14939
  runtime: exports_external.enum(["claude", "codex", "opencode"]),
14939
14940
  skills: exports_external.array(SkillItemSchema)
14940
14941
  });
@@ -16834,12 +16835,13 @@ var agentSkill = sqliteTable("agent_skill", {
16834
16835
  id: text("id").primaryKey().$defaultFn(() => "as_" + nanoid3()),
16835
16836
  workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
16836
16837
  agentId: text("agent_id"),
16838
+ daemonId: text("daemon_id"),
16837
16839
  runtime: text("runtime").notNull(),
16838
16840
  name: text("name").notNull(),
16839
16841
  description: text("description").notNull().default(""),
16840
16842
  syncedAt: text("synced_at").notNull().$defaultFn(() => new Date().toISOString())
16841
16843
  }, (t) => [
16842
- unique("agent_skill_ws_runtime_name_agent").on(t.workspaceId, t.runtime, t.name, t.agentId),
16844
+ unique("agent_skill_ws_runtime_name_agent_daemon").on(t.workspaceId, t.runtime, t.name, t.agentId, t.daemonId),
16843
16845
  index("idx_as_workspace_runtime").on(t.workspaceId, t.runtime),
16844
16846
  index("idx_as_agent_runtime").on(t.agentId, t.runtime),
16845
16847
  foreignKey({
@@ -18728,6 +18730,10 @@ ${task.agent.instructions}
18728
18730
  `;
18729
18731
  }
18730
18732
  content += `
18733
+ **Isolated workspaces:**
18734
+ - Each agent runs in its own isolated workspace directory. Colleagues CANNOT read your local files — even in the same workspace.
18735
+ - When sending plans, code, or any file to a colleague, you MUST attach the file to the email (use --attachment). Never reference local file paths expecting them to read it.
18736
+
18731
18737
  **Email threading rules:**
18732
18738
  - When communicating with a colleague on the **same topic** as an existing email thread, reply to that thread (use --in-reply-to) to keep context together.
18733
18739
  - **When starting a NEW topic or task that is unrelated to any previous email thread, you MUST compose a brand new email (do NOT use --in-reply-to). Never hijack an unrelated thread just because you recently emailed that colleague.** Judge by topic/task relevance, not by recency of communication.
@@ -18753,7 +18759,10 @@ ${lines.join(`
18753
18759
 
18754
18760
  ### Emails
18755
18761
  ---
18756
- Run '${cmdPrefix()} email pull --status unread' to download unread emails from inbox to '${tempDir("alook-emails")}/${task.workspaceId}/${task.agentId}/'.
18762
+ When your task prompt includes an \`email_id\` field, fetch ONLY that specific email:
18763
+ - Run '${cmdPrefix()} email pull --email_id <EMAIL_ID>' (uses the email_id from the prompt)
18764
+ When no \`email_id\` is present, fall back to listing unread:
18765
+ - Run '${cmdPrefix()} email pull --status unread' to download unread emails from inbox to '${tempDir("alook-emails")}/${task.workspaceId}/${task.agentId}/'.
18757
18766
  ---
18758
18767
  To download sent emails, add '--folder sent': '${cmdPrefix()} email pull --folder sent'
18759
18768
  Valid folders: inbox (default), sent, untrust.
@@ -18940,7 +18949,8 @@ function prepare(config2, task) {
18940
18949
  ALOOK_CONVERSATION_ID: task.conversationId,
18941
18950
  ALOOK_TRACE_ID: task.traceId ?? "",
18942
18951
  ALOOK_CHANNEL: task.channel ?? "default",
18943
- ALOOK_HEALTH_PORT: process.env.ALOOK_HEALTH_PORT || "19514"
18952
+ ALOOK_HEALTH_PORT: process.env.ALOOK_HEALTH_PORT || "19514",
18953
+ ...config2.token ? { ALOOK_TOKEN: config2.token } : {}
18944
18954
  };
18945
18955
  return { workDir, timelineDir, env };
18946
18956
  }
@@ -19265,6 +19275,9 @@ function buildPrompt(task, attachments) {
19265
19275
  } else {
19266
19276
  obj.notice = EMAIL_NOTICE;
19267
19277
  }
19278
+ if (ctx?.emailId != null) {
19279
+ obj.email_id = ctx.emailId;
19280
+ }
19268
19281
  }
19269
19282
  if (task.type === "calendar_event") {
19270
19283
  obj.notice = CALENDAR_NOTICE;
@@ -19412,7 +19425,7 @@ async function runSession(input) {
19412
19425
  const timelineDir = path.join(agentBaseDir, ".context_timeline").replace(/\\/g, "/");
19413
19426
  mkdirSync6(timelineDir, { recursive: true });
19414
19427
  await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, undefined, process.pid, provider, task.contextKey, input.logFilePath));
19415
- const { workDir, env } = prepare({ workspacesRoot }, task);
19428
+ const { workDir, env } = prepare({ workspacesRoot, token }, task);
19416
19429
  let killed = false;
19417
19430
  const earlyOnKill = async () => {
19418
19431
  if (killed)
@@ -20237,8 +20250,8 @@ function scanOpenCodeAgentSkills(workdir) {
20237
20250
  function computeHash(skills) {
20238
20251
  return createHash2("md5").update(JSON.stringify(skills)).digest("hex");
20239
20252
  }
20240
- function globalCachePath(runtime) {
20241
- return join10(getCacheDir(), "global", `${runtime}.json`);
20253
+ function globalCachePath(daemonId, runtime) {
20254
+ return join10(getCacheDir(), "global", daemonId, `${runtime}.json`);
20242
20255
  }
20243
20256
  function agentCachePath(agentId, runtime) {
20244
20257
  return join10(getCacheDir(), "agents", agentId, `${runtime}.json`);
@@ -20322,17 +20335,19 @@ function runScan() {
20322
20335
  try {
20323
20336
  const skills = getGlobalScanner(runtime)();
20324
20337
  const hash2 = computeHash(skills);
20325
- const prevHash = readCacheHash(globalCachePath(runtime));
20338
+ const prevHash = readCacheHash(globalCachePath(scannerConfig.daemonId, runtime));
20326
20339
  if (prevHash !== hash2) {
20327
20340
  const skillItems = skills.map((s) => ({ name: s.name, description: s.description }));
20328
20341
  log8.info(`Syncing global ${runtime} — ${skills.length} skills`);
20342
+ const daemonId = scannerConfig.daemonId;
20329
20343
  const syncPromises = scannerConfig.workspaces.map((ws) => clientRef.syncSkills(ws.token, {
20330
20344
  scope: "global",
20331
20345
  runtime,
20332
- skills: skillItems
20346
+ skills: skillItems,
20347
+ daemon_id: daemonId
20333
20348
  }));
20334
20349
  Promise.all(syncPromises).then(() => {
20335
- writeCacheFile(globalCachePath(runtime), hash2, skills);
20350
+ writeCacheFile(globalCachePath(daemonId, runtime), hash2, skills);
20336
20351
  }).catch((e) => log8.debug("Global skill sync failed", e));
20337
20352
  }
20338
20353
  } catch (e) {
@@ -20935,7 +20950,8 @@ async function startDaemon(profile, serverUrl) {
20935
20950
  token: ws.token,
20936
20951
  agentIds: ws.agent_ids ?? []
20937
20952
  })),
20938
- runtimes: providers.map((p) => p.type)
20953
+ runtimes: providers.map((p) => p.type),
20954
+ daemonId: config2.daemonId
20939
20955
  }, 60000);
20940
20956
  let shuttingDown = false;
20941
20957
  let restartRequested = false;
@@ -21426,11 +21442,12 @@ function configCommand() {
21426
21442
 
21427
21443
  // commands/email.ts
21428
21444
  import { Command as Command5 } from "commander";
21429
- import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readFileSync as readFileSync9, statSync as statSync5 } from "fs";
21430
- import { basename as basename2, join as join12 } from "path";
21445
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readFileSync as readFileSync11 } from "fs";
21446
+ import { join as join12 } from "path";
21431
21447
  import PostalMime from "postal-mime";
21432
21448
 
21433
21449
  // lib/flags.ts
21450
+ import { readFileSync as readFileSync9 } from "fs";
21434
21451
  function resolveAgentId(opts) {
21435
21452
  const id = opts.agent_id || process.env.ALOOK_AGENT_ID;
21436
21453
  if (!id) {
@@ -21439,83 +21456,157 @@ function resolveAgentId(opts) {
21439
21456
  }
21440
21457
  return id;
21441
21458
  }
21442
-
21443
- // commands/email.ts
21444
- var log10 = createLogger2({ module: "email" });
21445
- var VALID_STATUSES = ["unread", "read", "archived", "sent"];
21446
- var VALID_FOLDERS = ["inbox", "sent", "untrust"];
21447
- var EMAIL_BASE = tempDir("alook-emails");
21448
- var MIME_BY_EXT = {
21449
- ".pdf": "application/pdf",
21450
- ".png": "image/png",
21451
- ".jpg": "image/jpeg",
21452
- ".jpeg": "image/jpeg",
21453
- ".gif": "image/gif",
21454
- ".webp": "image/webp",
21455
- ".svg": "image/svg+xml",
21456
- ".txt": "text/plain",
21457
- ".html": "text/html",
21458
- ".htm": "text/html",
21459
- ".json": "application/json",
21460
- ".csv": "text/csv",
21461
- ".md": "text/markdown",
21462
- ".zip": "application/zip"
21463
- };
21464
- function guessContentType(filename) {
21465
- const idx = filename.lastIndexOf(".");
21466
- if (idx < 0)
21467
- return "application/octet-stream";
21468
- const ext = filename.slice(idx).toLowerCase();
21469
- return MIME_BY_EXT[ext] ?? "application/octet-stream";
21470
- }
21471
21459
  function collectRepeated(value, previous) {
21472
21460
  return previous.concat([value]);
21473
21461
  }
21474
- function resolveClientOpts(command, opts) {
21462
+ function readBody(opts) {
21463
+ if (opts.body && opts.bodyFile) {
21464
+ console.error("Error: --body and --body-file are mutually exclusive");
21465
+ process.exit(1);
21466
+ }
21467
+ if (opts.bodyFile)
21468
+ return readFileSync9(opts.bodyFile, "utf-8");
21469
+ return opts.body ?? "";
21470
+ }
21471
+
21472
+ // lib/command-utils.ts
21473
+ function getRootOpts(command) {
21475
21474
  let root = command;
21476
21475
  while (root.parent)
21477
21476
  root = root.parent;
21478
- const parentOpts = root.opts() || {};
21479
- const profile = parentOpts.profile;
21480
- const cfg = loadCLIConfigForProfile(profile);
21481
- const serverUrl = parentOpts.server || cfg.server_url;
21477
+ return root.opts() || {};
21478
+ }
21479
+
21480
+ // lib/resolve-client.ts
21481
+ function resolveClientOpts(command, opts = {}) {
21482
+ const parentOpts = getRootOpts(command);
21483
+ const cfg = loadCLIConfigForProfile(parentOpts.profile);
21484
+ const serverUrl = parentOpts.server || process.env.ALOOK_SERVER_URL || cfg.server_url;
21485
+ if (!serverUrl) {
21486
+ console.error("Error: no server URL configured. Set ALOOK_SERVER_URL or run register.");
21487
+ process.exit(1);
21488
+ }
21482
21489
  const workspaces = cfg.watched_workspaces || [];
21483
21490
  let ws;
21491
+ const envWorkspaceId = process.env.ALOOK_WORKSPACE_ID;
21484
21492
  if (opts.workspace) {
21485
21493
  ws = workspaces.find((w) => w.id === opts.workspace);
21486
21494
  if (!ws) {
21487
- console.error(`Error: workspace ${opts.workspace} not found in config.`);
21488
- process.exit(1);
21495
+ if (envWorkspaceId === opts.workspace) {
21496
+ ws = undefined;
21497
+ } else {
21498
+ console.error(`Error: workspace ${opts.workspace} not found in config.`);
21499
+ process.exit(1);
21500
+ }
21489
21501
  }
21490
21502
  } else if (opts.agentId) {
21491
21503
  ws = workspaces.find((w) => w.agent_ids?.includes(opts.agentId));
21492
21504
  if (!ws) {
21493
21505
  if (workspaces.length === 1) {
21494
21506
  ws = workspaces[0];
21495
- } else {
21496
- console.error(`Error: agent ${opts.agentId} not found in any registered workspace. Use --workspace to specify.`);
21497
- process.exit(1);
21498
21507
  }
21499
21508
  }
21500
21509
  } else if (workspaces.length === 1) {
21501
21510
  ws = workspaces[0];
21502
- } else {
21503
- console.error(`Error: multiple workspaces registered. Use --workspace to specify which one.`);
21504
- process.exit(1);
21505
21511
  }
21506
- const token = ws?.token;
21512
+ const envToken = process.env.ALOOK_TOKEN;
21513
+ const token = envToken || ws?.token;
21507
21514
  if (!token) {
21508
21515
  console.error(`Error: not registered. Run '${cmdPrefix()} register --token <token>' first.`);
21509
21516
  process.exit(1);
21510
21517
  }
21511
- return { serverUrl, token, cfg, profile, workspaceId: ws?.id };
21518
+ const workspaceId = ws?.id || envWorkspaceId;
21519
+ if (!workspaceId) {
21520
+ console.error("Error: cannot determine workspace. Set ALOOK_WORKSPACE_ID env var or use --workspace flag.");
21521
+ process.exit(1);
21522
+ }
21523
+ return { serverUrl, token, workspaceId };
21524
+ }
21525
+
21526
+ // lib/file-utils.ts
21527
+ import { readFileSync as readFileSync10, statSync as statSync5 } from "fs";
21528
+ import { basename as basename2 } from "path";
21529
+ var MIME_BY_EXT = {
21530
+ ".pdf": "application/pdf",
21531
+ ".png": "image/png",
21532
+ ".jpg": "image/jpeg",
21533
+ ".jpeg": "image/jpeg",
21534
+ ".gif": "image/gif",
21535
+ ".webp": "image/webp",
21536
+ ".svg": "image/svg+xml",
21537
+ ".txt": "text/plain",
21538
+ ".html": "text/html",
21539
+ ".htm": "text/html",
21540
+ ".json": "application/json",
21541
+ ".csv": "text/csv",
21542
+ ".md": "text/markdown",
21543
+ ".ts": "text/typescript",
21544
+ ".js": "text/javascript",
21545
+ ".yaml": "text/yaml",
21546
+ ".yml": "text/yaml",
21547
+ ".xml": "application/xml",
21548
+ ".zip": "application/zip"
21549
+ };
21550
+ function guessContentType(filename) {
21551
+ const idx = filename.lastIndexOf(".");
21552
+ if (idx < 0)
21553
+ return "application/octet-stream";
21554
+ const ext = filename.slice(idx).toLowerCase();
21555
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
21556
+ }
21557
+ function contentToBuffer(content) {
21558
+ if (typeof content === "string") {
21559
+ return Buffer.from(content, "base64");
21560
+ } else if (content instanceof ArrayBuffer) {
21561
+ return Buffer.from(new Uint8Array(content));
21562
+ }
21563
+ return Buffer.from(content);
21512
21564
  }
21565
+ async function uploadFile(client, filePath, endpoint) {
21566
+ let bytes;
21567
+ let size;
21568
+ try {
21569
+ bytes = readFileSync10(filePath);
21570
+ size = statSync5(filePath).size;
21571
+ } catch (err) {
21572
+ throw new Error(`cannot read file "${filePath}": ${err instanceof Error ? err.message : err}`);
21573
+ }
21574
+ const filename = basename2(filePath);
21575
+ const contentType = guessContentType(filename);
21576
+ const form = new FormData;
21577
+ form.append("file", new Blob([new Uint8Array(bytes)], { type: contentType }), filename);
21578
+ const uploaded = await client.postMultipart(endpoint, form);
21579
+ return {
21580
+ key: uploaded.key,
21581
+ filename: uploaded.filename,
21582
+ size: uploaded.size ?? size,
21583
+ contentType: uploaded.contentType ?? contentType
21584
+ };
21585
+ }
21586
+
21587
+ // lib/context-env.ts
21588
+ function gatherContextEnvVars() {
21589
+ const conversationId = process.env.ALOOK_CONVERSATION_ID || undefined;
21590
+ const traceId = process.env.ALOOK_TRACE_ID || undefined;
21591
+ const sourceTaskId = process.env.ALOOK_TASK_ID || undefined;
21592
+ return { conversationId, traceId, sourceTaskId };
21593
+ }
21594
+
21595
+ // commands/email.ts
21596
+ var log10 = createLogger2({ module: "email" });
21597
+ var VALID_STATUSES = ["unread", "read", "archived", "sent"];
21598
+ var VALID_FOLDERS = ["inbox", "sent", "untrust"];
21599
+ var EMAIL_BASE = tempDir("alook-emails");
21513
21600
  function emailCommand() {
21514
21601
  const cmd = new Command5("email").description("Manage agent emails");
21515
- cmd.command("pull").description("Download and parse emails to /tmp/alook-emails/{workspaceId}/{agentId}/").option("--agent_id <id>", "Agent ID").option("--status <status>", "Filter by status (unread, read, archived)").option("--folder <folder>", "Email folder (inbox, sent, untrust)").option("--limit <n>", "Maximum number of emails to download").option("--offset <n>", "Number of emails to skip").option("--workspace <id>", "Workspace ID").option("--json", "Output as JSON instead of files").action(async (opts, command) => {
21602
+ cmd.command("pull").description("Download and parse emails to /tmp/alook-emails/{workspaceId}/{agentId}/").option("--agent_id <id>", "Agent ID").option("--email_id <id>", "Fetch a single email by ID (mutually exclusive with --status/--folder/--limit/--offset)").option("--status <status>", "Filter by status (unread, read, archived)").option("--folder <folder>", "Email folder (inbox, sent, untrust)").option("--limit <n>", "Maximum number of emails to download").option("--offset <n>", "Number of emails to skip").option("--workspace <id>", "Workspace ID").option("--json", "Output as JSON instead of files").action(async (opts, command) => {
21516
21603
  const agentId = resolveAgentId(opts);
21517
21604
  const { serverUrl, token, workspaceId } = resolveClientOpts(command, { workspace: opts.workspace, agentId });
21518
21605
  const client = new APIClient(serverUrl, token, workspaceId);
21606
+ if (opts.email_id && (opts.status || opts.folder || opts.limit || opts.offset)) {
21607
+ console.error("Error: --email_id cannot be combined with --status, --folder, --limit, or --offset");
21608
+ process.exit(1);
21609
+ }
21519
21610
  if (opts.status && !VALID_STATUSES.includes(opts.status)) {
21520
21611
  console.error(`Error: invalid status "${opts.status}", must be one of: ${VALID_STATUSES.join(", ")}`);
21521
21612
  process.exit(1);
@@ -21540,16 +21631,22 @@ function emailCommand() {
21540
21631
  }
21541
21632
  const emailDir_base = join12(EMAIL_BASE, workspaceId, agentId);
21542
21633
  try {
21543
- let query = `/api/email?agentId=${agentId}`;
21544
- if (opts.status)
21545
- query += `&status=${opts.status}`;
21546
- if (opts.folder)
21547
- query += `&folder=${opts.folder}`;
21548
- if (opts.limit)
21549
- query += `&limit=${opts.limit}`;
21550
- if (opts.offset)
21551
- query += `&offset=${opts.offset}`;
21552
- const emails2 = await client.getJSON(query);
21634
+ let emails2;
21635
+ if (opts.email_id) {
21636
+ const single = await client.getJSON(`/api/email/${opts.email_id}`);
21637
+ emails2 = [single];
21638
+ } else {
21639
+ let query = `/api/email?agentId=${agentId}`;
21640
+ if (opts.status)
21641
+ query += `&status=${opts.status}`;
21642
+ if (opts.folder)
21643
+ query += `&folder=${opts.folder}`;
21644
+ if (opts.limit)
21645
+ query += `&limit=${opts.limit}`;
21646
+ if (opts.offset)
21647
+ query += `&offset=${opts.offset}`;
21648
+ emails2 = await client.getJSON(query);
21649
+ }
21553
21650
  if (!emails2.length) {
21554
21651
  console.log("No emails found.");
21555
21652
  return;
@@ -21611,16 +21708,7 @@ function emailCommand() {
21611
21708
  }
21612
21709
  usedFilenames.add(filename);
21613
21710
  const attPath = join12(attDir, filename);
21614
- const content = att.content;
21615
- let buf;
21616
- if (typeof content === "string") {
21617
- buf = Buffer.from(content, "base64");
21618
- } else if (content instanceof ArrayBuffer) {
21619
- buf = Buffer.from(new Uint8Array(content));
21620
- } else {
21621
- buf = Buffer.from(content);
21622
- }
21623
- writeFileSync8(attPath, buf);
21711
+ writeFileSync8(attPath, contentToBuffer(att.content));
21624
21712
  downloadedPaths.push(attPath);
21625
21713
  }
21626
21714
  }
@@ -21661,7 +21749,7 @@ function emailCommand() {
21661
21749
  const client = new APIClient(serverUrl, token, workspaceId);
21662
21750
  let htmlBody;
21663
21751
  try {
21664
- htmlBody = readFileSync9(opts.bodyFile, "utf-8");
21752
+ htmlBody = readFileSync11(opts.bodyFile, "utf-8");
21665
21753
  } catch (err) {
21666
21754
  console.error(`Error: cannot read body file "${opts.bodyFile}": ${err instanceof Error ? err.message : err}`);
21667
21755
  process.exit(1);
@@ -21673,27 +21761,8 @@ function emailCommand() {
21673
21761
  const attachmentPaths = opts.attachment ?? [];
21674
21762
  const attachments = [];
21675
21763
  try {
21676
- for (const path2 of attachmentPaths) {
21677
- let bytes;
21678
- let size;
21679
- try {
21680
- bytes = readFileSync9(path2);
21681
- size = statSync5(path2).size;
21682
- } catch (err) {
21683
- console.error(`Error: cannot read attachment "${path2}": ${err instanceof Error ? err.message : err}`);
21684
- process.exit(1);
21685
- }
21686
- const filename = basename2(path2);
21687
- const contentType = guessContentType(filename);
21688
- const form = new FormData;
21689
- form.append("file", new Blob([new Uint8Array(bytes)], { type: contentType }), filename);
21690
- const uploaded = await client.postMultipart("/api/email/upload", form);
21691
- attachments.push({
21692
- key: uploaded.key,
21693
- filename: uploaded.filename,
21694
- size: uploaded.size ?? size,
21695
- contentType: uploaded.contentType ?? contentType
21696
- });
21764
+ for (const filePath of attachmentPaths) {
21765
+ attachments.push(await uploadFile(client, filePath, "/api/email/upload"));
21697
21766
  }
21698
21767
  let inReplyTo;
21699
21768
  let references;
@@ -21708,9 +21777,7 @@ function emailCommand() {
21708
21777
  log10.warn(`could not fetch parent email ${opts.inReplyTo}, sending without threading`);
21709
21778
  }
21710
21779
  }
21711
- const conversationId = process.env.ALOOK_CONVERSATION_ID;
21712
- const traceId = process.env.ALOOK_TRACE_ID;
21713
- const sourceTaskId = process.env.ALOOK_TASK_ID;
21780
+ const ctx = gatherContextEnvVars();
21714
21781
  const res = await client.postJSON("/api/email/send", {
21715
21782
  agentId,
21716
21783
  to: opts.to,
@@ -21719,9 +21786,9 @@ function emailCommand() {
21719
21786
  attachments,
21720
21787
  ...inReplyTo ? { inReplyTo, references } : {},
21721
21788
  ...opts.from ? { from: opts.from } : {},
21722
- ...conversationId ? { conversationId } : {},
21723
- ...traceId ? { traceId } : {},
21724
- ...sourceTaskId ? { sourceTaskId } : {}
21789
+ ...ctx.conversationId ? { conversationId: ctx.conversationId } : {},
21790
+ ...ctx.traceId ? { traceId: ctx.traceId } : {},
21791
+ ...ctx.sourceTaskId ? { sourceTaskId: ctx.sourceTaskId } : {}
21725
21792
  });
21726
21793
  console.log(`Sent email to ${res.to_email} (id: ${res.id})`);
21727
21794
  } catch (err) {
@@ -21765,15 +21832,7 @@ function emailCommand() {
21765
21832
  for (const att of parsed.attachments) {
21766
21833
  const filename = att.filename || "attachment.bin";
21767
21834
  const contentType = att.mimeType || "application/octet-stream";
21768
- const content = att.content;
21769
- let buf;
21770
- if (typeof content === "string") {
21771
- buf = Buffer.from(content, "base64");
21772
- } else if (content instanceof ArrayBuffer) {
21773
- buf = Buffer.from(new Uint8Array(content));
21774
- } else {
21775
- buf = Buffer.from(content);
21776
- }
21835
+ const buf = contentToBuffer(att.content);
21777
21836
  const form = new FormData;
21778
21837
  form.append("file", new Blob([new Uint8Array(buf)], { type: contentType }), filename);
21779
21838
  const uploaded = await client.postMultipart("/api/email/upload", form);
@@ -21786,27 +21845,8 @@ function emailCommand() {
21786
21845
  }
21787
21846
  }
21788
21847
  const extraPaths = opts.attachment ?? [];
21789
- for (const path2 of extraPaths) {
21790
- let bytes;
21791
- let size;
21792
- try {
21793
- bytes = readFileSync9(path2);
21794
- size = statSync5(path2).size;
21795
- } catch (err) {
21796
- console.error(`Error: cannot read attachment "${path2}": ${err instanceof Error ? err.message : err}`);
21797
- process.exit(1);
21798
- }
21799
- const filename = basename2(path2);
21800
- const contentType = guessContentType(filename);
21801
- const form = new FormData;
21802
- form.append("file", new Blob([new Uint8Array(bytes)], { type: contentType }), filename);
21803
- const uploaded = await client.postMultipart("/api/email/upload", form);
21804
- attachments.push({
21805
- key: uploaded.key,
21806
- filename: uploaded.filename,
21807
- size: uploaded.size ?? size,
21808
- contentType: uploaded.contentType ?? contentType
21809
- });
21848
+ for (const filePath of extraPaths) {
21849
+ attachments.push(await uploadFile(client, filePath, "/api/email/upload"));
21810
21850
  }
21811
21851
  let htmlBody = "";
21812
21852
  if (opts.note) {
@@ -21823,9 +21863,7 @@ function emailCommand() {
21823
21863
  htmlBody += `<pre>${parsed.text}</pre>`;
21824
21864
  }
21825
21865
  const subject = /^fwd:/i.test(original.subject) ? original.subject : `Fwd: ${original.subject}`;
21826
- const conversationId = process.env.ALOOK_CONVERSATION_ID;
21827
- const traceId = process.env.ALOOK_TRACE_ID;
21828
- const sourceTaskId = process.env.ALOOK_TASK_ID;
21866
+ const ctx = gatherContextEnvVars();
21829
21867
  const res = await client.postJSON("/api/email/send", {
21830
21868
  agentId,
21831
21869
  to: opts.to,
@@ -21833,9 +21871,9 @@ function emailCommand() {
21833
21871
  htmlBody,
21834
21872
  attachments,
21835
21873
  ...opts.from ? { from: opts.from } : {},
21836
- ...conversationId ? { conversationId } : {},
21837
- ...traceId ? { traceId } : {},
21838
- ...sourceTaskId ? { sourceTaskId } : {}
21874
+ ...ctx.conversationId ? { conversationId: ctx.conversationId } : {},
21875
+ ...ctx.traceId ? { traceId: ctx.traceId } : {},
21876
+ ...ctx.sourceTaskId ? { sourceTaskId: ctx.sourceTaskId } : {}
21839
21877
  });
21840
21878
  console.log(`Forwarded email to ${res.to_email} (id: ${res.id})`);
21841
21879
  } catch (err) {
@@ -21921,19 +21959,6 @@ function emailCommand() {
21921
21959
 
21922
21960
  // commands/calendar.ts
21923
21961
  import { Command as Command6 } from "commander";
21924
- function resolveClientOpts2(command, agentId) {
21925
- const parentOpts = command.parent?.parent?.opts() || {};
21926
- const profile = parentOpts.profile;
21927
- const cfg = loadCLIConfigForProfile(profile);
21928
- const serverUrl = parentOpts.server || cfg.server_url;
21929
- const workspaces = cfg.watched_workspaces || [];
21930
- const ws = workspaces.find((w) => w.agent_ids?.includes(agentId));
21931
- if (!ws || !ws.token) {
21932
- console.error(`Error: no registered workspace contains agent ${agentId}. Run '${cmdPrefix()} register --token <token>' first.`);
21933
- process.exit(1);
21934
- }
21935
- return { serverUrl, token: ws.token, workspaceId: ws.id };
21936
- }
21937
21962
  function parseLocalDatetime(input) {
21938
21963
  const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(input);
21939
21964
  if (!match) {
@@ -21967,7 +21992,7 @@ function calendarCommand() {
21967
21992
  const cmd = new Command6("calendar").description("Manage scheduled agent events");
21968
21993
  cmd.command("set").description("Create a calendar event").option("--agent_id <id>", "Agent ID").requiredOption("--event_title <title>", "Event title (used as the task prompt)").requiredOption("--datetime <iso>", "Scheduled datetime (YYYY-MM-DDTHH:MM, local time)").option("--description <text>", "Optional longer-form notes for the event").option("--repeat <interval>", "Repeat interval, e.g. 1day, 2hour, 1month").option("--repeat_stop_date <date>", "Stop repeating on or after this date (YYYY-MM-DD, local time)").option("--json", "Output as JSON").action(async (opts, command) => {
21969
21994
  const agentId = resolveAgentId(opts);
21970
- const { serverUrl, token, workspaceId } = resolveClientOpts2(command, agentId);
21995
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
21971
21996
  const client = new APIClient(serverUrl, token, workspaceId);
21972
21997
  let scheduledAt;
21973
21998
  try {
@@ -22011,7 +22036,7 @@ function calendarCommand() {
22011
22036
  });
22012
22037
  cmd.command("list").description("List calendar events for an agent").option("--agent_id <id>", "Agent ID").option("--future_days <n>", "Include events scheduled in the next N days", "30").option("--past_days <n>", "Include events scheduled in the past N days", "0").option("--json", "Output as JSON").action(async (opts, command) => {
22013
22038
  const agentId = resolveAgentId(opts);
22014
- const { serverUrl, token, workspaceId } = resolveClientOpts2(command, agentId);
22039
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
22015
22040
  const client = new APIClient(serverUrl, token, workspaceId);
22016
22041
  const now = Date.now();
22017
22042
  const from = new Date(now - Number(opts.past_days) * 86400000).toISOString();
@@ -22038,7 +22063,7 @@ function calendarCommand() {
22038
22063
  });
22039
22064
  cmd.command("show").description("Show the full detail of a single calendar event").option("--agent_id <id>", "Agent ID").requiredOption("--event_id <id>", "Event ID").option("--json", "Output as JSON").action(async (opts, command) => {
22040
22065
  const agentId = resolveAgentId(opts);
22041
- const { serverUrl, token, workspaceId } = resolveClientOpts2(command, agentId);
22066
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
22042
22067
  const client = new APIClient(serverUrl, token, workspaceId);
22043
22068
  try {
22044
22069
  const ev = await client.getJSON(`/api/calendar/${opts.event_id}`);
@@ -22102,7 +22127,7 @@ function calendarCommand() {
22102
22127
  process.exit(1);
22103
22128
  }
22104
22129
  const agentId = resolveAgentId(opts);
22105
- const { serverUrl, token, workspaceId } = resolveClientOpts2(command, agentId);
22130
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
22106
22131
  const client = new APIClient(serverUrl, token, workspaceId);
22107
22132
  try {
22108
22133
  const updated = await client.patchJSON(`/api/calendar/${opts.event_id}`, body);
@@ -22122,7 +22147,7 @@ function calendarCommand() {
22122
22147
  });
22123
22148
  cmd.command("delete").description("Delete a calendar event").option("--agent_id <id>", "Agent ID").requiredOption("--event_id <id>", "Event ID").action(async (opts, command) => {
22124
22149
  const agentId = resolveAgentId(opts);
22125
- const { serverUrl, token, workspaceId } = resolveClientOpts2(command, agentId);
22150
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
22126
22151
  const client = new APIClient(serverUrl, token, workspaceId);
22127
22152
  try {
22128
22153
  await client.deleteJSON(`/api/calendar/${opts.event_id}`);
@@ -22137,33 +22162,7 @@ function calendarCommand() {
22137
22162
 
22138
22163
  // commands/issue.ts
22139
22164
  import { Command as Command7 } from "commander";
22140
- import { readFileSync as readFileSync10 } from "fs";
22141
22165
  var VALID_STATUSES2 = ["todo", "in_progress", "review", "done", "closed", "canceled", "failed"];
22142
- function resolveClientOpts3(command, agentId) {
22143
- let root = command;
22144
- while (root.parent)
22145
- root = root.parent;
22146
- const parentOpts = root.opts() || {};
22147
- const profile = parentOpts.profile;
22148
- const cfg = loadCLIConfigForProfile(profile);
22149
- const serverUrl = parentOpts.server || cfg.server_url;
22150
- const workspaces = cfg.watched_workspaces || [];
22151
- const ws = workspaces.find((w) => w.agent_ids?.includes(agentId));
22152
- if (!ws || !ws.token) {
22153
- console.error(`Error: no registered workspace contains agent ${agentId}. Run '${cmdPrefix()} register --token <token>' first.`);
22154
- process.exit(1);
22155
- }
22156
- return { serverUrl, token: ws.token, workspaceId: ws.id };
22157
- }
22158
- function readBody(opts) {
22159
- if (opts.body && opts.bodyFile) {
22160
- console.error("Error: --body and --body-file are mutually exclusive");
22161
- process.exit(1);
22162
- }
22163
- if (opts.bodyFile)
22164
- return readFileSync10(opts.bodyFile, "utf-8");
22165
- return opts.body ?? "";
22166
- }
22167
22166
  function printIssue(issue3) {
22168
22167
  console.log(`${issue3.id} ${issue3.status.padEnd(11)} ${issue3.title}`);
22169
22168
  }
@@ -22197,7 +22196,7 @@ function issueCommand() {
22197
22196
  const cmd = new Command7("issue").description("Manage assigned issues");
22198
22197
  cmd.command("create").description("Create and dispatch an issue to an agent").option("--agent_id <id>", "Agent ID").requiredOption("--title <title>", "Issue title").option("--description <text>", "Issue description").option("--body-file <path>", "Read issue description from a file").option("--json", "Output as JSON").action(async (opts, command) => {
22199
22198
  const agentId = resolveAgentId(opts);
22200
- const { serverUrl, token, workspaceId } = resolveClientOpts3(command, agentId);
22199
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
22201
22200
  const client = new APIClient(serverUrl, token, workspaceId);
22202
22201
  const description = readBody({ body: opts.description, bodyFile: opts.bodyFile });
22203
22202
  try {
@@ -22220,7 +22219,7 @@ function issueCommand() {
22220
22219
  process.exit(1);
22221
22220
  }
22222
22221
  const agentId = resolveAgentId(opts);
22223
- const { serverUrl, token, workspaceId } = resolveClientOpts3(command, agentId);
22222
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
22224
22223
  const client = new APIClient(serverUrl, token, workspaceId);
22225
22224
  const params = new URLSearchParams({ agentId });
22226
22225
  if (opts.status)
@@ -22244,7 +22243,7 @@ function issueCommand() {
22244
22243
  });
22245
22244
  cmd.command("show").description("Show issue details and conversation").option("--agent_id <id>", "Agent ID").requiredOption("--issue_id <id>", "Issue ID").option("--json", "Output as JSON").action(async (opts, command) => {
22246
22245
  const agentId = resolveAgentId(opts);
22247
- const { serverUrl, token, workspaceId } = resolveClientOpts3(command, agentId);
22246
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
22248
22247
  const client = new APIClient(serverUrl, token, workspaceId);
22249
22248
  try {
22250
22249
  const res = await client.getJSON(`/api/issues/${opts.issue_id}?agentId=${encodeURIComponent(agentId)}`);
@@ -22278,7 +22277,7 @@ function issueCommand() {
22278
22277
  process.exit(1);
22279
22278
  }
22280
22279
  const agentId = resolveAgentId(opts);
22281
- const { serverUrl, token, workspaceId } = resolveClientOpts3(command, agentId);
22280
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
22282
22281
  const client = new APIClient(serverUrl, token, workspaceId);
22283
22282
  try {
22284
22283
  const issue3 = await client.patchJSON(`/api/issues/${opts.issue_id}?agentId=${encodeURIComponent(agentId)}`, body);
@@ -22297,7 +22296,7 @@ function issueCommand() {
22297
22296
  process.exit(1);
22298
22297
  }
22299
22298
  const agentId = resolveAgentId(opts);
22300
- const { serverUrl, token, workspaceId } = resolveClientOpts3(command, agentId);
22299
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
22301
22300
  const client = new APIClient(serverUrl, token, workspaceId);
22302
22301
  try {
22303
22302
  const res = await client.postJSON(`/api/issues/${opts.issue_id}/comments?agentId=${encodeURIComponent(agentId)}`, { content });
@@ -22359,62 +22358,23 @@ ${result.output}`);
22359
22358
 
22360
22359
  // commands/sync.ts
22361
22360
  import { Command as Command10 } from "commander";
22362
- import { readFileSync as readFileSync11 } from "fs";
22361
+ import { readFileSync as readFileSync12 } from "fs";
22363
22362
  import { basename as basename3 } from "path";
22364
- var MIME_BY_EXT2 = {
22365
- ".pdf": "application/pdf",
22366
- ".png": "image/png",
22367
- ".jpg": "image/jpeg",
22368
- ".jpeg": "image/jpeg",
22369
- ".gif": "image/gif",
22370
- ".txt": "text/plain",
22371
- ".html": "text/html",
22372
- ".json": "application/json",
22373
- ".csv": "text/csv",
22374
- ".md": "text/markdown",
22375
- ".ts": "text/typescript",
22376
- ".js": "text/javascript",
22377
- ".yaml": "text/yaml",
22378
- ".yml": "text/yaml",
22379
- ".svg": "image/svg+xml",
22380
- ".xml": "application/xml",
22381
- ".zip": "application/zip"
22382
- };
22383
- function guessContentType2(filename) {
22384
- const idx = filename.lastIndexOf(".");
22385
- if (idx < 0)
22386
- return "application/octet-stream";
22387
- const ext = filename.slice(idx).toLowerCase();
22388
- return MIME_BY_EXT2[ext] ?? "application/octet-stream";
22389
- }
22390
- function resolveClientOpts4(command, agentId) {
22391
- const parentOpts = command.parent?.parent?.opts() || {};
22392
- const profile = parentOpts.profile;
22393
- const cfg = loadCLIConfigForProfile(profile);
22394
- const serverUrl = parentOpts.server || cfg.server_url;
22395
- const workspaces = cfg.watched_workspaces || [];
22396
- const ws = workspaces.find((w) => w.agent_ids?.includes(agentId));
22397
- if (!ws || !ws.token) {
22398
- console.error(`Error: no registered workspace contains agent ${agentId}. Run '${cmdPrefix()} register --token <token>' first.`);
22399
- process.exit(1);
22400
- }
22401
- return { serverUrl, token: ws.token, workspaceId: ws.id };
22402
- }
22403
22363
  function syncCommand() {
22404
22364
  const cmd = new Command10("sync").description("File sync utilities");
22405
22365
  cmd.command("upload-artifact").description("Upload a file artifact to a conversation").option("--agent_id <id>", "Agent ID").requiredOption("--conversation_id <id>", "Conversation ID").requiredOption("--file <path>", "Path to file to upload").action(async (opts, command) => {
22406
22366
  const agentId = resolveAgentId(opts);
22407
- const { serverUrl, token, workspaceId } = resolveClientOpts4(command, agentId);
22367
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, { agentId });
22408
22368
  const client = new APIClient(serverUrl, token, workspaceId);
22409
22369
  let bytes;
22410
22370
  try {
22411
- bytes = readFileSync11(opts.file);
22371
+ bytes = readFileSync12(opts.file);
22412
22372
  } catch (err) {
22413
22373
  console.error(`Error: cannot read file "${opts.file}": ${err.message}`);
22414
22374
  process.exit(1);
22415
22375
  }
22416
22376
  const filename = basename3(opts.file);
22417
- const contentType = guessContentType2(filename);
22377
+ const contentType = guessContentType(filename);
22418
22378
  const form = new FormData;
22419
22379
  form.append("file", new Blob([new Uint8Array(bytes)], { type: contentType }), filename);
22420
22380
  form.append("agent_id", agentId);
@@ -14700,7 +14700,7 @@ var EmailNotifyRequestSchema = exports_external.object({
14700
14700
  r2Key: exports_external.string().min(1),
14701
14701
  from: exports_external.string().min(1),
14702
14702
  to: exports_external.string().optional(),
14703
- subject: exports_external.string().min(1),
14703
+ subject: exports_external.string(),
14704
14704
  isWhitelisted: exports_external.boolean(),
14705
14705
  forwarded: exports_external.boolean().optional().default(false),
14706
14706
  messageId: exports_external.string().optional().default(""),
@@ -14802,6 +14802,7 @@ var SkillItemSchema = exports_external.object({
14802
14802
  var SkillSyncRequestSchema = exports_external.object({
14803
14803
  scope: exports_external.enum(["global", "agent"]),
14804
14804
  agent_id: exports_external.string().min(1).optional(),
14805
+ daemon_id: exports_external.string().min(1).optional(),
14805
14806
  runtime: exports_external.enum(["claude", "codex", "opencode"]),
14806
14807
  skills: exports_external.array(SkillItemSchema)
14807
14808
  });
@@ -16701,12 +16702,13 @@ var agentSkill = sqliteTable("agent_skill", {
16701
16702
  id: text("id").primaryKey().$defaultFn(() => "as_" + nanoid3()),
16702
16703
  workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
16703
16704
  agentId: text("agent_id"),
16705
+ daemonId: text("daemon_id"),
16704
16706
  runtime: text("runtime").notNull(),
16705
16707
  name: text("name").notNull(),
16706
16708
  description: text("description").notNull().default(""),
16707
16709
  syncedAt: text("synced_at").notNull().$defaultFn(() => new Date().toISOString())
16708
16710
  }, (t) => [
16709
- unique("agent_skill_ws_runtime_name_agent").on(t.workspaceId, t.runtime, t.name, t.agentId),
16711
+ unique("agent_skill_ws_runtime_name_agent_daemon").on(t.workspaceId, t.runtime, t.name, t.agentId, t.daemonId),
16710
16712
  index("idx_as_workspace_runtime").on(t.workspaceId, t.runtime),
16711
16713
  index("idx_as_agent_runtime").on(t.agentId, t.runtime),
16712
16714
  foreignKey({
@@ -18066,6 +18068,10 @@ ${task.agent.instructions}
18066
18068
  `;
18067
18069
  }
18068
18070
  content += `
18071
+ **Isolated workspaces:**
18072
+ - Each agent runs in its own isolated workspace directory. Colleagues CANNOT read your local files — even in the same workspace.
18073
+ - When sending plans, code, or any file to a colleague, you MUST attach the file to the email (use --attachment). Never reference local file paths expecting them to read it.
18074
+
18069
18075
  **Email threading rules:**
18070
18076
  - When communicating with a colleague on the **same topic** as an existing email thread, reply to that thread (use --in-reply-to) to keep context together.
18071
18077
  - **When starting a NEW topic or task that is unrelated to any previous email thread, you MUST compose a brand new email (do NOT use --in-reply-to). Never hijack an unrelated thread just because you recently emailed that colleague.** Judge by topic/task relevance, not by recency of communication.
@@ -18091,7 +18097,10 @@ ${lines.join(`
18091
18097
 
18092
18098
  ### Emails
18093
18099
  ---
18094
- Run '${cmdPrefix()} email pull --status unread' to download unread emails from inbox to '${tempDir("alook-emails")}/${task.workspaceId}/${task.agentId}/'.
18100
+ When your task prompt includes an \`email_id\` field, fetch ONLY that specific email:
18101
+ - Run '${cmdPrefix()} email pull --email_id <EMAIL_ID>' (uses the email_id from the prompt)
18102
+ When no \`email_id\` is present, fall back to listing unread:
18103
+ - Run '${cmdPrefix()} email pull --status unread' to download unread emails from inbox to '${tempDir("alook-emails")}/${task.workspaceId}/${task.agentId}/'.
18095
18104
  ---
18096
18105
  To download sent emails, add '--folder sent': '${cmdPrefix()} email pull --folder sent'
18097
18106
  Valid folders: inbox (default), sent, untrust.
@@ -18278,7 +18287,8 @@ function prepare(config2, task) {
18278
18287
  ALOOK_CONVERSATION_ID: task.conversationId,
18279
18288
  ALOOK_TRACE_ID: task.traceId ?? "",
18280
18289
  ALOOK_CHANNEL: task.channel ?? "default",
18281
- ALOOK_HEALTH_PORT: process.env.ALOOK_HEALTH_PORT || "19514"
18290
+ ALOOK_HEALTH_PORT: process.env.ALOOK_HEALTH_PORT || "19514",
18291
+ ...config2.token ? { ALOOK_TOKEN: config2.token } : {}
18282
18292
  };
18283
18293
  return { workDir, timelineDir, env };
18284
18294
  }
@@ -18651,6 +18661,9 @@ function buildPrompt(task, attachments) {
18651
18661
  } else {
18652
18662
  obj.notice = EMAIL_NOTICE;
18653
18663
  }
18664
+ if (ctx?.emailId != null) {
18665
+ obj.email_id = ctx.emailId;
18666
+ }
18654
18667
  }
18655
18668
  if (task.type === "calendar_event") {
18656
18669
  obj.notice = CALENDAR_NOTICE;
@@ -18798,7 +18811,7 @@ async function runSession(input) {
18798
18811
  const timelineDir = path.join(agentBaseDir, ".context_timeline").replace(/\\/g, "/");
18799
18812
  mkdirSync4(timelineDir, { recursive: true });
18800
18813
  await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, undefined, process.pid, provider, task.contextKey, input.logFilePath));
18801
- const { workDir, env } = prepare({ workspacesRoot }, task);
18814
+ const { workDir, env } = prepare({ workspacesRoot, token }, task);
18802
18815
  let killed = false;
18803
18816
  const earlyOnKill = async () => {
18804
18817
  if (killed)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/cli",
3
- "version": "0.0.98",
3
+ "version": "0.0.100",
4
4
  "description": "Alook CLI — Enable Your Person Colleague",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/alookai/alook#readme",