@alook/cli 0.0.8 → 0.0.10

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";
@@ -167,7 +167,8 @@ function saveCLIConfigForProfile(profile, profileConfig) {
167
167
  // commands/register.ts
168
168
  function isCommandAvailable(cmd) {
169
169
  try {
170
- execSync(`which ${cmd}`, { stdio: "ignore" });
170
+ const check = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
171
+ execSync(check, { stdio: "ignore" });
171
172
  return true;
172
173
  } catch {
173
174
  return false;
@@ -13864,6 +13865,7 @@ var ClaimedTaskRowSchema = exports_external.object({
13864
13865
  result: exports_external.unknown().nullable(),
13865
13866
  context: exports_external.unknown().nullable(),
13866
13867
  type: exports_external.string().default(TASK_TYPES.USER_DM_MESSAGE),
13868
+ contextKey: exports_external.string().nullable().optional(),
13867
13869
  sessionId: exports_external.string().nullable(),
13868
13870
  createdAt: exports_external.coerce.date(),
13869
13871
  dispatchedAt: exports_external.coerce.date().nullable(),
@@ -13893,7 +13895,9 @@ var TaskApiBaseSchema = exports_external.object({
13893
13895
  result: exports_external.unknown().nullable(),
13894
13896
  error: exports_external.string().nullable(),
13895
13897
  created_at: exports_external.string(),
13896
- type: exports_external.string()
13898
+ type: exports_external.string(),
13899
+ context_key: exports_external.string().nullable().optional(),
13900
+ context: exports_external.unknown().nullable().optional()
13897
13901
  });
13898
13902
  var TaskApiSchema = TaskApiBaseSchema.extend({
13899
13903
  agent: TaskAgentDataApiSchema.nullable().optional()
@@ -14012,6 +14016,64 @@ var CalendarEventApiSchema = exports_external.object({
14012
14016
  var AddWhitelistRequestSchema = exports_external.object({
14013
14017
  email: exports_external.string().email()
14014
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
+ });
14015
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
14016
14078
  var entityKind = Symbol.for("drizzle:entityKind");
14017
14079
  var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
@@ -15518,6 +15580,7 @@ var message = sqliteTable("message", {
15518
15580
  role: text("role").notNull(),
15519
15581
  content: text("content").notNull().default(""),
15520
15582
  taskId: text("task_id"),
15583
+ attachmentIds: text("attachment_ids"),
15521
15584
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15522
15585
  });
15523
15586
  var agentTaskQueue = sqliteTable("agent_task_queue", {
@@ -15528,6 +15591,7 @@ var agentTaskQueue = sqliteTable("agent_task_queue", {
15528
15591
  conversationId: text("conversation_id").notNull().references(() => conversation.id),
15529
15592
  prompt: text("prompt").notNull(),
15530
15593
  type: text("type").notNull().default(TASK_TYPES.USER_DM_MESSAGE),
15594
+ contextKey: text("context_key"),
15531
15595
  status: text("status").notNull().default("queued"),
15532
15596
  priority: integer2("priority").notNull().default(0),
15533
15597
  result: text("result", { mode: "json" }),
@@ -15601,6 +15665,24 @@ var calendarEvent = sqliteTable("calendar_event", {
15601
15665
  foreignColumns: [agent.id, agent.workspaceId]
15602
15666
  }).onDelete("cascade")
15603
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
+ ]);
15604
15686
  var machineToken = sqliteTable("machine_token", {
15605
15687
  id: text("id").primaryKey().$defaultFn(() => nanoid3()),
15606
15688
  userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
@@ -15697,6 +15779,16 @@ class DaemonClient {
15697
15779
  error: error48
15698
15780
  });
15699
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
+ }
15700
15792
  reportMessages(token, taskId, messages) {
15701
15793
  return this.request("POST", `/api/daemon/tasks/${taskId}/messages`, token, { messages });
15702
15794
  }
@@ -15866,6 +15958,8 @@ function fromApiTask(api2) {
15866
15958
  status: api2.status,
15867
15959
  priority: api2.priority,
15868
15960
  type: api2.type,
15961
+ contextKey: api2.context_key ?? null,
15962
+ context: api2.context ?? undefined,
15869
15963
  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,
15870
15964
  repos: undefined,
15871
15965
  createdAt: api2.created_at
@@ -16077,7 +16171,8 @@ var _dir = dirname3(fileURLToPath2(import.meta.url));
16077
16171
  var sessionRunnerPath = existsSync(join4(_dir, "session-runner.js")) ? join4(_dir, "session-runner.js") : join4(_dir, "session-runner.ts");
16078
16172
  function isCommandAvailable2(cmd) {
16079
16173
  try {
16080
- execSync3(`which ${cmd}`, { stdio: "ignore" });
16174
+ const check2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
16175
+ execSync3(check2, { stdio: "ignore" });
16081
16176
  return true;
16082
16177
  } catch {
16083
16178
  return false;
@@ -16178,7 +16273,16 @@ async function startDaemon(profile, serverUrl) {
16178
16273
  runtimes
16179
16274
  });
16180
16275
  } catch (e) {
16181
- log.error(`Failed to register workspace ${ws.id}, skipping`, e);
16276
+ if (e instanceof Error && e.message.startsWith("HTTP 401")) {
16277
+ log.warn(`Workspace ${ws.id} token invalid — removing from config`);
16278
+ try {
16279
+ const cfg = loadCLIConfigForProfile(profile);
16280
+ cfg.watched_workspaces = (cfg.watched_workspaces || []).filter((w) => w.id !== ws.id);
16281
+ saveCLIConfigForProfile(profile, cfg);
16282
+ } catch {}
16283
+ } else {
16284
+ log.error(`Failed to register workspace ${ws.id}, skipping`, e);
16285
+ }
16182
16286
  continue;
16183
16287
  }
16184
16288
  log.info(`Workspace ${ws.id} registered — ${resp.runtimes.length} runtime(s)`);
@@ -16270,7 +16374,11 @@ async function startDaemon(profile, serverUrl) {
16270
16374
  });
16271
16375
  }
16272
16376
  } catch (e) {
16273
- log.debug("Poll error", e);
16377
+ if (e instanceof Error && e.message.startsWith("HTTP 401")) {
16378
+ evictedIds.push(ws.workspaceId);
16379
+ } else {
16380
+ log.debug("Poll error", e);
16381
+ }
16274
16382
  }
16275
16383
  }
16276
16384
  for (const id of evictedIds) {
@@ -17015,8 +17123,83 @@ ${result.output}`);
17015
17123
  return cmd;
17016
17124
  }
17017
17125
 
17126
+ // commands/sync.ts
17127
+ import { Command as Command9 } from "commander";
17128
+ import { readFileSync as readFileSync5, statSync as statSync3 } from "fs";
17129
+ import { basename as basename2 } from "path";
17130
+ var MIME_BY_EXT2 = {
17131
+ ".pdf": "application/pdf",
17132
+ ".png": "image/png",
17133
+ ".jpg": "image/jpeg",
17134
+ ".jpeg": "image/jpeg",
17135
+ ".gif": "image/gif",
17136
+ ".txt": "text/plain",
17137
+ ".html": "text/html",
17138
+ ".json": "application/json",
17139
+ ".csv": "text/csv",
17140
+ ".md": "text/markdown",
17141
+ ".ts": "text/typescript",
17142
+ ".js": "text/javascript",
17143
+ ".yaml": "text/yaml",
17144
+ ".yml": "text/yaml",
17145
+ ".svg": "image/svg+xml",
17146
+ ".xml": "application/xml",
17147
+ ".zip": "application/zip"
17148
+ };
17149
+ function guessContentType2(filename) {
17150
+ const idx = filename.lastIndexOf(".");
17151
+ if (idx < 0)
17152
+ return "application/octet-stream";
17153
+ const ext = filename.slice(idx).toLowerCase();
17154
+ return MIME_BY_EXT2[ext] ?? "application/octet-stream";
17155
+ }
17156
+ function resolveClientOpts3(command, agentId) {
17157
+ const parentOpts = command.parent?.parent?.opts() || {};
17158
+ const profile = parentOpts.profile;
17159
+ const cfg = loadCLIConfigForProfile(profile);
17160
+ const serverUrl = parentOpts.server || cfg.server_url;
17161
+ const workspaces = cfg.watched_workspaces || [];
17162
+ const ws = workspaces.find((w) => w.agent_ids?.includes(agentId));
17163
+ if (!ws || !ws.token) {
17164
+ console.error(`Error: no registered workspace contains agent ${agentId}. Run '${cmdPrefix()} register --token <token>' first.`);
17165
+ process.exit(1);
17166
+ }
17167
+ return { serverUrl, token: ws.token, workspaceId: ws.id };
17168
+ }
17169
+ function syncCommand() {
17170
+ const cmd = new Command9("sync").description("File sync utilities");
17171
+ 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) => {
17172
+ const { serverUrl, token, workspaceId } = resolveClientOpts3(command, opts.agent_id);
17173
+ const client = new APIClient(serverUrl, token, workspaceId);
17174
+ let bytes;
17175
+ let size;
17176
+ try {
17177
+ const stat = statSync3(opts.file);
17178
+ size = stat.size;
17179
+ bytes = readFileSync5(opts.file);
17180
+ } catch (err) {
17181
+ console.error(`Error: cannot read file "${opts.file}": ${err.message}`);
17182
+ process.exit(1);
17183
+ }
17184
+ const filename = basename2(opts.file);
17185
+ const contentType = guessContentType2(filename);
17186
+ const form = new FormData;
17187
+ form.append("file", new Blob([new Uint8Array(bytes)], { type: contentType }), filename);
17188
+ form.append("agent_id", opts.agent_id);
17189
+ form.append("conversation_id", opts.conversation_id);
17190
+ try {
17191
+ const result = await client.postMultipart("/api/artifacts/upload", form);
17192
+ printJSON(result);
17193
+ } catch (err) {
17194
+ console.error(`Error uploading artifact: ${err.message}`);
17195
+ process.exit(1);
17196
+ }
17197
+ });
17198
+ return cmd;
17199
+ }
17200
+
17018
17201
  // src/index.ts
17019
- var program = new Command9;
17202
+ var program = new Command10;
17020
17203
  program.name("alook").description("Alook CLI").option("--server <url>", "Server URL").option("--profile <name>", "Profile name");
17021
17204
  program.addCommand(registerCommand());
17022
17205
  program.addCommand(statusCommand());
@@ -17026,4 +17209,5 @@ program.addCommand(calendarCommand());
17026
17209
  program.addCommand(configCommand());
17027
17210
  program.addCommand(versionCommand());
17028
17211
  program.addCommand(updateCommand());
17212
+ program.addCommand(syncCommand());
17029
17213
  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 = {
@@ -13581,6 +13583,7 @@ var ClaimedTaskRowSchema = exports_external.object({
13581
13583
  result: exports_external.unknown().nullable(),
13582
13584
  context: exports_external.unknown().nullable(),
13583
13585
  type: exports_external.string().default(TASK_TYPES.USER_DM_MESSAGE),
13586
+ contextKey: exports_external.string().nullable().optional(),
13584
13587
  sessionId: exports_external.string().nullable(),
13585
13588
  createdAt: exports_external.coerce.date(),
13586
13589
  dispatchedAt: exports_external.coerce.date().nullable(),
@@ -13610,7 +13613,9 @@ var TaskApiBaseSchema = exports_external.object({
13610
13613
  result: exports_external.unknown().nullable(),
13611
13614
  error: exports_external.string().nullable(),
13612
13615
  created_at: exports_external.string(),
13613
- type: exports_external.string()
13616
+ type: exports_external.string(),
13617
+ context_key: exports_external.string().nullable().optional(),
13618
+ context: exports_external.unknown().nullable().optional()
13614
13619
  });
13615
13620
  var TaskApiSchema = TaskApiBaseSchema.extend({
13616
13621
  agent: TaskAgentDataApiSchema.nullable().optional()
@@ -13729,6 +13734,64 @@ var CalendarEventApiSchema = exports_external.object({
13729
13734
  var AddWhitelistRequestSchema = exports_external.object({
13730
13735
  email: exports_external.string().email()
13731
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
+ });
13732
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
13733
13796
  var entityKind = Symbol.for("drizzle:entityKind");
13734
13797
  var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
@@ -15235,6 +15298,7 @@ var message = sqliteTable("message", {
15235
15298
  role: text("role").notNull(),
15236
15299
  content: text("content").notNull().default(""),
15237
15300
  taskId: text("task_id"),
15301
+ attachmentIds: text("attachment_ids"),
15238
15302
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15239
15303
  });
15240
15304
  var agentTaskQueue = sqliteTable("agent_task_queue", {
@@ -15245,6 +15309,7 @@ var agentTaskQueue = sqliteTable("agent_task_queue", {
15245
15309
  conversationId: text("conversation_id").notNull().references(() => conversation.id),
15246
15310
  prompt: text("prompt").notNull(),
15247
15311
  type: text("type").notNull().default(TASK_TYPES.USER_DM_MESSAGE),
15312
+ contextKey: text("context_key"),
15248
15313
  status: text("status").notNull().default("queued"),
15249
15314
  priority: integer2("priority").notNull().default(0),
15250
15315
  result: text("result", { mode: "json" }),
@@ -15318,6 +15383,24 @@ var calendarEvent = sqliteTable("calendar_event", {
15318
15383
  foreignColumns: [agent.id, agent.workspaceId]
15319
15384
  }).onDelete("cascade")
15320
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
+ ]);
15321
15404
  var machineToken = sqliteTable("machine_token", {
15322
15405
  id: text("id").primaryKey().$defaultFn(() => nanoid3()),
15323
15406
  userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
@@ -15403,6 +15486,16 @@ class DaemonClient {
15403
15486
  error: error48
15404
15487
  });
15405
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
+ }
15406
15499
  reportMessages(token, taskId, messages) {
15407
15500
  return this.request("POST", `/api/daemon/tasks/${taskId}/messages`, token, { messages });
15408
15501
  }
@@ -16350,8 +16443,8 @@ var SYSTEM_PROMPT_BODY = `## Memory Management
16350
16443
  - For SPECIFIC yet LONG rules or pattern, write to experiences/[NAME].md, and add index to ./memory.md for later recall.
16351
16444
  ### whats is ESSENTIAL and SHORT Memory?
16352
16445
  - basic user profile, e.g.:
16353
- - "user name is gus"
16354
- - "user is working on alook"
16446
+ - "user name is ..."
16447
+ - "user is working on ..."
16355
16448
  - certain local project mapping, e.g.:
16356
16449
  - "alook means the project under /user/home/alook/"
16357
16450
  - when to read certain stuff, e.g.:
@@ -16388,6 +16481,7 @@ those json are sorted by datetime in asc order.
16388
16481
  - When you start a new task, read the last ~10 lines of today's timeline to understand what has been asked and done recently.
16389
16482
  - if you don't know the current datetime, obtain the current datetime first.
16390
16483
  - When user ask you something you don't have in your current context, try to read the timeline jsonl files for answer (today or previous days).
16484
+ - When access other local projects, make sure you read the CLAUDE.md/AGENTS.md file under the project root dir to understand the requirements.
16391
16485
  `;
16392
16486
  function buildInstructionContent(task) {
16393
16487
  const displayName = task.agent?.name || "Alook Agent";
@@ -16438,21 +16532,38 @@ To reply to an email, add '--in-reply-to <EMAIL_ID>' to the send command. This s
16438
16532
  `;
16439
16533
  }
16440
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 += `
16441
16548
  ### Calendar
16442
16549
  You have your own calendar to setup daily routines and reminders.
16443
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').
16551
+
16552
+ !USE Calendar when you think the tasks are recurring or it should be conducted in the future.
16553
+ !When scheduling calendar events relative to a weekday (e.g. "every Monday"), always run date '+%A' first to confirm today's weekday before calculating the target date
16444
16554
  ---
16555
+ Keep the event title informative and concise, less than 20 words.
16556
+ Place the event details in description.
16445
16557
  Create a one-off event:
16446
- - Run 'npx @alook/cli calendar set --agent_id ${task.agentId} --event_title "<PROMPT_TEXT>" --datetime <YYYY-MM-DDTHH:MM>'
16558
+ - Run 'npx @alook/cli calendar set --agent_id ${task.agentId} --event_title "<TASK_TITLE>" --description "<TASK_BODY>" --datetime <YYYY-MM-DDTHH:MM>'
16447
16559
  - '--datetime' is LOCAL time, format 'YYYY-MM-DDTHH:MM' (e.g. '2026-04-17T09:30'). Do NOT pass UTC / ISO strings with 'Z'.
16448
16560
  - '--event_title' becomes the task prompt when the event fires — write it as the instruction you want future-you to receive.
16449
- - Optional '--description "<text>"' — longer notes/context shown alongside the event in the web UI. Use it for anything that wouldn't fit cleanly in the title.
16450
16561
 
16451
16562
  Create a repeating event:
16452
16563
  - Add '--repeat <interval>' where interval is like '1day', '2hour', '1week', '1month'.
16453
16564
  - Optionally add '--repeat_stop_date <YYYY-MM-DD>' to stop the recurrence (local date).
16454
- - Example: 'npx @alook/cli calendar set --agent_id ${task.agentId} --event_title "daily standup summary" --datetime 2026-04-18T09:00 --repeat 1day --repeat_stop_date 2026-05-18'
16455
-
16565
+ - Example: 'npx @alook/cli calendar set --agent_id ${task.agentId} --event_title "<REPEAT_TASK_TITLE>" --description "<REPEAT_TASK_BODY>" --datetime 2026-04-18T09:00 --repeat 1day --repeat_stop_date 2026-05-18'
16566
+ ---
16456
16567
  List upcoming events:
16457
16568
  - Run 'npx @alook/cli calendar list --agent_id ${task.agentId}' (defaults: next 30 days, past 0 days).
16458
16569
  - Tune the window with '--future_days <N>' and '--past_days <N>'. Add '--json' for machine-readable output.
@@ -16785,9 +16896,10 @@ function updateEntry(timelineDir, taskId, updater) {
16785
16896
  }
16786
16897
  log.debug(`Timeline updateEntry: task_id ${taskId} not found in last 7 days`);
16787
16898
  }
16788
- function createTimelineEntry(taskId, prompt, type, sessionId, pid, provider) {
16899
+ function createTimelineEntry(taskId, prompt, type, sessionId, pid, provider, contextKey) {
16789
16900
  return {
16790
16901
  task_id: taskId,
16902
+ context_key: contextKey ?? null,
16791
16903
  session_id: sessionId || null,
16792
16904
  pid: pid ?? null,
16793
16905
  status: "running",
@@ -16800,7 +16912,9 @@ function createTimelineEntry(taskId, prompt, type, sessionId, pid, provider) {
16800
16912
  };
16801
16913
  }
16802
16914
  var DEFAULT_RESUME_MAX_AGE_MS = 3 * 60 * 60 * 1000;
16803
- function findResumableSessionId(timelineDir, type, provider, maxAgeMs = DEFAULT_RESUME_MAX_AGE_MS) {
16915
+ var EMAIL_RESUME_MAX_AGE_MS = 48 * 60 * 60 * 1000;
16916
+ function findResumableSessionByContextKey(timelineDir, contextKey, provider) {
16917
+ const maxAgeMs = contextKey.startsWith("email:") ? EMAIL_RESUME_MAX_AGE_MS : DEFAULT_RESUME_MAX_AGE_MS;
16804
16918
  const now = new Date;
16805
16919
  const cutoff = new Date(now.getTime() - maxAgeMs);
16806
16920
  const daysToScan = Math.ceil(maxAgeMs / 86400000) + 1;
@@ -16810,7 +16924,7 @@ function findResumableSessionId(timelineDir, type, provider, maxAgeMs = DEFAULT_
16810
16924
  }
16811
16925
  entries.sort((a, b) => new Date(b.datetime).getTime() - new Date(a.datetime).getTime());
16812
16926
  for (const entry of entries) {
16813
- if (entry.status === "completed" && entry.type === type && entry.provider === provider && entry.session_id && new Date(entry.datetime) >= cutoff) {
16927
+ if (entry.status === "completed" && entry.context_key === contextKey && entry.provider === provider && entry.session_id && new Date(entry.datetime) >= cutoff) {
16814
16928
  return entry.session_id;
16815
16929
  }
16816
16930
  }
@@ -16837,20 +16951,68 @@ function prepare(config2, task) {
16837
16951
  }
16838
16952
 
16839
16953
  // daemon/prompt.ts
16840
- function buildPrompt(task) {
16841
- 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);
16842
16964
  }
16843
16965
 
16844
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
+ }
16845
16994
  async function runSession(input) {
16846
16995
  const { task, provider, cliPath, model, serverURL, token, workspacesRoot, agentTimeout } = input;
16847
16996
  const client = new DaemonClient(serverURL);
16848
16997
  const backend = createBackend(provider, cliPath);
16849
- const prompt = buildPrompt(task);
16850
16998
  const { workDir, logFile, timelineDir, env } = prepare({ workspacesRoot }, task);
16851
- const resumeSessionId = task.type === TASK_TYPES.USER_DM_MESSAGE ? findResumableSessionId(timelineDir, task.type, provider) ?? undefined : undefined;
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);
17013
+ const resumeSessionId = task.contextKey ? findResumableSessionByContextKey(timelineDir, task.contextKey, provider) ?? undefined : undefined;
16852
17014
  if (resumeSessionId) {
16853
- log.info(`Task ${task.id} resuming session ${resumeSessionId}`);
17015
+ log.info(`Task ${task.id} resuming session ${resumeSessionId} (context_key: ${task.contextKey})`);
16854
17016
  }
16855
17017
  const session2 = backend.execute(prompt, {
16856
17018
  cwd: workDir,
@@ -16861,11 +17023,11 @@ async function runSession(input) {
16861
17023
  });
16862
17024
  const agentPid = session2.pid;
16863
17025
  const earlySessionId = await session2.sessionId;
16864
- await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, earlySessionId, process.pid, provider));
17026
+ await initEntryAsync(timelineDir, createTimelineEntry(task.id, task.prompt, task.type, earlySessionId, process.pid, provider, task.contextKey));
16865
17027
  const pendingMessages = [];
16866
17028
  let seq = 0;
16867
17029
  const BATCH_SIZE = Number(process.env.ALOOK_MESSAGE_BATCH_SIZE) || 20;
16868
- const FLUSH_INTERVAL_MS = Number(process.env.ALOOK_MESSAGE_FLUSH_INTERVAL_MS) || 2000;
17030
+ const FLUSH_INTERVAL_MS = Number(process.env.ALOOK_MESSAGE_FLUSH_INTERVAL_MS) || 100;
16869
17031
  const flushMessages = async () => {
16870
17032
  if (pendingMessages.length === 0)
16871
17033
  return;
@@ -16906,6 +17068,7 @@ async function runSession(input) {
16906
17068
  await flushMessages();
16907
17069
  } catch {}
16908
17070
  logStream?.end();
17071
+ await cleanupAttachments(task.id);
16909
17072
  updateEntry(timelineDir, task.id, (entry) => {
16910
17073
  entry.pid = null;
16911
17074
  entry.status = "killed";
@@ -16968,6 +17131,7 @@ async function runSession(input) {
16968
17131
  process.removeListener("SIGINT", onKill);
16969
17132
  if (killed)
16970
17133
  return;
17134
+ await cleanupAttachments(task.id);
16971
17135
  if (result.status === "completed") {
16972
17136
  updateEntry(timelineDir, task.id, (entry) => {
16973
17137
  entry.session_id = result.sessionId || null;
@@ -17013,6 +17177,7 @@ async function main() {
17013
17177
  await runSession(input);
17014
17178
  } catch (e) {
17015
17179
  log.error(`session-runner: unhandled error for task ${input.task.id}`, e);
17180
+ await cleanupAttachments(input.task.id);
17016
17181
  try {
17017
17182
  await client.failTask(input.token, input.task.id, `session-runner crash: ${e}`);
17018
17183
  } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/cli",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
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",