@alook/cli 0.0.9 → 0.0.11

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
@@ -15,7 +15,7 @@ var __export = (target, all) => {
15
15
  };
16
16
 
17
17
  // src/index.ts
18
- import { Command as Command9 } from "commander";
18
+ import { Command as Command10 } from "commander";
19
19
 
20
20
  // commands/register.ts
21
21
  import { Command } from "commander";
@@ -13896,7 +13896,8 @@ var TaskApiBaseSchema = exports_external.object({
13896
13896
  error: exports_external.string().nullable(),
13897
13897
  created_at: exports_external.string(),
13898
13898
  type: exports_external.string(),
13899
- context_key: exports_external.string().nullable().optional()
13899
+ context_key: exports_external.string().nullable().optional(),
13900
+ context: exports_external.unknown().nullable().optional()
13900
13901
  });
13901
13902
  var TaskApiSchema = TaskApiBaseSchema.extend({
13902
13903
  agent: TaskAgentDataApiSchema.nullable().optional()
@@ -14015,6 +14016,64 @@ var CalendarEventApiSchema = exports_external.object({
14015
14016
  var AddWhitelistRequestSchema = exports_external.object({
14016
14017
  email: exports_external.string().email()
14017
14018
  });
14019
+ var RuntimeConfigSchema = exports_external.object({ model: exports_external.string().max(100).optional() }).passthrough().optional();
14020
+ var CreateAgentRequestSchema = exports_external.object({
14021
+ name: exports_external.string().min(1, "name is required"),
14022
+ description: exports_external.string().optional().default(""),
14023
+ instructions: exports_external.string().optional().default(""),
14024
+ runtime_id: exports_external.string().min(1, "runtime_id is required"),
14025
+ runtime_config: RuntimeConfigSchema,
14026
+ max_concurrent_tasks: exports_external.number().int().optional(),
14027
+ email_handle: exports_external.string().optional()
14028
+ });
14029
+ var UpdateAgentRequestSchema = exports_external.object({
14030
+ name: exports_external.string().min(1).optional(),
14031
+ description: exports_external.string().optional(),
14032
+ instructions: exports_external.string().optional(),
14033
+ runtime_id: exports_external.string().min(1).optional(),
14034
+ runtime_config: RuntimeConfigSchema
14035
+ }).refine((v) => v.name !== undefined || v.description !== undefined || v.instructions !== undefined || v.runtime_id !== undefined || v.runtime_config !== undefined, { message: "at least one field is required" });
14036
+ var CreateConversationRequestSchema = exports_external.object({
14037
+ agent_id: exports_external.string().min(1, "agent_id is required")
14038
+ });
14039
+ var CreateMessageRequestSchema = exports_external.object({
14040
+ content: exports_external.string().min(1, "content is required")
14041
+ });
14042
+ var EmailAttachmentSchema = exports_external.object({
14043
+ key: exports_external.string().min(1),
14044
+ filename: exports_external.string().min(1),
14045
+ size: exports_external.number().int().nonnegative().optional(),
14046
+ contentType: exports_external.string().min(1)
14047
+ });
14048
+ var SendEmailRequestSchema = exports_external.object({
14049
+ agentId: exports_external.string().min(1, "agentId is required"),
14050
+ to: exports_external.string().min(1, "to is required"),
14051
+ subject: exports_external.string().min(1, "subject is required"),
14052
+ htmlBody: exports_external.string().default(""),
14053
+ inReplyTo: exports_external.string().optional(),
14054
+ references: exports_external.string().optional(),
14055
+ attachments: exports_external.array(EmailAttachmentSchema).optional()
14056
+ });
14057
+ var UpdateEmailStatusRequestSchema = exports_external.object({
14058
+ status: exports_external.enum(["unread", "read", "archived"])
14059
+ });
14060
+ var EmailNotifyRequestSchema = exports_external.object({
14061
+ agentId: exports_external.string().min(1),
14062
+ workspaceId: exports_external.string().min(1),
14063
+ r2Key: exports_external.string().min(1),
14064
+ from: exports_external.string().min(1),
14065
+ to: exports_external.string().optional(),
14066
+ subject: exports_external.string().min(1),
14067
+ isWhitelisted: exports_external.boolean(),
14068
+ forwarded: exports_external.boolean().optional().default(false),
14069
+ messageId: exports_external.string().optional().default(""),
14070
+ inReplyTo: exports_external.string().optional().default(""),
14071
+ references: exports_external.string().optional().default("")
14072
+ });
14073
+ var CreateWorkspaceRequestSchema = exports_external.object({
14074
+ name: exports_external.string().min(1, "name is required"),
14075
+ slug: exports_external.string().min(1, "slug is required")
14076
+ });
14018
14077
  // ../../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
14019
14078
  var entityKind = Symbol.for("drizzle:entityKind");
14020
14079
  var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
@@ -15521,6 +15580,7 @@ var message = sqliteTable("message", {
15521
15580
  role: text("role").notNull(),
15522
15581
  content: text("content").notNull().default(""),
15523
15582
  taskId: text("task_id"),
15583
+ attachmentIds: text("attachment_ids"),
15524
15584
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15525
15585
  });
15526
15586
  var agentTaskQueue = sqliteTable("agent_task_queue", {
@@ -15605,6 +15665,24 @@ var calendarEvent = sqliteTable("calendar_event", {
15605
15665
  foreignColumns: [agent.id, agent.workspaceId]
15606
15666
  }).onDelete("cascade")
15607
15667
  ]);
15668
+ var artifact = sqliteTable("artifact", {
15669
+ id: text("id").primaryKey().$defaultFn(() => "art_" + nanoid3()),
15670
+ conversationId: text("conversation_id").notNull().references(() => conversation.id, { onDelete: "cascade" }),
15671
+ agentId: text("agent_id").notNull(),
15672
+ workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
15673
+ filename: text("filename").notNull(),
15674
+ contentType: text("content_type").notNull().default("application/octet-stream"),
15675
+ size: integer2("size").notNull(),
15676
+ r2Key: text("r2_key").notNull(),
15677
+ source: text("source").notNull().default("agent"),
15678
+ createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15679
+ }, (t) => [
15680
+ index("idx_artifact_conversation").on(t.conversationId),
15681
+ foreignKey({
15682
+ columns: [t.agentId, t.workspaceId],
15683
+ foreignColumns: [agent.id, agent.workspaceId]
15684
+ }).onDelete("cascade")
15685
+ ]);
15608
15686
  var machineToken = sqliteTable("machine_token", {
15609
15687
  id: text("id").primaryKey().$defaultFn(() => nanoid3()),
15610
15688
  userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
@@ -15701,6 +15779,16 @@ class DaemonClient {
15701
15779
  error: error48
15702
15780
  });
15703
15781
  }
15782
+ async getArtifactMeta(token, artifactId, workspaceId) {
15783
+ return this.request("GET", `/api/artifacts/${artifactId}?workspace_id=${encodeURIComponent(workspaceId)}`, token);
15784
+ }
15785
+ async downloadArtifact(token, artifactId, workspaceId) {
15786
+ const res = await fetch(`${this.baseURL}/api/artifacts/${artifactId}/content?workspace_id=${encodeURIComponent(workspaceId)}`, { headers: { Authorization: `Bearer ${token}` } });
15787
+ if (!res.ok) {
15788
+ throw new Error(`artifact download failed: HTTP ${res.status}`);
15789
+ }
15790
+ return res.arrayBuffer();
15791
+ }
15704
15792
  reportMessages(token, taskId, messages) {
15705
15793
  return this.request("POST", `/api/daemon/tasks/${taskId}/messages`, token, { messages });
15706
15794
  }
@@ -15735,6 +15823,10 @@ function pidFilePath(profile) {
15735
15823
  const name = profile ? `daemon_${profile}.pid` : "daemon.pid";
15736
15824
  return join3(configDir(), name);
15737
15825
  }
15826
+ function lastUpdateMarkerPath(profile) {
15827
+ const name = profile ? `last_update_${profile}` : "last_update";
15828
+ return join3(configDir(), name);
15829
+ }
15738
15830
  function daemonLogDir() {
15739
15831
  return join3(configDir(), "daemon", "logs");
15740
15832
  }
@@ -15871,6 +15963,7 @@ function fromApiTask(api2) {
15871
15963
  priority: api2.priority,
15872
15964
  type: api2.type,
15873
15965
  contextKey: api2.context_key ?? null,
15966
+ context: api2.context ?? undefined,
15874
15967
  agent: api2.agent ? { name: api2.agent.name, instructions: api2.agent.instructions, emailHandle: api2.agent.email_handle ?? undefined, userEmail: api2.agent.user_email ?? undefined, runtimeConfig: api2.agent.runtime_config ?? undefined } : undefined,
15875
15968
  repos: undefined,
15876
15969
  createdAt: api2.created_at
@@ -16015,6 +16108,9 @@ function releaseDaemonPid(profile) {
16015
16108
  removePidFileIfMatches(process.pid, profile);
16016
16109
  }
16017
16110
 
16111
+ // daemon/update-handler.ts
16112
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
16113
+
16018
16114
  // lib/update.ts
16019
16115
  import { spawn } from "child_process";
16020
16116
  function fetchLatestVersion() {
@@ -16049,16 +16145,39 @@ var MAX_RETRIES = 3;
16049
16145
  function isUpdating() {
16050
16146
  return updating;
16051
16147
  }
16052
- async function handleCliUpdate(version3, onSuccess) {
16148
+ function readUpdateMarker(profile) {
16149
+ try {
16150
+ return readFileSync4(lastUpdateMarkerPath(profile), "utf-8").trim() || null;
16151
+ } catch {
16152
+ return null;
16153
+ }
16154
+ }
16155
+ function writeUpdateMarker(version3, profile) {
16156
+ try {
16157
+ writeFileSync3(lastUpdateMarkerPath(profile), version3, { mode: 384 });
16158
+ } catch {}
16159
+ }
16160
+ function clearUpdateMarker(profile) {
16161
+ try {
16162
+ unlinkSync2(lastUpdateMarkerPath(profile));
16163
+ } catch {}
16164
+ }
16165
+ async function handleCliUpdate(version3, onSuccess, profile) {
16053
16166
  if (updating)
16054
16167
  return;
16055
16168
  if (retryCount >= MAX_RETRIES)
16056
16169
  return;
16170
+ const marker = readUpdateMarker(profile);
16171
+ if (marker === version3) {
16172
+ log.info(`Skipping update to v${version3} — already attempted (marker exists)`);
16173
+ return;
16174
+ }
16057
16175
  updating = true;
16058
16176
  try {
16059
16177
  log.info(`Updating CLI to v${version3}...`);
16060
16178
  const result = await runNpmUpdate(version3);
16061
16179
  if (result.success) {
16180
+ writeUpdateMarker(version3, profile);
16062
16181
  log.info(`CLI updated to v${version3} — restarting`);
16063
16182
  onSuccess();
16064
16183
  } else {
@@ -16074,7 +16193,7 @@ async function handleCliUpdate(version3, onSuccess) {
16074
16193
  }
16075
16194
 
16076
16195
  // daemon/daemon.ts
16077
- import { existsSync, mkdirSync as mkdirSync3, openSync, closeSync, renameSync, readdirSync, statSync, unlinkSync as unlinkSync2 } from "fs";
16196
+ import { existsSync, mkdirSync as mkdirSync3, openSync, closeSync, renameSync, readdirSync, statSync, unlinkSync as unlinkSync3 } from "fs";
16078
16197
  import { execSync as execSync3, spawn as spawn2 } from "child_process";
16079
16198
  import { fileURLToPath as fileURLToPath2 } from "url";
16080
16199
  import { dirname as dirname3, join as join4 } from "path";
@@ -16111,7 +16230,7 @@ function pruneSessionRunnerLogs() {
16111
16230
  withMtime.sort((a, b) => b.mtime - a.mtime);
16112
16231
  for (const entry of withMtime.slice(MAX_SESSION_RUNNER_LOGS)) {
16113
16232
  try {
16114
- unlinkSync2(join4(logDir, entry.name));
16233
+ unlinkSync3(join4(logDir, entry.name));
16115
16234
  } catch {}
16116
16235
  }
16117
16236
  }
@@ -16131,6 +16250,11 @@ async function startDaemon(profile, serverUrl) {
16131
16250
  const config2 = loadDaemonConfig(profile);
16132
16251
  if (serverUrl)
16133
16252
  config2.serverURL = serverUrl;
16253
+ const marker = readUpdateMarker(profile);
16254
+ if (marker && marker === config2.cliVersion) {
16255
+ clearUpdateMarker(profile);
16256
+ log.info(`Cleared update marker — now running v${config2.cliVersion}`);
16257
+ }
16134
16258
  const cliConfig = loadCLIConfigForProfile(profile);
16135
16259
  const workspaces = cliConfig.watched_workspaces || [];
16136
16260
  if (workspaces.length === 0) {
@@ -16271,8 +16395,8 @@ async function startDaemon(profile, serverUrl) {
16271
16395
  evictedIds.push(ws.workspaceId);
16272
16396
  continue;
16273
16397
  }
16274
- if (pending_update && !isUpdating()) {
16275
- handleCliUpdate(pending_update.version, () => requestRestart());
16398
+ if (pending_update && !isUpdating() && pending_update.version !== config2.cliVersion) {
16399
+ handleCliUpdate(pending_update.version, () => requestRestart(), profile);
16276
16400
  }
16277
16401
  for (const apiTask of apiTasks) {
16278
16402
  const task = fromApiTask(apiTask);
@@ -16541,7 +16665,7 @@ function configCommand() {
16541
16665
 
16542
16666
  // commands/email.ts
16543
16667
  import { Command as Command5 } from "commander";
16544
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
16668
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
16545
16669
  import { basename, join as join5 } from "path";
16546
16670
  import PostalMime from "postal-mime";
16547
16671
  var VALID_STATUSES = ["unread", "read", "archived"];
@@ -16632,7 +16756,7 @@ function emailCommand() {
16632
16756
  references: email3.references || ""
16633
16757
  };
16634
16758
  const metadataPath = join5(emailDir, "metadata.json");
16635
- writeFileSync3(metadataPath, JSON.stringify(metadata, null, 2));
16759
+ writeFileSync4(metadataPath, JSON.stringify(metadata, null, 2));
16636
16760
  downloadedPaths.push(metadataPath);
16637
16761
  let rawMime;
16638
16762
  try {
@@ -16648,12 +16772,12 @@ function emailCommand() {
16648
16772
  const parsed = await new PostalMime().parse(rawMime);
16649
16773
  if (parsed.text) {
16650
16774
  const bodyPath = join5(emailDir, "body.txt");
16651
- writeFileSync3(bodyPath, parsed.text);
16775
+ writeFileSync4(bodyPath, parsed.text);
16652
16776
  downloadedPaths.push(bodyPath);
16653
16777
  }
16654
16778
  if (parsed.html) {
16655
16779
  const htmlPath = join5(emailDir, "body.html");
16656
- writeFileSync3(htmlPath, parsed.html);
16780
+ writeFileSync4(htmlPath, parsed.html);
16657
16781
  downloadedPaths.push(htmlPath);
16658
16782
  }
16659
16783
  if (parsed.attachments && parsed.attachments.length > 0) {
@@ -16677,7 +16801,7 @@ function emailCommand() {
16677
16801
  } else {
16678
16802
  buf = Buffer.from(content);
16679
16803
  }
16680
- writeFileSync3(attPath, buf);
16804
+ writeFileSync4(attPath, buf);
16681
16805
  downloadedPaths.push(attPath);
16682
16806
  }
16683
16807
  }
@@ -16716,7 +16840,7 @@ function emailCommand() {
16716
16840
  const client = new APIClient(serverUrl, token, workspaceId);
16717
16841
  let htmlBody;
16718
16842
  try {
16719
- htmlBody = readFileSync4(opts.bodyFile, "utf-8");
16843
+ htmlBody = readFileSync5(opts.bodyFile, "utf-8");
16720
16844
  } catch (err) {
16721
16845
  console.error(`Error: cannot read body file "${opts.bodyFile}": ${err instanceof Error ? err.message : err}`);
16722
16846
  process.exit(1);
@@ -16732,7 +16856,7 @@ function emailCommand() {
16732
16856
  let bytes;
16733
16857
  let size;
16734
16858
  try {
16735
- bytes = readFileSync4(path);
16859
+ bytes = readFileSync5(path);
16736
16860
  size = statSync2(path).size;
16737
16861
  } catch (err) {
16738
16862
  console.error(`Error: cannot read attachment "${path}": ${err instanceof Error ? err.message : err}`);
@@ -17034,8 +17158,83 @@ ${result.output}`);
17034
17158
  return cmd;
17035
17159
  }
17036
17160
 
17161
+ // commands/sync.ts
17162
+ import { Command as Command9 } from "commander";
17163
+ import { readFileSync as readFileSync6, statSync as statSync3 } from "fs";
17164
+ import { basename as basename2 } from "path";
17165
+ var MIME_BY_EXT2 = {
17166
+ ".pdf": "application/pdf",
17167
+ ".png": "image/png",
17168
+ ".jpg": "image/jpeg",
17169
+ ".jpeg": "image/jpeg",
17170
+ ".gif": "image/gif",
17171
+ ".txt": "text/plain",
17172
+ ".html": "text/html",
17173
+ ".json": "application/json",
17174
+ ".csv": "text/csv",
17175
+ ".md": "text/markdown",
17176
+ ".ts": "text/typescript",
17177
+ ".js": "text/javascript",
17178
+ ".yaml": "text/yaml",
17179
+ ".yml": "text/yaml",
17180
+ ".svg": "image/svg+xml",
17181
+ ".xml": "application/xml",
17182
+ ".zip": "application/zip"
17183
+ };
17184
+ function guessContentType2(filename) {
17185
+ const idx = filename.lastIndexOf(".");
17186
+ if (idx < 0)
17187
+ return "application/octet-stream";
17188
+ const ext = filename.slice(idx).toLowerCase();
17189
+ return MIME_BY_EXT2[ext] ?? "application/octet-stream";
17190
+ }
17191
+ function resolveClientOpts3(command, agentId) {
17192
+ const parentOpts = command.parent?.parent?.opts() || {};
17193
+ const profile = parentOpts.profile;
17194
+ const cfg = loadCLIConfigForProfile(profile);
17195
+ const serverUrl = parentOpts.server || cfg.server_url;
17196
+ const workspaces = cfg.watched_workspaces || [];
17197
+ const ws = workspaces.find((w) => w.agent_ids?.includes(agentId));
17198
+ if (!ws || !ws.token) {
17199
+ console.error(`Error: no registered workspace contains agent ${agentId}. Run '${cmdPrefix()} register --token <token>' first.`);
17200
+ process.exit(1);
17201
+ }
17202
+ return { serverUrl, token: ws.token, workspaceId: ws.id };
17203
+ }
17204
+ function syncCommand() {
17205
+ const cmd = new Command9("sync").description("File sync utilities");
17206
+ cmd.command("upload-artifact").description("Upload a file artifact to a conversation").requiredOption("--agent_id <id>", "Agent ID").requiredOption("--conversation_id <id>", "Conversation ID").requiredOption("--file <path>", "Path to file to upload").action(async (opts, command) => {
17207
+ const { serverUrl, token, workspaceId } = resolveClientOpts3(command, opts.agent_id);
17208
+ const client = new APIClient(serverUrl, token, workspaceId);
17209
+ let bytes;
17210
+ let size;
17211
+ try {
17212
+ const stat = statSync3(opts.file);
17213
+ size = stat.size;
17214
+ bytes = readFileSync6(opts.file);
17215
+ } catch (err) {
17216
+ console.error(`Error: cannot read file "${opts.file}": ${err.message}`);
17217
+ process.exit(1);
17218
+ }
17219
+ const filename = basename2(opts.file);
17220
+ const contentType = guessContentType2(filename);
17221
+ const form = new FormData;
17222
+ form.append("file", new Blob([new Uint8Array(bytes)], { type: contentType }), filename);
17223
+ form.append("agent_id", opts.agent_id);
17224
+ form.append("conversation_id", opts.conversation_id);
17225
+ try {
17226
+ const result = await client.postMultipart("/api/artifacts/upload", form);
17227
+ printJSON(result);
17228
+ } catch (err) {
17229
+ console.error(`Error uploading artifact: ${err.message}`);
17230
+ process.exit(1);
17231
+ }
17232
+ });
17233
+ return cmd;
17234
+ }
17235
+
17037
17236
  // src/index.ts
17038
- var program = new Command9;
17237
+ var program = new Command10;
17039
17238
  program.name("alook").description("Alook CLI").option("--server <url>", "Server URL").option("--profile <name>", "Profile name");
17040
17239
  program.addCommand(registerCommand());
17041
17240
  program.addCommand(statusCommand());
@@ -17045,4 +17244,5 @@ program.addCommand(calendarCommand());
17045
17244
  program.addCommand(configCommand());
17046
17245
  program.addCommand(versionCommand());
17047
17246
  program.addCommand(updateCommand());
17247
+ program.addCommand(syncCommand());
17048
17248
  program.parse();
@@ -15,6 +15,8 @@ var __export = (target, all) => {
15
15
 
16
16
  // daemon/session-runner.ts
17
17
  import { createWriteStream } from "fs";
18
+ import { mkdir, writeFile, rm } from "fs/promises";
19
+ import path from "path";
18
20
 
19
21
  // ../shared/src/constants.ts
20
22
  var TASK_TYPES = {
@@ -13612,7 +13614,8 @@ var TaskApiBaseSchema = exports_external.object({
13612
13614
  error: exports_external.string().nullable(),
13613
13615
  created_at: exports_external.string(),
13614
13616
  type: exports_external.string(),
13615
- context_key: exports_external.string().nullable().optional()
13617
+ context_key: exports_external.string().nullable().optional(),
13618
+ context: exports_external.unknown().nullable().optional()
13616
13619
  });
13617
13620
  var TaskApiSchema = TaskApiBaseSchema.extend({
13618
13621
  agent: TaskAgentDataApiSchema.nullable().optional()
@@ -13731,6 +13734,64 @@ var CalendarEventApiSchema = exports_external.object({
13731
13734
  var AddWhitelistRequestSchema = exports_external.object({
13732
13735
  email: exports_external.string().email()
13733
13736
  });
13737
+ var RuntimeConfigSchema = exports_external.object({ model: exports_external.string().max(100).optional() }).passthrough().optional();
13738
+ var CreateAgentRequestSchema = exports_external.object({
13739
+ name: exports_external.string().min(1, "name is required"),
13740
+ description: exports_external.string().optional().default(""),
13741
+ instructions: exports_external.string().optional().default(""),
13742
+ runtime_id: exports_external.string().min(1, "runtime_id is required"),
13743
+ runtime_config: RuntimeConfigSchema,
13744
+ max_concurrent_tasks: exports_external.number().int().optional(),
13745
+ email_handle: exports_external.string().optional()
13746
+ });
13747
+ var UpdateAgentRequestSchema = exports_external.object({
13748
+ name: exports_external.string().min(1).optional(),
13749
+ description: exports_external.string().optional(),
13750
+ instructions: exports_external.string().optional(),
13751
+ runtime_id: exports_external.string().min(1).optional(),
13752
+ runtime_config: RuntimeConfigSchema
13753
+ }).refine((v) => v.name !== undefined || v.description !== undefined || v.instructions !== undefined || v.runtime_id !== undefined || v.runtime_config !== undefined, { message: "at least one field is required" });
13754
+ var CreateConversationRequestSchema = exports_external.object({
13755
+ agent_id: exports_external.string().min(1, "agent_id is required")
13756
+ });
13757
+ var CreateMessageRequestSchema = exports_external.object({
13758
+ content: exports_external.string().min(1, "content is required")
13759
+ });
13760
+ var EmailAttachmentSchema = exports_external.object({
13761
+ key: exports_external.string().min(1),
13762
+ filename: exports_external.string().min(1),
13763
+ size: exports_external.number().int().nonnegative().optional(),
13764
+ contentType: exports_external.string().min(1)
13765
+ });
13766
+ var SendEmailRequestSchema = exports_external.object({
13767
+ agentId: exports_external.string().min(1, "agentId is required"),
13768
+ to: exports_external.string().min(1, "to is required"),
13769
+ subject: exports_external.string().min(1, "subject is required"),
13770
+ htmlBody: exports_external.string().default(""),
13771
+ inReplyTo: exports_external.string().optional(),
13772
+ references: exports_external.string().optional(),
13773
+ attachments: exports_external.array(EmailAttachmentSchema).optional()
13774
+ });
13775
+ var UpdateEmailStatusRequestSchema = exports_external.object({
13776
+ status: exports_external.enum(["unread", "read", "archived"])
13777
+ });
13778
+ var EmailNotifyRequestSchema = exports_external.object({
13779
+ agentId: exports_external.string().min(1),
13780
+ workspaceId: exports_external.string().min(1),
13781
+ r2Key: exports_external.string().min(1),
13782
+ from: exports_external.string().min(1),
13783
+ to: exports_external.string().optional(),
13784
+ subject: exports_external.string().min(1),
13785
+ isWhitelisted: exports_external.boolean(),
13786
+ forwarded: exports_external.boolean().optional().default(false),
13787
+ messageId: exports_external.string().optional().default(""),
13788
+ inReplyTo: exports_external.string().optional().default(""),
13789
+ references: exports_external.string().optional().default("")
13790
+ });
13791
+ var CreateWorkspaceRequestSchema = exports_external.object({
13792
+ name: exports_external.string().min(1, "name is required"),
13793
+ slug: exports_external.string().min(1, "slug is required")
13794
+ });
13734
13795
  // ../../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
13735
13796
  var entityKind = Symbol.for("drizzle:entityKind");
13736
13797
  var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
@@ -15237,6 +15298,7 @@ var message = sqliteTable("message", {
15237
15298
  role: text("role").notNull(),
15238
15299
  content: text("content").notNull().default(""),
15239
15300
  taskId: text("task_id"),
15301
+ attachmentIds: text("attachment_ids"),
15240
15302
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15241
15303
  });
15242
15304
  var agentTaskQueue = sqliteTable("agent_task_queue", {
@@ -15321,6 +15383,24 @@ var calendarEvent = sqliteTable("calendar_event", {
15321
15383
  foreignColumns: [agent.id, agent.workspaceId]
15322
15384
  }).onDelete("cascade")
15323
15385
  ]);
15386
+ var artifact = sqliteTable("artifact", {
15387
+ id: text("id").primaryKey().$defaultFn(() => "art_" + nanoid3()),
15388
+ conversationId: text("conversation_id").notNull().references(() => conversation.id, { onDelete: "cascade" }),
15389
+ agentId: text("agent_id").notNull(),
15390
+ workspaceId: text("workspace_id").notNull().references(() => workspace.id, { onDelete: "cascade" }),
15391
+ filename: text("filename").notNull(),
15392
+ contentType: text("content_type").notNull().default("application/octet-stream"),
15393
+ size: integer2("size").notNull(),
15394
+ r2Key: text("r2_key").notNull(),
15395
+ source: text("source").notNull().default("agent"),
15396
+ createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15397
+ }, (t) => [
15398
+ index("idx_artifact_conversation").on(t.conversationId),
15399
+ foreignKey({
15400
+ columns: [t.agentId, t.workspaceId],
15401
+ foreignColumns: [agent.id, agent.workspaceId]
15402
+ }).onDelete("cascade")
15403
+ ]);
15324
15404
  var machineToken = sqliteTable("machine_token", {
15325
15405
  id: text("id").primaryKey().$defaultFn(() => nanoid3()),
15326
15406
  userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
@@ -15406,6 +15486,16 @@ class DaemonClient {
15406
15486
  error: error48
15407
15487
  });
15408
15488
  }
15489
+ async getArtifactMeta(token, artifactId, workspaceId) {
15490
+ return this.request("GET", `/api/artifacts/${artifactId}?workspace_id=${encodeURIComponent(workspaceId)}`, token);
15491
+ }
15492
+ async downloadArtifact(token, artifactId, workspaceId) {
15493
+ const res = await fetch(`${this.baseURL}/api/artifacts/${artifactId}/content?workspace_id=${encodeURIComponent(workspaceId)}`, { headers: { Authorization: `Bearer ${token}` } });
15494
+ if (!res.ok) {
15495
+ throw new Error(`artifact download failed: HTTP ${res.status}`);
15496
+ }
15497
+ return res.arrayBuffer();
15498
+ }
15409
15499
  reportMessages(token, taskId, messages) {
15410
15500
  return this.request("POST", `/api/daemon/tasks/${taskId}/messages`, token, { messages });
15411
15501
  }
@@ -16442,6 +16532,19 @@ To reply to an email, add '--in-reply-to <EMAIL_ID>' to the send command. This s
16442
16532
  `;
16443
16533
  }
16444
16534
  content += `
16535
+ ### Artifacts
16536
+ Upload files for your owner to review in the app.
16537
+ - Your current conversation id is available via env var: $ALOOK_CONVERSATION_ID
16538
+ - Run 'npx @alook/cli sync upload-artifact --agent_id ${task.agentId} --conversation_id $ALOOK_CONVERSATION_ID --file <PATH>'
16539
+ - Use this after generating plans, reports, or any file the owner should review.
16540
+ ---
16541
+
16542
+ ### Attachments
16543
+ When your task includes attachments, their local paths are listed in the prompt JSON under "attachments".
16544
+ Use your Read tool to open them. Images and PDFs are read visually.
16545
+ ---
16546
+ `;
16547
+ content += `
16445
16548
  ### Calendar
16446
16549
  You have your own calendar to setup daily routines and reminders.
16447
16550
  Schedule future tasks for yourself. At the scheduled time, a new task is dispatched to you with the event as the prompt (task type 'calendar_event').
@@ -16848,17 +16951,65 @@ function prepare(config2, task) {
16848
16951
  }
16849
16952
 
16850
16953
  // daemon/prompt.ts
16851
- function buildPrompt(task) {
16852
- return JSON.stringify({ type: task.type, instruction: task.prompt });
16954
+ function buildPrompt(task, attachments) {
16955
+ const obj = { type: task.type, instruction: task.prompt };
16956
+ if (attachments && attachments.length > 0) {
16957
+ obj.attachments = attachments.map((a) => ({
16958
+ path: a.path,
16959
+ content_type: a.content_type,
16960
+ filename: a.filename
16961
+ }));
16962
+ }
16963
+ return JSON.stringify(obj);
16853
16964
  }
16854
16965
 
16855
16966
  // daemon/session-runner.ts
16967
+ var ATTACHMENTS_BASE = "/tmp/alook-attachments";
16968
+ function sanitizeFilename(name) {
16969
+ return path.basename(name).replace(/[/\\]/g, "_").replace(/\.\./g, "_").slice(0, 255) || "file";
16970
+ }
16971
+ async function cleanupAttachments(taskId) {
16972
+ try {
16973
+ await rm(path.join(ATTACHMENTS_BASE, taskId), { recursive: true, force: true });
16974
+ } catch {}
16975
+ }
16976
+ async function downloadAttachments(client, token, workspaceId, taskId, attachmentIds) {
16977
+ const dir = path.join(ATTACHMENTS_BASE, taskId);
16978
+ await mkdir(dir, { recursive: true });
16979
+ const attachments = [];
16980
+ for (const artId of attachmentIds) {
16981
+ const meta3 = await client.getArtifactMeta(token, artId, workspaceId);
16982
+ const content = await client.downloadArtifact(token, artId, workspaceId);
16983
+ const filename = sanitizeFilename(meta3.filename);
16984
+ const localPath = path.join(dir, `${artId}_${filename}`);
16985
+ await writeFile(localPath, Buffer.from(content));
16986
+ attachments.push({
16987
+ path: localPath,
16988
+ content_type: meta3.content_type,
16989
+ filename: meta3.filename
16990
+ });
16991
+ }
16992
+ return attachments;
16993
+ }
16856
16994
  async function runSession(input) {
16857
16995
  const { task, provider, cliPath, model, serverURL, token, workspacesRoot, agentTimeout } = input;
16858
16996
  const client = new DaemonClient(serverURL);
16859
16997
  const backend = createBackend(provider, cliPath);
16860
- const prompt = buildPrompt(task);
16861
16998
  const { workDir, logFile, timelineDir, env } = prepare({ workspacesRoot }, task);
16999
+ const attachmentIds = task.context?.attachment_ids ?? [];
17000
+ let attachments;
17001
+ if (attachmentIds.length > 0) {
17002
+ try {
17003
+ attachments = await downloadAttachments(client, token, task.workspaceId, task.id, attachmentIds);
17004
+ } catch (e) {
17005
+ await cleanupAttachments(task.id);
17006
+ const errMsg = `failed to download attachments: ${e}`;
17007
+ log.error(`Task ${task.id} ${errMsg}`);
17008
+ await client.failTask(token, task.id, errMsg);
17009
+ return;
17010
+ }
17011
+ }
17012
+ const prompt = buildPrompt(task, attachments);
16862
17013
  const resumeSessionId = task.contextKey ? findResumableSessionByContextKey(timelineDir, task.contextKey, provider) ?? undefined : undefined;
16863
17014
  if (resumeSessionId) {
16864
17015
  log.info(`Task ${task.id} resuming session ${resumeSessionId} (context_key: ${task.contextKey})`);
@@ -16917,6 +17068,7 @@ async function runSession(input) {
16917
17068
  await flushMessages();
16918
17069
  } catch {}
16919
17070
  logStream?.end();
17071
+ await cleanupAttachments(task.id);
16920
17072
  updateEntry(timelineDir, task.id, (entry) => {
16921
17073
  entry.pid = null;
16922
17074
  entry.status = "killed";
@@ -16979,6 +17131,7 @@ async function runSession(input) {
16979
17131
  process.removeListener("SIGINT", onKill);
16980
17132
  if (killed)
16981
17133
  return;
17134
+ await cleanupAttachments(task.id);
16982
17135
  if (result.status === "completed") {
16983
17136
  updateEntry(timelineDir, task.id, (entry) => {
16984
17137
  entry.session_id = result.sessionId || null;
@@ -17024,6 +17177,7 @@ async function main() {
17024
17177
  await runSession(input);
17025
17178
  } catch (e) {
17026
17179
  log.error(`session-runner: unhandled error for task ${input.task.id}`, e);
17180
+ await cleanupAttachments(input.task.id);
17027
17181
  try {
17028
17182
  await client.failTask(input.token, input.task.id, `session-runner crash: ${e}`);
17029
17183
  } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/cli",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Alook CLI — register and run always-on AI coding agents.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/alookai/alook#readme",