@alook/cli 0.0.2 → 0.0.4

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.
Files changed (2) hide show
  1. package/dist/index.js +436 -165
  2. package/package.json +1 -1
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 Command7 } from "commander";
18
+ import { Command as Command8 } from "commander";
19
19
 
20
20
  // commands/register.ts
21
21
  import { Command } from "commander";
@@ -64,6 +64,25 @@ class APIClient {
64
64
  patchJSON(path, body) {
65
65
  return this.request("PATCH", path, body);
66
66
  }
67
+ async postMultipart(path, form) {
68
+ const headers = {
69
+ Authorization: `Bearer ${this.token}`
70
+ };
71
+ if (this.workspaceId)
72
+ headers["X-Workspace-ID"] = this.workspaceId;
73
+ const res = await fetch(this.baseURL + path, {
74
+ method: "POST",
75
+ headers,
76
+ body: form
77
+ });
78
+ if (!res.ok) {
79
+ const text = await res.text();
80
+ throw new Error(`HTTP ${res.status}: ${text}`);
81
+ }
82
+ if (res.status === 204)
83
+ return;
84
+ return res.json();
85
+ }
67
86
  async getText(path) {
68
87
  const headers = {
69
88
  Authorization: `Bearer ${this.token}`
@@ -97,7 +116,7 @@ function isDev() {
97
116
  return !!process.env.ALOOK_SERVER_URL;
98
117
  }
99
118
  function cmdPrefix() {
100
- return isDev() ? "pnpm dev:cli" : "alook";
119
+ return isDev() ? "pnpm dev:cli" : "npx @alook/cli";
101
120
  }
102
121
 
103
122
  // lib/config.ts
@@ -145,137 +164,6 @@ function saveCLIConfigForProfile(profile, profileConfig) {
145
164
  saveCLIConfig(cfg);
146
165
  }
147
166
 
148
- // lib/installer.ts
149
- import { spawnSync } from "child_process";
150
-
151
- // lib/version.ts
152
- import { readFileSync as readFileSync2 } from "fs";
153
- import { join as join2, dirname } from "path";
154
- import { fileURLToPath } from "url";
155
- function getCurrentVersion() {
156
- const __dirname2 = dirname(fileURLToPath(import.meta.url));
157
- const candidates = [
158
- join2(__dirname2, "..", "package.json"),
159
- join2(__dirname2, "..", "..", "package.json")
160
- ];
161
- for (const candidate of candidates) {
162
- try {
163
- const pkg = JSON.parse(readFileSync2(candidate, "utf-8"));
164
- if (typeof pkg.version === "string")
165
- return pkg.version;
166
- } catch {}
167
- }
168
- return "unknown";
169
- }
170
-
171
- // lib/installer.ts
172
- var PACKAGE = "@alook/cli";
173
- var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE}/latest`;
174
- var FETCH_TIMEOUT_MS = 3000;
175
- async function fetchLatestVersion() {
176
- const ctrl = new AbortController;
177
- const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
178
- try {
179
- const res = await fetch(REGISTRY_URL, { signal: ctrl.signal });
180
- if (!res.ok)
181
- return null;
182
- const data = await res.json();
183
- return typeof data.version === "string" ? data.version : null;
184
- } catch {
185
- return null;
186
- } finally {
187
- clearTimeout(timer);
188
- }
189
- }
190
- function isNewer(current, latest) {
191
- const parse = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
192
- const [cMaj = 0, cMin = 0, cPatch = 0] = parse(current);
193
- const [lMaj = 0, lMin = 0, lPatch = 0] = parse(latest);
194
- if (lMaj !== cMaj)
195
- return lMaj > cMaj;
196
- if (lMin !== cMin)
197
- return lMin > cMin;
198
- return lPatch > cPatch;
199
- }
200
- function isNpx() {
201
- return process.env.npm_command === "exec" || !!process.env.npm_execpath?.includes("npx-cli");
202
- }
203
- function detectPackageManager() {
204
- const ua = process.env.npm_config_user_agent || "";
205
- if (ua.startsWith("pnpm/"))
206
- return "pnpm";
207
- if (ua.startsWith("yarn/"))
208
- return "yarn";
209
- return "npm";
210
- }
211
- function installArgs(pm) {
212
- switch (pm) {
213
- case "pnpm":
214
- return ["pnpm", ["add", "-g", PACKAGE]];
215
- case "yarn":
216
- return ["yarn", ["global", "add", PACKAGE]];
217
- case "npm":
218
- return ["npm", ["install", "-g", `${PACKAGE}@latest`]];
219
- }
220
- }
221
- function installCmdString(pm) {
222
- const [bin, args] = installArgs(pm);
223
- return `${bin} ${args.join(" ")}`;
224
- }
225
- function runInstall(pm) {
226
- const [bin, args] = installArgs(pm);
227
- const result = spawnSync(bin, args, { stdio: "inherit" });
228
- return result.status === 0;
229
- }
230
- async function ensureInstalled(opts = {}, deps = {}) {
231
- const fetchLatest = deps.fetchLatest ?? fetchLatestVersion;
232
- const runInst = deps.runInstall ?? runInstall;
233
- const getCurrent = deps.getCurrent ?? getCurrentVersion;
234
- const isNpxFn = deps.isNpxFn ?? isNpx;
235
- const isDevFn = deps.isDevFn ?? isDev;
236
- const log = deps.log ?? ((m) => console.log(m));
237
- const current = getCurrent();
238
- const pm = detectPackageManager();
239
- if (isDevFn() || opts.skip) {
240
- return { skipped: true, action: "none", current, latest: null, pm };
241
- }
242
- const latest = await fetchLatest();
243
- if (!latest) {
244
- return { skipped: false, action: "none", current, latest: null, pm };
245
- }
246
- const runningViaNpx = isNpxFn();
247
- const needsInstall = runningViaNpx;
248
- const needsUpdate = !runningViaNpx && isNewer(current, latest);
249
- if (!needsInstall && !needsUpdate) {
250
- log(`
251
- ✓ ${PACKAGE} is up to date (${current})`);
252
- return { skipped: false, action: "none", current, latest, pm };
253
- }
254
- const cmdStr = installCmdString(pm);
255
- if (needsInstall) {
256
- log(`
257
- Installing ${PACKAGE} globally (${cmdStr})...`);
258
- } else {
259
- log(`
260
- Updating ${PACKAGE} ${current} → ${latest} (${cmdStr})...`);
261
- }
262
- const ok = runInst(pm);
263
- if (!ok) {
264
- log(`
265
- Could not ${needsInstall ? "install" : "update"} ${PACKAGE} automatically.`);
266
- log(`Install it manually: ${cmdStr}`);
267
- return { skipped: false, action: "failed", current, latest, pm };
268
- }
269
- log(`✓ ${needsInstall ? "Installed" : "Updated"} ${PACKAGE} ${latest}`);
270
- return {
271
- skipped: false,
272
- action: needsInstall ? "installed" : "updated",
273
- current,
274
- latest,
275
- pm
276
- };
277
- }
278
-
279
167
  // commands/register.ts
280
168
  function isCommandAvailable(cmd) {
281
169
  try {
@@ -299,7 +187,7 @@ function detectRuntimes() {
299
187
  return found;
300
188
  }
301
189
  function registerCommand() {
302
- const cmd = new Command("register").description("Register CLI with your Alook account").requiredOption("--token <token>", "API token (starts with al_)").option("--server <url>", "Server URL").option("--profile <name>", "Profile name").option("--no-install", "Skip auto-install/update of @alook/cli").action(async (opts, command) => {
190
+ const cmd = new Command("register").description("Register CLI with your Alook account").requiredOption("--token <token>", "API token (starts with al_)").option("--server <url>", "Server URL").option("--profile <name>", "Profile name").action(async (opts, command) => {
303
191
  const token = opts.token;
304
192
  const profile = opts.profile || command.parent?.opts().profile;
305
193
  const serverUrl = opts.server || command.parent?.opts().server || process.env.ALOOK_SERVER_URL || "https://alook.ai";
@@ -380,7 +268,6 @@ Usage: ${cmdPrefix()} register --token <token>`);
380
268
  Registered as ${me.email}`);
381
269
  console.log(`Workspace: ${ws.name} (${ws.id})`);
382
270
  console.log(`Runtimes: ${activateResp.runtimes.map((r) => r.provider).join(", ")}`);
383
- await ensureInstalled({ skip: opts.install === false });
384
271
  console.log();
385
272
  console.log(`Run '${cmdPrefix()} daemon start --foreground' to start the daemon.`);
386
273
  });
@@ -410,9 +297,14 @@ function statusCommand() {
410
297
  import { Command as Command3 } from "commander";
411
298
  import { spawn as spawn2 } from "child_process";
412
299
  import { openSync, closeSync, mkdirSync as mkdirSync3 } from "fs";
413
- import { dirname as dirname4 } from "path";
300
+ import { dirname as dirname3 } from "path";
414
301
 
415
302
  // ../shared/src/constants.ts
303
+ var TASK_TYPES = {
304
+ USER_DM_MESSAGE: "user_dm_message",
305
+ EMAIL_NOTIFICATION: "email_notification",
306
+ CALENDAR_EVENT: "calendar_event"
307
+ };
416
308
  var POLL_INTERVAL_MS = Number(process.env.POLL_INTERVAL_MS) || 3000;
417
309
  var OFFLINE_THRESHOLD_MS = Number(process.env.OFFLINE_THRESHOLD_MS) || 9000;
418
310
  var EVENT_POLL_INTERVAL_MS = Number(process.env.EVENT_POLL_INTERVAL_MS) || 2000;
@@ -13971,7 +13863,7 @@ var ClaimedTaskRowSchema = exports_external.object({
13971
13863
  priority: exports_external.coerce.number(),
13972
13864
  result: exports_external.unknown().nullable(),
13973
13865
  context: exports_external.unknown().nullable(),
13974
- type: exports_external.string().default("user_dm_message"),
13866
+ type: exports_external.string().default(TASK_TYPES.USER_DM_MESSAGE),
13975
13867
  sessionId: exports_external.string().nullable(),
13976
13868
  createdAt: exports_external.coerce.date(),
13977
13869
  dispatchedAt: exports_external.coerce.date().nullable(),
@@ -14064,6 +13956,50 @@ var MessageItemSchema = exports_external.object({
14064
13956
  var ReportMessagesRequestSchema = exports_external.object({
14065
13957
  messages: exports_external.array(MessageItemSchema)
14066
13958
  });
13959
+ var RepeatIntervalSchema = exports_external.string().regex(/^\d+(min|hour|day|week|month)$/, {
13960
+ message: "repeat_interval must match <positive_integer><min|hour|day|week|month>"
13961
+ });
13962
+ var CreateCalendarEventRequestSchema = exports_external.object({
13963
+ agent_id: exports_external.string().min(1),
13964
+ title: exports_external.string().min(1),
13965
+ description: exports_external.string().max(20000).optional(),
13966
+ scheduled_at: exports_external.string().min(1).refine((s) => !Number.isNaN(Date.parse(s)), {
13967
+ message: "scheduled_at must be a valid ISO datetime"
13968
+ }),
13969
+ repeat_interval: RepeatIntervalSchema.optional(),
13970
+ repeat_stop_date: exports_external.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
13971
+ }).refine((data) => !data.repeat_stop_date || !!data.repeat_interval, {
13972
+ message: "repeat_stop_date requires repeat_interval",
13973
+ path: ["repeat_stop_date"]
13974
+ });
13975
+ var UpdateCalendarEventRequestSchema = exports_external.object({
13976
+ title: exports_external.string().min(1).optional(),
13977
+ description: exports_external.string().max(20000).nullable().optional(),
13978
+ agent_id: exports_external.string().min(1).optional(),
13979
+ scheduled_at: exports_external.string().min(1).refine((s) => !Number.isNaN(Date.parse(s)), {
13980
+ message: "scheduled_at must be a valid ISO datetime"
13981
+ }).optional(),
13982
+ repeat_interval: RepeatIntervalSchema.nullable().optional(),
13983
+ repeat_stop_date: exports_external.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional(),
13984
+ scope: exports_external.enum(["this", "following"]).optional(),
13985
+ occurrence_at: exports_external.string().min(1).refine((s) => !Number.isNaN(Date.parse(s)), {
13986
+ message: "occurrence_at must be a valid ISO datetime"
13987
+ }).optional()
13988
+ }).refine((v) => v.title !== undefined || v.description !== undefined || v.agent_id !== undefined || v.scheduled_at !== undefined || v.repeat_interval !== undefined || v.repeat_stop_date !== undefined, { message: "at least one field is required" });
13989
+ var CalendarEventApiSchema = exports_external.object({
13990
+ id: exports_external.string(),
13991
+ agent_id: exports_external.string(),
13992
+ workspace_id: exports_external.string(),
13993
+ title: exports_external.string(),
13994
+ description: exports_external.string().nullable(),
13995
+ scheduled_at: exports_external.string(),
13996
+ occurrence_at: exports_external.string(),
13997
+ repeat_interval: exports_external.string().nullable(),
13998
+ repeat_stop_at: exports_external.string().nullable(),
13999
+ last_triggered_at: exports_external.string().nullable(),
14000
+ created_at: exports_external.string(),
14001
+ updated_at: exports_external.string()
14002
+ });
14067
14003
  // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@4.20260414.1_@opentelemetry+api@1.9.1_bun-types@1.3.12_kysely@0.28.15/node_modules/drizzle-orm/entity.js
14068
14004
  var entityKind = Symbol.for("drizzle:entityKind");
14069
14005
  var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
@@ -14650,7 +14586,7 @@ function sql(strings, ...params) {
14650
14586
  return new SQL([new StringChunk(str)]);
14651
14587
  }
14652
14588
  sql2.raw = raw;
14653
- function join3(chunks, separator) {
14589
+ function join2(chunks, separator) {
14654
14590
  const result = [];
14655
14591
  for (const [i, chunk] of chunks.entries()) {
14656
14592
  if (i > 0 && separator !== undefined) {
@@ -14660,7 +14596,7 @@ function sql(strings, ...params) {
14660
14596
  }
14661
14597
  return new SQL(result);
14662
14598
  }
14663
- sql2.join = join3;
14599
+ sql2.join = join2;
14664
14600
  function identifier(value) {
14665
14601
  return new Name(value);
14666
14602
  }
@@ -15556,6 +15492,7 @@ var conversation = sqliteTable("conversation", {
15556
15492
  agentId: text("agent_id").notNull(),
15557
15493
  userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
15558
15494
  title: text("title").notNull().default(""),
15495
+ type: text("type").notNull().default(TASK_TYPES.USER_DM_MESSAGE),
15559
15496
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
15560
15497
  }, (t) => [
15561
15498
  foreignKey({
@@ -15578,7 +15515,7 @@ var agentTaskQueue = sqliteTable("agent_task_queue", {
15578
15515
  workspaceId: text("workspace_id").notNull().references(() => workspace.id),
15579
15516
  conversationId: text("conversation_id").notNull().references(() => conversation.id),
15580
15517
  prompt: text("prompt").notNull(),
15581
- type: text("type").notNull().default("user_dm_message"),
15518
+ type: text("type").notNull().default(TASK_TYPES.USER_DM_MESSAGE),
15582
15519
  status: text("status").notNull().default("queued"),
15583
15520
  priority: integer2("priority").notNull().default(0),
15584
15521
  result: text("result", { mode: "json" }),
@@ -15628,6 +15565,27 @@ var emails = sqliteTable("emails", {
15628
15565
  foreignColumns: [agent.id, agent.workspaceId]
15629
15566
  }).onDelete("cascade")
15630
15567
  ]);
15568
+ var calendarEvent = sqliteTable("calendar_event", {
15569
+ id: text("id").primaryKey().$defaultFn(() => "ce_" + nanoid3()),
15570
+ agentId: text("agent_id").notNull(),
15571
+ workspaceId: text("workspace_id").notNull(),
15572
+ title: text("title").notNull(),
15573
+ description: text("description"),
15574
+ scheduledAt: text("scheduled_at").notNull(),
15575
+ repeatInterval: text("repeat_interval"),
15576
+ repeatStopAt: text("repeat_stop_at"),
15577
+ lastTriggeredAt: text("last_triggered_at"),
15578
+ exceptions: text("exceptions", { mode: "json" }).$type().notNull().default(sql`'[]'`),
15579
+ createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString()),
15580
+ updatedAt: text("updated_at").notNull().$defaultFn(() => new Date().toISOString())
15581
+ }, (t) => [
15582
+ index("idx_calendar_event_agent_ws").on(t.agentId, t.workspaceId),
15583
+ index("idx_calendar_event_ws_scheduled").on(t.workspaceId, t.scheduledAt),
15584
+ foreignKey({
15585
+ columns: [t.agentId, t.workspaceId],
15586
+ foreignColumns: [agent.id, agent.workspaceId]
15587
+ }).onDelete("cascade")
15588
+ ]);
15631
15589
  var machineToken = sqliteTable("machine_token", {
15632
15590
  id: text("id").primaryKey().$defaultFn(() => nanoid3()),
15633
15591
  userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
@@ -15713,19 +15671,19 @@ class DaemonClient {
15713
15671
 
15714
15672
  // daemon/config.ts
15715
15673
  import { hostname as hostname4 } from "os";
15716
- import { join as join3 } from "path";
15674
+ import { join as join2 } from "path";
15717
15675
  function pidFilePath(profile) {
15718
15676
  const name = profile ? `daemon_${profile}.pid` : "daemon.pid";
15719
- return join3(configDir(), name);
15677
+ return join2(configDir(), name);
15720
15678
  }
15721
15679
  function daemonLogDir() {
15722
- return join3(configDir(), "daemon", "logs");
15680
+ return join2(configDir(), "daemon", "logs");
15723
15681
  }
15724
15682
  function daemonLogFilePath(date5 = new Date) {
15725
15683
  const y = date5.getFullYear();
15726
15684
  const m = String(date5.getMonth() + 1).padStart(2, "0");
15727
15685
  const d = String(date5.getDate()).padStart(2, "0");
15728
- return join3(daemonLogDir(), `${y}-${m}-${d}.log`);
15686
+ return join2(daemonLogDir(), `${y}-${m}-${d}.log`);
15729
15687
  }
15730
15688
  function parseDuration(s) {
15731
15689
  if (!s)
@@ -15765,7 +15723,7 @@ function loadDaemonConfig(profile) {
15765
15723
  if (profile && !daemonId.endsWith(`-${profile}`)) {
15766
15724
  daemonId = `${daemonId}-${profile}`;
15767
15725
  }
15768
- const defaultRoot = join3(configDir(), profile ? `workspaces_${profile}` : "workspaces");
15726
+ const defaultRoot = join2(configDir(), profile ? `workspaces_${profile}` : "workspaces");
15769
15727
  const workspacesRoot = process.env.ALOOK_WORKSPACES_ROOT || defaultRoot;
15770
15728
  return {
15771
15729
  serverURL: normalizeServerBaseURL(process.env.ALOOK_SERVER_URL || "https://alook.ai"),
@@ -15947,8 +15905,8 @@ function createLogger2(level) {
15947
15905
  var log = createLogger2();
15948
15906
 
15949
15907
  // daemon/pidfile.ts
15950
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
15951
- import { dirname as dirname2 } from "path";
15908
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
15909
+ import { dirname } from "path";
15952
15910
  function isProcessAlive(pid) {
15953
15911
  try {
15954
15912
  process.kill(pid, 0);
@@ -15959,7 +15917,7 @@ function isProcessAlive(pid) {
15959
15917
  }
15960
15918
  function readDaemonPid(profile) {
15961
15919
  try {
15962
- const content = readFileSync3(pidFilePath(profile), "utf-8").trim();
15920
+ const content = readFileSync2(pidFilePath(profile), "utf-8").trim();
15963
15921
  const pid = parseInt(content, 10);
15964
15922
  return Number.isNaN(pid) ? null : pid;
15965
15923
  } catch {
@@ -15969,14 +15927,14 @@ function readDaemonPid(profile) {
15969
15927
  function acquireDaemonPid(profile) {
15970
15928
  const pidPath = pidFilePath(profile);
15971
15929
  try {
15972
- const content = readFileSync3(pidPath, "utf-8").trim();
15930
+ const content = readFileSync2(pidPath, "utf-8").trim();
15973
15931
  const existingPid = parseInt(content, 10);
15974
15932
  if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
15975
15933
  log.error(`Another daemon is already running (PID ${existingPid}). ` + `Remove ${pidPath} if this is stale.`);
15976
15934
  return false;
15977
15935
  }
15978
15936
  } catch {}
15979
- mkdirSync2(dirname2(pidPath), { recursive: true, mode: 448 });
15937
+ mkdirSync2(dirname(pidPath), { recursive: true, mode: 448 });
15980
15938
  writeFileSync2(pidPath, String(process.pid), { mode: 384 });
15981
15939
  return true;
15982
15940
  }
@@ -15995,8 +15953,8 @@ function releaseDaemonPid(profile) {
15995
15953
 
15996
15954
  // daemon/daemon.ts
15997
15955
  import { execSync as execSync3, spawn } from "child_process";
15998
- import { fileURLToPath as fileURLToPath2 } from "url";
15999
- import { dirname as dirname3, join as join4 } from "path";
15956
+ import { fileURLToPath } from "url";
15957
+ import { dirname as dirname2, join as join3 } from "path";
16000
15958
  function isCommandAvailable2(cmd) {
16001
15959
  try {
16002
15960
  execSync3(`which ${cmd}`, { stdio: "ignore" });
@@ -16168,7 +16126,7 @@ async function startDaemon(profile, serverUrl) {
16168
16126
  process.on("SIGINT", shutdown);
16169
16127
  await pollCycle();
16170
16128
  }
16171
- var SESSION_RUNNER_PATH = join4(dirname3(fileURLToPath2(import.meta.url)), "session-runner.ts");
16129
+ var SESSION_RUNNER_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "session-runner.ts");
16172
16130
  function spawnSessionRunner(input) {
16173
16131
  const encoded = Buffer.from(JSON.stringify(input)).toString("base64");
16174
16132
  const child = spawn("bun", ["run", SESSION_RUNNER_PATH, encoded], {
@@ -16246,7 +16204,7 @@ async function startInBackground(profile, serverUrl) {
16246
16204
  return;
16247
16205
  }
16248
16206
  const logPath = daemonLogFilePath();
16249
- mkdirSync3(dirname4(logPath), { recursive: true, mode: 448 });
16207
+ mkdirSync3(dirname3(logPath), { recursive: true, mode: 448 });
16250
16208
  const logFd = openSync(logPath, "a", 384);
16251
16209
  const child = spawn2(process.execPath, buildChildArgs(profile, serverUrl), {
16252
16210
  detached: true,
@@ -16356,11 +16314,37 @@ function configCommand() {
16356
16314
 
16357
16315
  // commands/email.ts
16358
16316
  import { Command as Command5 } from "commander";
16359
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
16360
- import { join as join5 } from "path";
16317
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync3, statSync } from "fs";
16318
+ import { basename, join as join4 } from "path";
16361
16319
  import PostalMime from "postal-mime";
16362
16320
  var VALID_STATUSES = ["unread", "read", "archived"];
16363
16321
  var EMAIL_DIR = "/tmp/alook-emails";
16322
+ var MIME_BY_EXT = {
16323
+ ".pdf": "application/pdf",
16324
+ ".png": "image/png",
16325
+ ".jpg": "image/jpeg",
16326
+ ".jpeg": "image/jpeg",
16327
+ ".gif": "image/gif",
16328
+ ".webp": "image/webp",
16329
+ ".svg": "image/svg+xml",
16330
+ ".txt": "text/plain",
16331
+ ".html": "text/html",
16332
+ ".htm": "text/html",
16333
+ ".json": "application/json",
16334
+ ".csv": "text/csv",
16335
+ ".md": "text/markdown",
16336
+ ".zip": "application/zip"
16337
+ };
16338
+ function guessContentType(filename) {
16339
+ const idx = filename.lastIndexOf(".");
16340
+ if (idx < 0)
16341
+ return "application/octet-stream";
16342
+ const ext = filename.slice(idx).toLowerCase();
16343
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
16344
+ }
16345
+ function collectRepeated(value, previous) {
16346
+ return previous.concat([value]);
16347
+ }
16364
16348
  function resolveClientOpts(command, opts) {
16365
16349
  const parentOpts = command.parent?.parent?.opts() || {};
16366
16350
  const profile = parentOpts.profile;
@@ -16407,7 +16391,7 @@ function emailCommand() {
16407
16391
  mkdirSync4(EMAIL_DIR, { recursive: true });
16408
16392
  const downloadedPaths = [];
16409
16393
  for (const email3 of emails2) {
16410
- const emailDir = join5(EMAIL_DIR, email3.id);
16394
+ const emailDir = join4(EMAIL_DIR, email3.id);
16411
16395
  mkdirSync4(emailDir, { recursive: true });
16412
16396
  const metadata = {
16413
16397
  id: email3.id,
@@ -16417,7 +16401,7 @@ function emailCommand() {
16417
16401
  date: email3.created_at,
16418
16402
  status: email3.status
16419
16403
  };
16420
- const metadataPath = join5(emailDir, "metadata.json");
16404
+ const metadataPath = join4(emailDir, "metadata.json");
16421
16405
  writeFileSync3(metadataPath, JSON.stringify(metadata, null, 2));
16422
16406
  downloadedPaths.push(metadataPath);
16423
16407
  let rawMime;
@@ -16433,17 +16417,17 @@ function emailCommand() {
16433
16417
  }
16434
16418
  const parsed = await new PostalMime().parse(rawMime);
16435
16419
  if (parsed.text) {
16436
- const bodyPath = join5(emailDir, "body.txt");
16420
+ const bodyPath = join4(emailDir, "body.txt");
16437
16421
  writeFileSync3(bodyPath, parsed.text);
16438
16422
  downloadedPaths.push(bodyPath);
16439
16423
  }
16440
16424
  if (parsed.html) {
16441
- const htmlPath = join5(emailDir, "body.html");
16425
+ const htmlPath = join4(emailDir, "body.html");
16442
16426
  writeFileSync3(htmlPath, parsed.html);
16443
16427
  downloadedPaths.push(htmlPath);
16444
16428
  }
16445
16429
  if (parsed.attachments && parsed.attachments.length > 0) {
16446
- const attDir = join5(emailDir, "attachments");
16430
+ const attDir = join4(emailDir, "attachments");
16447
16431
  mkdirSync4(attDir, { recursive: true });
16448
16432
  const usedFilenames = new Set;
16449
16433
  for (let i = 0;i < parsed.attachments.length; i++) {
@@ -16453,7 +16437,7 @@ function emailCommand() {
16453
16437
  filename = `${i}-${filename}`;
16454
16438
  }
16455
16439
  usedFilenames.add(filename);
16456
- const attPath = join5(attDir, filename);
16440
+ const attPath = join4(attDir, filename);
16457
16441
  const content = att.content;
16458
16442
  let buf;
16459
16443
  if (typeof content === "string") {
@@ -16494,25 +16478,312 @@ function emailCommand() {
16494
16478
  process.exit(1);
16495
16479
  }
16496
16480
  });
16481
+ cmd.command("send").description("Send an email from the agent").requiredOption("--agent_id <id>", "Agent ID").requiredOption("--to <addr>", "Recipient email address").requiredOption("--subject <s>", "Subject line").requiredOption("--body-file <path>", "Path to HTML body file").option("--attachment <path>", "Path to a file to attach (repeatable)", collectRepeated, []).option("--workspace <id>", "Workspace ID").action(async (opts, command) => {
16482
+ const { serverUrl, token, workspaceId } = resolveClientOpts(command, {
16483
+ workspace: opts.workspace,
16484
+ agentId: opts.agent_id
16485
+ });
16486
+ const client = new APIClient(serverUrl, token, workspaceId);
16487
+ let htmlBody;
16488
+ try {
16489
+ htmlBody = readFileSync3(opts.bodyFile, "utf-8");
16490
+ } catch (err) {
16491
+ console.error(`Error: cannot read body file "${opts.bodyFile}": ${err instanceof Error ? err.message : err}`);
16492
+ process.exit(1);
16493
+ }
16494
+ if (!htmlBody) {
16495
+ console.error(`Error: body file "${opts.bodyFile}" is empty`);
16496
+ process.exit(1);
16497
+ }
16498
+ const attachmentPaths = opts.attachment ?? [];
16499
+ const attachments = [];
16500
+ try {
16501
+ for (const path of attachmentPaths) {
16502
+ let bytes;
16503
+ let size;
16504
+ try {
16505
+ bytes = readFileSync3(path);
16506
+ size = statSync(path).size;
16507
+ } catch (err) {
16508
+ console.error(`Error: cannot read attachment "${path}": ${err instanceof Error ? err.message : err}`);
16509
+ process.exit(1);
16510
+ }
16511
+ const filename = basename(path);
16512
+ const contentType = guessContentType(filename);
16513
+ const form = new FormData;
16514
+ form.append("file", new Blob([new Uint8Array(bytes)], { type: contentType }), filename);
16515
+ const uploaded = await client.postMultipart("/api/email/upload", form);
16516
+ attachments.push({
16517
+ key: uploaded.key,
16518
+ filename: uploaded.filename,
16519
+ size: uploaded.size ?? size,
16520
+ contentType: uploaded.contentType ?? contentType
16521
+ });
16522
+ }
16523
+ const res = await client.postJSON("/api/email/send", {
16524
+ agentId: opts.agent_id,
16525
+ to: opts.to,
16526
+ subject: opts.subject,
16527
+ htmlBody,
16528
+ attachments
16529
+ });
16530
+ console.log(`Sent email to ${res.to_email} (id: ${res.id})`);
16531
+ } catch (err) {
16532
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
16533
+ process.exit(1);
16534
+ }
16535
+ });
16497
16536
  return cmd;
16498
16537
  }
16499
16538
 
16500
- // commands/version.ts
16539
+ // commands/calendar.ts
16501
16540
  import { Command as Command6 } from "commander";
16541
+ function resolveClientOpts2(command, agentId) {
16542
+ const parentOpts = command.parent?.parent?.opts() || {};
16543
+ const profile = parentOpts.profile;
16544
+ const cfg = loadCLIConfigForProfile(profile);
16545
+ const serverUrl = parentOpts.server || cfg.server_url;
16546
+ const workspaces = cfg.watched_workspaces || [];
16547
+ const ws = workspaces.find((w) => w.agent_ids?.includes(agentId));
16548
+ if (!ws || !ws.token) {
16549
+ console.error(`Error: no registered workspace contains agent ${agentId}. Run '${cmdPrefix()} register --token <token>' first.`);
16550
+ process.exit(1);
16551
+ }
16552
+ return { serverUrl, token: ws.token, workspaceId: ws.id };
16553
+ }
16554
+ function parseLocalDatetime(input) {
16555
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(input);
16556
+ if (!match) {
16557
+ throw new Error(`invalid --datetime "${input}" — expected YYYY-MM-DDTHH:MM`);
16558
+ }
16559
+ const [, y, mo, d, h, mi, s] = match;
16560
+ const date5 = new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), s ? Number(s) : 0);
16561
+ return date5.toISOString();
16562
+ }
16563
+ function formatLocalDatetime(iso) {
16564
+ const d = new Date(iso);
16565
+ const pad = (n) => String(n).padStart(2, "0");
16566
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
16567
+ }
16568
+ function printEventDetail(ev) {
16569
+ console.log(`id: ${ev.id}`);
16570
+ console.log(`agent_id: ${ev.agent_id}`);
16571
+ console.log(`title: ${ev.title}`);
16572
+ console.log(`scheduled_at: ${formatLocalDatetime(ev.scheduled_at)}`);
16573
+ if (ev.repeat_interval) {
16574
+ const until = ev.repeat_stop_at ? ` until ${formatLocalDatetime(ev.repeat_stop_at)}` : "";
16575
+ console.log(`repeat: every ${ev.repeat_interval}${until}`);
16576
+ } else {
16577
+ console.log(`repeat: (none)`);
16578
+ }
16579
+ console.log(`last_fired_at: ${ev.last_triggered_at ? formatLocalDatetime(ev.last_triggered_at) : "(never)"}`);
16580
+ console.log("description:");
16581
+ console.log(ev.description ?? "(no description)");
16582
+ }
16583
+ function calendarCommand() {
16584
+ const cmd = new Command6("calendar").description("Manage scheduled agent events");
16585
+ cmd.command("set").description("Create a calendar event").requiredOption("--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) => {
16586
+ const { serverUrl, token, workspaceId } = resolveClientOpts2(command, opts.agent_id);
16587
+ const client = new APIClient(serverUrl, token, workspaceId);
16588
+ let scheduledAt;
16589
+ try {
16590
+ scheduledAt = parseLocalDatetime(opts.datetime);
16591
+ } catch (err) {
16592
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
16593
+ process.exit(1);
16594
+ }
16595
+ if (opts.repeat_stop_date && !opts.repeat) {
16596
+ console.error("Error: --repeat_stop_date requires --repeat");
16597
+ process.exit(1);
16598
+ }
16599
+ if (opts.repeat_stop_date && !/^\d{4}-\d{2}-\d{2}$/.test(opts.repeat_stop_date)) {
16600
+ console.error("Error: --repeat_stop_date must be YYYY-MM-DD");
16601
+ process.exit(1);
16602
+ }
16603
+ const body = {
16604
+ agent_id: opts.agent_id,
16605
+ title: opts.event_title,
16606
+ scheduled_at: scheduledAt
16607
+ };
16608
+ if (opts.description)
16609
+ body.description = opts.description;
16610
+ if (opts.repeat)
16611
+ body.repeat_interval = opts.repeat;
16612
+ if (opts.repeat_stop_date)
16613
+ body.repeat_stop_date = opts.repeat_stop_date;
16614
+ try {
16615
+ const created = await client.postJSON("/api/calendar", body);
16616
+ if (opts.json) {
16617
+ printJSON(created);
16618
+ return;
16619
+ }
16620
+ console.log(`Created ${created.id} — ${created.title} @ ${formatLocalDatetime(created.scheduled_at)}${created.repeat_interval ? ` (every ${created.repeat_interval})` : ""}`);
16621
+ } catch (err) {
16622
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
16623
+ process.exit(1);
16624
+ }
16625
+ });
16626
+ cmd.command("list").description("List calendar events for an agent").requiredOption("--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) => {
16627
+ const { serverUrl, token, workspaceId } = resolveClientOpts2(command, opts.agent_id);
16628
+ const client = new APIClient(serverUrl, token, workspaceId);
16629
+ const now = Date.now();
16630
+ const from = new Date(now - Number(opts.past_days) * 86400000).toISOString();
16631
+ const to = new Date(now + Number(opts.future_days) * 86400000).toISOString();
16632
+ try {
16633
+ const events = await client.getJSON(`/api/calendar?agentId=${encodeURIComponent(opts.agent_id)}&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`);
16634
+ if (opts.json) {
16635
+ printJSON(events);
16636
+ return;
16637
+ }
16638
+ if (events.length === 0) {
16639
+ console.log("No calendar events.");
16640
+ return;
16641
+ }
16642
+ for (const ev of events) {
16643
+ const repeatBadge = ev.repeat_interval ? ` [every ${ev.repeat_interval}${ev.repeat_stop_at ? ` until ${formatLocalDatetime(ev.repeat_stop_at)}` : ""}]` : "";
16644
+ const descBadge = ev.description ? " [has description]" : "";
16645
+ console.log(`${ev.id} ${formatLocalDatetime(ev.scheduled_at)} ${ev.title}${repeatBadge}${descBadge}`);
16646
+ }
16647
+ } catch (err) {
16648
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
16649
+ process.exit(1);
16650
+ }
16651
+ });
16652
+ cmd.command("show").description("Show the full detail of a single calendar event").requiredOption("--agent_id <id>", "Agent ID").requiredOption("--event_id <id>", "Event ID").option("--json", "Output as JSON").action(async (opts, command) => {
16653
+ const { serverUrl, token, workspaceId } = resolveClientOpts2(command, opts.agent_id);
16654
+ const client = new APIClient(serverUrl, token, workspaceId);
16655
+ try {
16656
+ const ev = await client.getJSON(`/api/calendar/${opts.event_id}`);
16657
+ if (ev.agent_id !== opts.agent_id) {
16658
+ console.error(`Error: event ${ev.id} does not belong to agent ${opts.agent_id}`);
16659
+ process.exit(1);
16660
+ }
16661
+ if (opts.json) {
16662
+ printJSON(ev);
16663
+ return;
16664
+ }
16665
+ printEventDetail(ev);
16666
+ } catch (err) {
16667
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
16668
+ process.exit(1);
16669
+ }
16670
+ });
16671
+ cmd.command("update").description("Update fields on an existing calendar event").requiredOption("--agent_id <id>", "Agent ID").requiredOption("--event_id <id>", "Event ID").option("--event_title <title>", "New event title (task prompt)").option("--description <text>", "New description text").option("--clear_description", "Remove the description (sets to null)").option("--datetime <iso>", "New scheduled datetime (YYYY-MM-DDTHH:MM, local time)").option("--repeat <interval>", "New repeat interval, e.g. 1day, 2hour").option("--clear_repeat", "Convert a repeating event into a one-off").option("--repeat_stop_date <date>", "New stop date (YYYY-MM-DD, local time)").option("--clear_repeat_stop_date", "Remove the repeat stop date").option("--json", "Output as JSON").action(async (opts, command) => {
16672
+ if (opts.description && opts.clear_description) {
16673
+ console.error("Error: --description and --clear_description are mutually exclusive");
16674
+ process.exit(1);
16675
+ }
16676
+ if (opts.repeat && opts.clear_repeat) {
16677
+ console.error("Error: --repeat and --clear_repeat are mutually exclusive");
16678
+ process.exit(1);
16679
+ }
16680
+ if (opts.repeat_stop_date && opts.clear_repeat_stop_date) {
16681
+ console.error("Error: --repeat_stop_date and --clear_repeat_stop_date are mutually exclusive");
16682
+ process.exit(1);
16683
+ }
16684
+ const body = {};
16685
+ if (opts.event_title)
16686
+ body.title = opts.event_title;
16687
+ if (opts.description)
16688
+ body.description = opts.description;
16689
+ if (opts.clear_description)
16690
+ body.description = null;
16691
+ if (opts.datetime) {
16692
+ try {
16693
+ body.scheduled_at = parseLocalDatetime(opts.datetime);
16694
+ } catch (err) {
16695
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
16696
+ process.exit(1);
16697
+ }
16698
+ }
16699
+ if (opts.repeat)
16700
+ body.repeat_interval = opts.repeat;
16701
+ if (opts.clear_repeat)
16702
+ body.repeat_interval = null;
16703
+ if (opts.repeat_stop_date) {
16704
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.repeat_stop_date)) {
16705
+ console.error("Error: --repeat_stop_date must be YYYY-MM-DD");
16706
+ process.exit(1);
16707
+ }
16708
+ body.repeat_stop_date = opts.repeat_stop_date;
16709
+ }
16710
+ if (opts.clear_repeat_stop_date)
16711
+ body.repeat_stop_date = null;
16712
+ if (Object.keys(body).length === 0) {
16713
+ console.error("Error: no fields to update — pass at least one of --event_title, --description, --clear_description, --datetime, --repeat, --clear_repeat, --repeat_stop_date, --clear_repeat_stop_date");
16714
+ process.exit(1);
16715
+ }
16716
+ const { serverUrl, token, workspaceId } = resolveClientOpts2(command, opts.agent_id);
16717
+ const client = new APIClient(serverUrl, token, workspaceId);
16718
+ try {
16719
+ const updated = await client.patchJSON(`/api/calendar/${opts.event_id}`, body);
16720
+ if (updated.agent_id !== opts.agent_id) {
16721
+ console.error(`Error: event ${updated.id} does not belong to agent ${opts.agent_id}`);
16722
+ process.exit(1);
16723
+ }
16724
+ if (opts.json) {
16725
+ printJSON(updated);
16726
+ return;
16727
+ }
16728
+ printEventDetail(updated);
16729
+ } catch (err) {
16730
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
16731
+ process.exit(1);
16732
+ }
16733
+ });
16734
+ cmd.command("delete").description("Delete a calendar event").requiredOption("--agent_id <id>", "Agent ID").requiredOption("--event_id <id>", "Event ID").action(async (opts, command) => {
16735
+ const { serverUrl, token, workspaceId } = resolveClientOpts2(command, opts.agent_id);
16736
+ const client = new APIClient(serverUrl, token, workspaceId);
16737
+ try {
16738
+ await client.deleteJSON(`/api/calendar/${opts.event_id}`);
16739
+ console.log(`Deleted ${opts.event_id}`);
16740
+ } catch (err) {
16741
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
16742
+ process.exit(1);
16743
+ }
16744
+ });
16745
+ return cmd;
16746
+ }
16747
+
16748
+ // commands/version.ts
16749
+ import { Command as Command7 } from "commander";
16750
+
16751
+ // lib/version.ts
16752
+ import { readFileSync as readFileSync4 } from "fs";
16753
+ import { join as join5, dirname as dirname4 } from "path";
16754
+ import { fileURLToPath as fileURLToPath2 } from "url";
16755
+ function getCurrentVersion() {
16756
+ const __dirname2 = dirname4(fileURLToPath2(import.meta.url));
16757
+ const candidates = [
16758
+ join5(__dirname2, "..", "package.json"),
16759
+ join5(__dirname2, "..", "..", "package.json")
16760
+ ];
16761
+ for (const candidate of candidates) {
16762
+ try {
16763
+ const pkg = JSON.parse(readFileSync4(candidate, "utf-8"));
16764
+ if (typeof pkg.version === "string")
16765
+ return pkg.version;
16766
+ } catch {}
16767
+ }
16768
+ return "unknown";
16769
+ }
16770
+
16771
+ // commands/version.ts
16502
16772
  function versionCommand() {
16503
- const cmd = new Command6("version").description("Show CLI version").action(() => {
16773
+ const cmd = new Command7("version").description("Show CLI version").action(() => {
16504
16774
  console.log(`alook version ${getCurrentVersion()}`);
16505
16775
  });
16506
16776
  return cmd;
16507
16777
  }
16508
16778
 
16509
16779
  // src/index.ts
16510
- var program = new Command7;
16780
+ var program = new Command8;
16511
16781
  program.name("alook").description("Alook CLI").option("--server <url>", "Server URL").option("--profile <name>", "Profile name");
16512
16782
  program.addCommand(registerCommand());
16513
16783
  program.addCommand(statusCommand());
16514
16784
  program.addCommand(daemonCommand());
16515
16785
  program.addCommand(emailCommand());
16786
+ program.addCommand(calendarCommand());
16516
16787
  program.addCommand(configCommand());
16517
16788
  program.addCommand(versionCommand());
16518
16789
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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",