@alook/daemon 0.1.7 → 0.1.9

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/cli/index.js CHANGED
@@ -16635,6 +16635,7 @@ var communityBotBinding = sqliteTable("community_bot_binding", {
16635
16635
  userId: text("user_id").primaryKey().references(() => user.id, { onDelete: "cascade" }),
16636
16636
  machineId: text("machine_id").notNull().references(() => communityMachine.id, { onDelete: "restrict" }),
16637
16637
  runtime: text("runtime").notNull(),
16638
+ instruction: text("instruction").notNull().default(""),
16638
16639
  modelName: text("model_name"),
16639
16640
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
16640
16641
  }, (t) => [index("idx_community_bot_binding_machine").on(t.machineId)]);
@@ -16851,12 +16852,19 @@ import * as fs from "fs";
16851
16852
  import * as path from "path";
16852
16853
  // ../shared/src/constants/community.ts
16853
16854
  var MAX_PROFILE_NAME_LENGTH = 100;
16855
+ var MAX_PROFILE_ABOUT_LENGTH = 1000;
16854
16856
  var MAX_MESSAGE_CONTENT_LENGTH = 4000;
16855
16857
  var MAX_EMOJI_BYTES = 32;
16856
16858
  var MAX_ATTACHMENTS_PER_MESSAGE = 10;
16857
16859
  var MAX_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024;
16858
16860
  var MAX_ATTACHMENT_THUMBNAIL_SIZE_BYTES = 50 * 1024;
16859
16861
  var MAX_SERVER_ICON_SIZE_BYTES = 5 * 1024 * 1024;
16862
+ var ALLOWED_ICON_MIME_TYPES = [
16863
+ "image/png",
16864
+ "image/jpeg",
16865
+ "image/webp",
16866
+ "image/gif"
16867
+ ];
16860
16868
  var MAX_ICON_SOURCE_FILE_SIZE_BYTES = 15 * 1024 * 1024;
16861
16869
  // ../shared/src/utils/slug.ts
16862
16870
  init_nanoid();
@@ -17538,6 +17546,16 @@ var CommunityAgentAttachmentUploadResponseSchema = exports_external.object({
17538
17546
  var CommunityAgentAttachmentDownloadRequestSchema = exports_external.object({
17539
17547
  id: exports_external.string().min(1)
17540
17548
  });
17549
+ var CommunityAgentUpdateProfileRequestSchema = exports_external.object({
17550
+ bio: exports_external.string().max(MAX_PROFILE_ABOUT_LENGTH).optional(),
17551
+ avatar: exports_external.object({
17552
+ filename: exports_external.string().min(1),
17553
+ contentType: exports_external.enum(ALLOWED_ICON_MIME_TYPES),
17554
+ data: exports_external.instanceof(Uint8Array).refine((value) => value.byteLength > 0, "avatar must not be empty").refine((value) => value.byteLength <= MAX_SERVER_ICON_SIZE_BYTES, `avatar must be ≤ ${MAX_SERVER_ICON_SIZE_BYTES} bytes`)
17555
+ }).optional()
17556
+ }).refine((value) => value.bio !== undefined || value.avatar !== undefined, {
17557
+ message: "bio or avatar is required"
17558
+ });
17541
17559
  var CommunityAgentInboxPullRequestSchema = exports_external.object({
17542
17560
  max: exports_external.number().int().min(1).max(200).optional()
17543
17561
  });
@@ -18689,6 +18707,7 @@ function createProxyServerApi(config2) {
18689
18707
  }
18690
18708
  if (!res.ok) {
18691
18709
  const e = new Error(json2?.error ?? `proxy ${method} failed (${res.status})`);
18710
+ e.status = res.status;
18692
18711
  if (json2?.code !== undefined)
18693
18712
  e.code = json2.code;
18694
18713
  if (json2?.hint !== undefined)
@@ -18697,6 +18716,24 @@ function createProxyServerApi(config2) {
18697
18716
  }
18698
18717
  return json2;
18699
18718
  }
18719
+ function isTransientProfileStepError(err) {
18720
+ const message2 = err instanceof Error ? err.message : String(err);
18721
+ const status = typeof err === "object" && err !== null && "status" in err ? err.status : undefined;
18722
+ return typeof status === "number" && status >= 500 && status <= 599 || /upstream returned 5\d\d/.test(message2) || message2.includes("upstream body read failed") || message2.includes("fetch failed") || message2.includes("ECONNRESET") || message2.includes("ETIMEDOUT") || message2.includes("socket hang up") || message2.includes("network");
18723
+ }
18724
+ async function withProfileStepRetry(step) {
18725
+ const maxAttempts = 4;
18726
+ for (let attempt = 0;attempt < maxAttempts; attempt += 1) {
18727
+ try {
18728
+ return await step();
18729
+ } catch (err) {
18730
+ if (!isTransientProfileStepError(err) || attempt === maxAttempts - 1)
18731
+ throw err;
18732
+ await new Promise((resolve) => setTimeout(resolve, Math.min(2000, 150 * 2 ** attempt)));
18733
+ }
18734
+ }
18735
+ throw new Error("profile step retry exhausted");
18736
+ }
18700
18737
  async function callUpload(req) {
18701
18738
  const form = new FormData;
18702
18739
  const blobType = req.file.contentType ?? "application/octet-stream";
@@ -18937,6 +18974,57 @@ function createProxyServerApi(config2) {
18937
18974
  });
18938
18975
  return parseJsonResponse(res, "nap");
18939
18976
  }
18977
+ async function callUpdateProfile(req) {
18978
+ const parsed = CommunityAgentUpdateProfileRequestSchema.safeParse(req);
18979
+ if (!parsed.success) {
18980
+ throw new Error(parsed.error.issues[0]?.message ?? "invalid profile update");
18981
+ }
18982
+ const updated = [];
18983
+ let avatarUrl;
18984
+ if (req.avatar) {
18985
+ const body = await withProfileStepRetry(async () => {
18986
+ const form = new FormData;
18987
+ const blob2 = new Blob([new Uint8Array(req.avatar.data)], { type: req.avatar.contentType });
18988
+ form.append("file", blob2, req.avatar.filename);
18989
+ const res = await fetchImpl(`${base}/api/community/users/me/avatar`, {
18990
+ method: "POST",
18991
+ headers: { authorization: `Bearer ${config2.voucher}` },
18992
+ body: form
18993
+ });
18994
+ return parseJsonResponse(res, "updateProfile avatar");
18995
+ });
18996
+ avatarUrl = body.url;
18997
+ updated.push("avatar");
18998
+ }
18999
+ let bio;
19000
+ if (req.bio !== undefined) {
19001
+ try {
19002
+ const body = await withProfileStepRetry(async () => {
19003
+ const res = await fetchImpl(`${base}/api/community/users/me/profile`, {
19004
+ method: "PATCH",
19005
+ headers: {
19006
+ "content-type": "application/json",
19007
+ authorization: `Bearer ${config2.voucher}`
19008
+ },
19009
+ body: JSON.stringify({ aboutMe: req.bio })
19010
+ });
19011
+ return parseJsonResponse(res, "updateProfile bio");
19012
+ });
19013
+ bio = body.aboutMe;
19014
+ updated.push("bio");
19015
+ } catch (err) {
19016
+ if (avatarUrl !== undefined && err instanceof Error) {
19017
+ err.hint = "avatar was applied; bio was not applied";
19018
+ }
19019
+ throw err;
19020
+ }
19021
+ }
19022
+ return {
19023
+ updated,
19024
+ ...bio !== undefined ? { bio } : {},
19025
+ ...avatarUrl !== undefined ? { avatarUrl } : {}
19026
+ };
19027
+ }
18940
19028
  async function callInboxSnapshot() {
18941
19029
  const res = await fetchImpl(`${base}/api/community/users/me/inbox/snapshot`, {
18942
19030
  method: "GET",
@@ -19014,6 +19102,7 @@ function createProxyServerApi(config2) {
19014
19102
  listMembers: callListMembers,
19015
19103
  attachmentUpload: callUpload,
19016
19104
  attachmentDownload: callDownload,
19105
+ updateProfile: callUpdateProfile,
19017
19106
  reactAdd: callReactAdd,
19018
19107
  markSet: callMarkSet,
19019
19108
  markRemove: callMarkRemove,
@@ -19098,6 +19187,10 @@ function cliCommandsSection() {
19098
19187
  `1. \`${CLI} friend request --username "<name#0042>"\` — ask to friend a user by handle. ` + `Your owner must approve it in DM before it goes through, so expect a \`pending\` ` + `result with a hint (a same-owner sibling bot auto-accepts instead).`,
19099
19188
  `2. \`${CLI} friend list\` — list your friends and pending requests ` + `(\`accepted\`, \`pendingOutgoing\`, \`pendingIncoming\`).`,
19100
19189
  "",
19190
+ "### Settings",
19191
+ "",
19192
+ `1. \`${CLI} setting profile --set-bio <text> --set-avatar <path>\` — update your public bio ` + `and/or avatar. At least one flag is required; pass \`--set-bio ""\` to clear the bio.`,
19193
+ "",
19101
19194
  "### Context Lifecycle",
19102
19195
  "",
19103
19196
  `1. \`${CLI} nap --handoff <file>\` (or \`--text <note>\`) — reset your current session and ` + `start fresh. The required handoff is injected into the new session so your future self can ` + `quickly pick up unfinished work. Never nap on your own; only do it when someone explicitly asks.`,
@@ -22010,6 +22103,8 @@ function parseBearer(authHeader) {
22010
22103
  return m ? m[1].trim() : null;
22011
22104
  }
22012
22105
  var DEFAULT_CAPABILITY_RESOLVER = (method, pathname) => {
22106
+ if ((method === "GET" || method === "PATCH") && pathname === "/api/community/users/me/profile" || method === "POST" && pathname === "/api/community/users/me/avatar")
22107
+ return "profile";
22013
22108
  if (pathname.includes("/attachment"))
22014
22109
  return "attach";
22015
22110
  if (/\/friends(\/|$|\?)/.test(pathname))
@@ -25184,6 +25279,10 @@ function deriveAuditLogSubcommand(pathname, method) {
25184
25279
  return "inboxSnapshot";
25185
25280
  if (/^\/api\/community\/users\/me\/inbox\/ack(\/|$|\?)/.test(canonical))
25186
25281
  return null;
25282
+ if (method === "PATCH" && canonical === "/api/community/users/me/profile")
25283
+ return "profileBioUpdate";
25284
+ if (method === "POST" && canonical === "/api/community/users/me/avatar")
25285
+ return "profileAvatarUpdate";
25187
25286
  if (/^\/api\/community\/bots\/me\/nap(\/|$|\?)/.test(canonical))
25188
25287
  return "nap";
25189
25288
  return null;
@@ -27082,7 +27181,7 @@ async function daemonReplace(opts) {
27082
27181
  }
27083
27182
 
27084
27183
  // src/cli/daemonRunner.ts
27085
- var CAPABILITIES = ["send", "read", "mentions", "tasks", "reactions", "server", "channels", "knowledge", "attach", "friend"];
27184
+ var CAPABILITIES = ["send", "read", "mentions", "tasks", "reactions", "server", "channels", "knowledge", "attach", "friend", "profile"];
27086
27185
  var DAEMON_LOG_MAX_BYTES = 8 * 1024 * 1024;
27087
27186
  var DAEMON_ERROR_MESSAGE_MAX_CHARS = 512;
27088
27187
  var DIAGNOSTIC_MAX_LINE_BYTES = 128 * 1024;
@@ -28057,6 +28156,17 @@ async function daemonResume(opts) {
28057
28156
  resumeRequestId: opts.requestId
28058
28157
  });
28059
28158
  }
28159
+ async function daemonStartById(opts) {
28160
+ const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
28161
+ const record2 = readDaemonLaunchRecord(baseDir, opts.id);
28162
+ await daemonStart({
28163
+ machineKey: record2.credential,
28164
+ serverUrl: record2.serverUrl,
28165
+ wsUrl: record2.wsUrl,
28166
+ baseDir,
28167
+ foreground: opts.foreground
28168
+ });
28169
+ }
28060
28170
  function runnerArguments() {
28061
28171
  const command = process.env.ALOOK_DAEMON_PACKAGE_WRAPPER === "1" ? ["run"] : ["daemon", "run"];
28062
28172
  return [...process.execArgv, process.argv[1], ...command];
@@ -28426,7 +28536,8 @@ function contentTypeFromFilename(filename) {
28426
28536
  }
28427
28537
  function isTransientMutationError(err) {
28428
28538
  const msg = err instanceof Error ? err.message : String(err);
28429
- return /upstream returned 5\d\d/.test(msg) || msg.includes("upstream body read failed") || msg.includes("fetch failed") || msg.includes("ECONNRESET") || msg.includes("ETIMEDOUT") || msg.includes("socket hang up") || msg.includes("network");
28539
+ const status = typeof err === "object" && err !== null && "status" in err ? err.status : undefined;
28540
+ return typeof status === "number" && status >= 500 && status <= 599 || /upstream returned 5\d\d/.test(msg) || msg.includes("upstream body read failed") || msg.includes("fetch failed") || msg.includes("ECONNRESET") || msg.includes("ETIMEDOUT") || msg.includes("socket hang up") || msg.includes("network");
28430
28541
  }
28431
28542
  async function withTransientMutationRetry(mutation) {
28432
28543
  const MAX_ATTEMPTS = 4;
@@ -28690,6 +28801,43 @@ async function cmdAttachmentDownload(opts) {
28690
28801
  }
28691
28802
  return result;
28692
28803
  }
28804
+ async function cmdSettingProfile(opts) {
28805
+ const bio = opts.setBio;
28806
+ const avatarPath = opts.setAvatar;
28807
+ if (bio === undefined && avatarPath === undefined) {
28808
+ throw new CliError("setting profile: --set-bio <text> or --set-avatar <path> is required");
28809
+ }
28810
+ if (bio !== undefined && bio.length > MAX_PROFILE_ABOUT_LENGTH) {
28811
+ throw new CliError(`setting profile: bio must be ≤ ${MAX_PROFILE_ABOUT_LENGTH} characters`);
28812
+ }
28813
+ let avatar;
28814
+ if (avatarPath !== undefined) {
28815
+ const fs13 = await import("fs/promises");
28816
+ let bytes;
28817
+ try {
28818
+ bytes = await fs13.readFile(avatarPath);
28819
+ } catch (err) {
28820
+ throw new CliError(`setting profile: cannot read avatar: ${err.message}`);
28821
+ }
28822
+ if (bytes.byteLength === 0)
28823
+ throw new CliError("setting profile: avatar file is empty");
28824
+ if (bytes.byteLength > MAX_SERVER_ICON_SIZE_BYTES) {
28825
+ throw new CliError(`setting profile: avatar too large — ${bytes.byteLength} bytes, max ${MAX_SERVER_ICON_SIZE_BYTES}`);
28826
+ }
28827
+ const pathMod = await import("path");
28828
+ const filename = pathMod.basename(avatarPath);
28829
+ const contentType = contentTypeFromFilename(filename);
28830
+ if (!ALLOWED_ICON_MIME_TYPES.includes(contentType)) {
28831
+ throw new CliError("setting profile: avatar must be png / jpeg / webp / gif");
28832
+ }
28833
+ avatar = { filename, contentType, data: new Uint8Array(bytes) };
28834
+ }
28835
+ const api2 = getApi();
28836
+ return api2.updateProfile({
28837
+ ...bio !== undefined ? { bio } : {},
28838
+ ...avatar ? { avatar } : {}
28839
+ });
28840
+ }
28693
28841
  async function cmdInboxPull(opts) {
28694
28842
  const api2 = getApi();
28695
28843
  const agent2 = agentId(opts);
@@ -28943,6 +29091,12 @@ function buildProgram() {
28943
29091
  const result = await cmdFriendList({ ...globalOpts, ...localOpts });
28944
29092
  printEnvelope({ success: result });
28945
29093
  });
29094
+ const setting = program.command("setting").description("account settings").exitOverride();
29095
+ setting.configureOutput({ writeOut: () => {}, writeErr: () => {} });
29096
+ setting.command("profile").description("update your public bio and/or avatar").option("--set-bio <text>", "set public bio; pass an empty string to clear it").option("--set-avatar <path>", "upload a png, jpeg, webp, or gif avatar").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
29097
+ const result = await cmdSettingProfile({ ...program.opts(), ...this.opts() });
29098
+ printEnvelope({ success: result });
29099
+ });
28946
29100
  program.command("nap").description("end your session and start fresh, carrying a handoff to your reborn self (read the nap rule first)").option("--handoff <file>", "path to your handoff note (your note to your reborn self)").option("--text <note>", "inline handoff note (alternative to --handoff)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
28947
29101
  const localOpts = this.opts();
28948
29102
  const globalOpts = program.opts();
@@ -28951,10 +29105,23 @@ function buildProgram() {
28951
29105
  });
28952
29106
  const daemon = program.command("daemon").description("daemon operations").exitOverride();
28953
29107
  daemon.configureOutput({ writeOut: () => {}, writeErr: () => {} });
28954
- daemon.command("start").description("start the daemon (connects to server, manages agent lifecycles)").requiredOption("--machine-key <key>", "machine key for server authentication").option("--server-url <url>", "server HTTP URL (or ALOOK_SERVER_URL env)").option("--ws-url <url>", "server WebSocket URL (or ALOOK_SERVER_WS_URL env)").option("--base-dir <path>", "data directory for agent workspaces and pidfile (or ALOOK_DATA_DIR env)").option("--foreground", "run in the current process and tee daemon logs to the terminal").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
29108
+ daemon.command("start").description("start the daemon (connects to server, manages agent lifecycles)").option("--machine-key <key>", "machine key for first-time pairing").option("--id <machineId>", "restart a previously paired machine by id").option("--server-url <url>", "server HTTP URL (or ALOOK_SERVER_URL env)").option("--ws-url <url>", "server WebSocket URL (or ALOOK_SERVER_WS_URL env)").option("--base-dir <path>", "data directory for agent workspaces and pidfile (or ALOOK_DATA_DIR env)").option("--foreground", "run in the current process and tee daemon logs to the terminal").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
28955
29109
  const localOpts = this.opts();
29110
+ const machineKey = localOpts.machineKey;
29111
+ const id = localOpts.id;
29112
+ if (!machineKey && !id || machineKey && id) {
29113
+ throw new CliError("daemon start requires exactly one of --machine-key <key> or --id <machineId>");
29114
+ }
29115
+ if (id) {
29116
+ await daemonStartById({
29117
+ id,
29118
+ baseDir: localOpts.baseDir,
29119
+ foreground: localOpts.foreground === true
29120
+ });
29121
+ return;
29122
+ }
28956
29123
  await daemonStart({
28957
- machineKey: localOpts.machineKey,
29124
+ machineKey,
28958
29125
  serverUrl: localOpts.serverUrl,
28959
29126
  wsUrl: localOpts.wsUrl,
28960
29127
  baseDir: localOpts.baseDir,
@@ -28987,9 +29154,13 @@ function buildProgram() {
28987
29154
  baseDir: localOpts.baseDir
28988
29155
  });
28989
29156
  });
28990
- daemon.command("list").description("list running daemons on this machine").option("--base-dir <path>", "data directory (or ALOOK_DATA_DIR env)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(function() {
29157
+ daemon.command("list").description("list running daemons on this machine").option("--base-dir <path>", "data directory (or ALOOK_DATA_DIR env)").option("--json", "print a machine-readable JSON envelope").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(function() {
28991
29158
  const localOpts = this.opts();
28992
29159
  const daemons = daemonList({ baseDir: localOpts.baseDir });
29160
+ if (localOpts.json === true) {
29161
+ printEnvelope({ success: { daemons } });
29162
+ return;
29163
+ }
28993
29164
  process.stdout.write(renderDaemonList(daemons) + `
28994
29165
  `);
28995
29166
  });
package/dist/index.js CHANGED
@@ -82,6 +82,10 @@ function cliCommandsSection() {
82
82
  `1. \`${CLI} friend request --username "<name#0042>"\` — ask to friend a user by handle. ` + `Your owner must approve it in DM before it goes through, so expect a \`pending\` ` + `result with a hint (a same-owner sibling bot auto-accepts instead).`,
83
83
  `2. \`${CLI} friend list\` — list your friends and pending requests ` + `(\`accepted\`, \`pendingOutgoing\`, \`pendingIncoming\`).`,
84
84
  "",
85
+ "### Settings",
86
+ "",
87
+ `1. \`${CLI} setting profile --set-bio <text> --set-avatar <path>\` — update your public bio ` + `and/or avatar. At least one flag is required; pass \`--set-bio ""\` to clear the bio.`,
88
+ "",
85
89
  "### Context Lifecycle",
86
90
  "",
87
91
  `1. \`${CLI} nap --handoff <file>\` (or \`--text <note>\`) — reset your current session and ` + `start fresh. The required handoff is injected into the new session so your future self can ` + `quickly pick up unfinished work. Never nap on your own; only do it when someone explicitly asks.`,
@@ -21638,6 +21642,7 @@ var communityBotBinding = sqliteTable("community_bot_binding", {
21638
21642
  userId: text("user_id").primaryKey().references(() => user.id, { onDelete: "cascade" }),
21639
21643
  machineId: text("machine_id").notNull().references(() => communityMachine.id, { onDelete: "restrict" }),
21640
21644
  runtime: text("runtime").notNull(),
21645
+ instruction: text("instruction").notNull().default(""),
21641
21646
  modelName: text("model_name"),
21642
21647
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
21643
21648
  }, (t) => [index("idx_community_bot_binding_machine").on(t.machineId)]);
@@ -21947,6 +21952,8 @@ function parseBearer(authHeader) {
21947
21952
  return m ? m[1].trim() : null;
21948
21953
  }
21949
21954
  var DEFAULT_CAPABILITY_RESOLVER = (method, pathname) => {
21955
+ if ((method === "GET" || method === "PATCH") && pathname === "/api/community/users/me/profile" || method === "POST" && pathname === "/api/community/users/me/avatar")
21956
+ return "profile";
21950
21957
  if (pathname.includes("/attachment"))
21951
21958
  return "attach";
21952
21959
  if (/\/friends(\/|$|\?)/.test(pathname))
@@ -23689,6 +23696,10 @@ function deriveAuditLogSubcommand(pathname, method) {
23689
23696
  return "inboxSnapshot";
23690
23697
  if (/^\/api\/community\/users\/me\/inbox\/ack(\/|$|\?)/.test(canonical))
23691
23698
  return null;
23699
+ if (method === "PATCH" && canonical === "/api/community/users/me/profile")
23700
+ return "profileBioUpdate";
23701
+ if (method === "POST" && canonical === "/api/community/users/me/avatar")
23702
+ return "profileAvatarUpdate";
23692
23703
  if (/^\/api\/community\/bots\/me\/nap(\/|$|\?)/.test(canonical))
23693
23704
  return "nap";
23694
23705
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/daemon",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Alook agent daemon — host-side runtime backend, process manager, credential proxy, and control plane.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/alookai/alook#readme",