@alook/daemon 0.1.11 → 0.1.12

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
@@ -14945,6 +14945,9 @@ class Name {
14945
14945
  return new SQL([this]);
14946
14946
  }
14947
14947
  }
14948
+ function isDriverValueEncoder(value) {
14949
+ return typeof value === "object" && value !== null && "mapToDriverValue" in value && typeof value.mapToDriverValue === "function";
14950
+ }
14948
14951
  var noopDecoder = {
14949
14952
  mapFromDriverValue: (value) => value
14950
14953
  };
@@ -15838,6 +15841,39 @@ class PrimaryKey {
15838
15841
  }
15839
15842
  }
15840
15843
 
15844
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@4.20260702.1_@opentelemetry+api@1.9.1_@typ_5aa9492b131e5fac58fa98e88e5c0ad3/node_modules/drizzle-orm/sql/expressions/conditions.js
15845
+ function bindIfParam(value, column) {
15846
+ if (isDriverValueEncoder(column) && !isSQLWrapper(value) && !is(value, Param) && !is(value, Placeholder) && !is(value, Column) && !is(value, Table) && !is(value, View)) {
15847
+ return new Param(value, column);
15848
+ }
15849
+ return value;
15850
+ }
15851
+ var eq = (left, right) => {
15852
+ return sql`${left} = ${bindIfParam(right, left)}`;
15853
+ };
15854
+ function and(...unfilteredConditions) {
15855
+ const conditions = unfilteredConditions.filter((c) => c !== undefined);
15856
+ if (conditions.length === 0) {
15857
+ return;
15858
+ }
15859
+ if (conditions.length === 1) {
15860
+ return new SQL(conditions);
15861
+ }
15862
+ return new SQL([
15863
+ new StringChunk("("),
15864
+ sql.join(conditions, new StringChunk(" and ")),
15865
+ new StringChunk(")")
15866
+ ]);
15867
+ }
15868
+ function isNotNull(value) {
15869
+ return sql`${value} is not null`;
15870
+ }
15871
+
15872
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@4.20260702.1_@opentelemetry+api@1.9.1_@typ_5aa9492b131e5fac58fa98e88e5c0ad3/node_modules/drizzle-orm/sql/expressions/select.js
15873
+ function desc(column) {
15874
+ return sql`${column} desc`;
15875
+ }
15876
+
15841
15877
  // ../shared/src/db/community-machine-schema.ts
15842
15878
  init_nanoid();
15843
15879
 
@@ -17576,13 +17612,6 @@ var CommunityAgentResolveRequestSchema = exports_external.object({
17576
17612
  var CommunityAgentListChannelsRequestSchema = exports_external.object({
17577
17613
  server: exports_external.string().min(1).optional()
17578
17614
  });
17579
- var CommunityAgentCreatePostRequestSchema = exports_external.object({
17580
- forum: exports_external.string().min(1),
17581
- title: exports_external.string().min(1),
17582
- content: CommunityAgentMessageContentSchema,
17583
- attachments: exports_external.array(exports_external.string().min(1)).max(MAX_ATTACHMENTS_PER_MESSAGE).default([]),
17584
- nonce: exports_external.string().min(1).max(128).optional()
17585
- }).refine((d) => d.content.text.trim().length > 0 || d.attachments.length > 0, { message: "post must have text or attachments" });
17586
17615
  var CommunityAgentListMembersRequestSchema = exports_external.object({
17587
17616
  server: exports_external.string().min(1),
17588
17617
  limit: exports_external.number().int().positive().optional(),
@@ -17596,7 +17625,9 @@ var CommunityAgentJoinServerRequestSchema = exports_external.object({
17596
17625
  invite: exports_external.string().min(1)
17597
17626
  });
17598
17627
  var CommunityAgentNapRequestSchema = exports_external.object({
17599
- handoff: exports_external.string().trim().min(1)
17628
+ handoff: exports_external.string().refine((value) => value.trim().length > 0, {
17629
+ message: "handoff is required"
17630
+ })
17600
17631
  });
17601
17632
  var CommunityAgentReactAddRequestSchema = exports_external.object({
17602
17633
  channel: exports_external.string().min(1),
@@ -17748,6 +17779,7 @@ var communityChannel = sqliteTable("community_channel", {
17748
17779
  index("idx_channel_server_position").on(t.serverId, t.position),
17749
17780
  index("idx_channel_server_last_message").on(t.serverId, t.lastMessageAt),
17750
17781
  index("idx_channel_parent").on(t.parentChannelId),
17782
+ index("idx_channel_forum_created").on(t.parentChannelId, desc(t.createdAt), desc(t.id)).where(and(eq(t.type, "thread"), eq(t.archived, 0), isNotNull(t.parentMessageId))),
17751
17783
  uniqueIndex("idx_channel_server_name").on(t.serverId, t.name).where(sql`parent_channel_id IS NULL`)
17752
17784
  ]);
17753
17785
  var communityChannelMember = sqliteTable("community_channel_member", {
@@ -18769,50 +18801,6 @@ function createProxyServerApi(config2) {
18769
18801
  });
18770
18802
  return parseJsonResponse(res, "send");
18771
18803
  }
18772
- async function callCreatePost(req) {
18773
- const endpoint = `${base}/api/community/channels/${REF_PLACEHOLDER_ID}/messages`;
18774
- const openerNonce = req.nonce !== undefined ? `${req.nonce}:opener` : undefined;
18775
- const replyNonce = req.nonce !== undefined ? `${req.nonce}:reply` : undefined;
18776
- const openerRes = await fetchImpl(endpoint, {
18777
- method: "POST",
18778
- headers: {
18779
- "content-type": "application/json",
18780
- authorization: `Bearer ${config2.voucher}`
18781
- },
18782
- body: JSON.stringify({
18783
- channel: req.forum,
18784
- content: { text: req.title },
18785
- attachments: req.attachments ?? [],
18786
- ...openerNonce !== undefined ? { nonce: openerNonce } : {}
18787
- })
18788
- });
18789
- const opener = await parseJsonResponse(openerRes, "createPost opener");
18790
- if (opener.state !== "sent" || !opener.message || !opener.threadId) {
18791
- throw new Error("createPost: upstream response missing opener thread");
18792
- }
18793
- const threadRef = `${req.forum}/#${opener.message.seq.replace(/^#/, "")}`;
18794
- const replyRes = await fetchImpl(endpoint, {
18795
- method: "POST",
18796
- headers: {
18797
- "content-type": "application/json",
18798
- authorization: `Bearer ${config2.voucher}`
18799
- },
18800
- body: JSON.stringify({
18801
- channel: threadRef,
18802
- content: req.content,
18803
- attachments: req.attachments ?? [],
18804
- ...replyNonce !== undefined ? { nonce: replyNonce } : {}
18805
- })
18806
- });
18807
- const reply = await parseJsonResponse(replyRes, "createPost reply");
18808
- if (reply.state !== "sent" || !reply.message)
18809
- throw new Error("createPost: upstream response missing reply");
18810
- return {
18811
- ref: reply.message.channel,
18812
- name: req.title,
18813
- seq: Number(reply.message.seq.replace(/^#/, ""))
18814
- };
18815
- }
18816
18804
  async function callRead(req) {
18817
18805
  const q = new URLSearchParams;
18818
18806
  q.set("ref", req.channel);
@@ -19096,7 +19084,6 @@ function createProxyServerApi(config2) {
19096
19084
  inboxSnapshot: (_r) => callInboxSnapshot(),
19097
19085
  ack: callAck,
19098
19086
  send: callSend,
19099
- createPost: callCreatePost,
19100
19087
  read: callRead,
19101
19088
  resolve: callResolve,
19102
19089
  listMembers: callListMembers,
@@ -19162,7 +19149,7 @@ function cliCommandsSection() {
19162
19149
  "### Messaging",
19163
19150
  "",
19164
19151
  `1. \`${CLI} inbox pull\` — fetch unread messages (advances your read waterline by default, ` + `so they won't re-pull; \`--no-ack\` to peek without advancing).`,
19165
- `2. \`${CLI} message send\` — send to a channel, DM, or thread. Attach with ` + `\`--attachment <id>\` (repeatable, order matters). Optionally add ` + `\`--remind-after <duration>\` to be reminded if the same channel, thread, or DM receives no ` + `newer message after your send. Use a whole number of minutes or hours from \`1m\` to \`24h\`, ` + `such as \`15m\` or \`2h\`.`,
19152
+ `2. \`${CLI} message send\` — send to a channel, DM, or thread. ` + `For a short body, use explicit \`--stdin\` with a quoted heredoc; for a long or complicated body, ` + `use \`--file <path>\`. Attach with \`--attachment <id>\` (repeatable, order matters). Optionally add ` + `\`--remind-after <duration>\` to be reminded if the same channel, thread, or DM receives no ` + `newer message after your send. Use a whole number of minutes or hours from \`1m\` to \`24h\`, ` + `such as \`15m\` or \`2h\`.`,
19166
19153
  `3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted. Feed it into ` + `\`message send --attachment <id>\`.`,
19167
19154
  `4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
19168
19155
  `5. \`${CLI} message emoji --target <ref> --emoji <e>\` — react with a single emoji. ` + `Works on channel messages (\`/<server>/<channel>#N\`), DM messages ` + `(\`/.dm/<peer>#N\`), and thread-reply messages (\`/<server>/<channel>/#N#M\`).`,
@@ -19193,7 +19180,7 @@ function cliCommandsSection() {
19193
19180
  "",
19194
19181
  "### Context Lifecycle",
19195
19182
  "",
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.`,
19183
+ `1. \`${CLI} nap --handoff <file>\` — 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.`,
19197
19184
  "",
19198
19185
  "### Output format",
19199
19186
  "",
@@ -19212,9 +19199,11 @@ function messagingSection() {
19212
19199
  "You can initiate conversations — send to any channel or DM someone directly. You're not " + "limited to replying. Use the same `message send` command whether you're replying or " + "starting a conversation.",
19213
19200
  "",
19214
19201
  "- Reply where the message came from. Post results in the channel that owns the topic. " + "When uncertain, read history (below) or DM the relevant people.",
19215
- `- Short reply: \`${CLI} message send --target <ref> --text "brief reply"\`.`,
19216
- `- Long or complicated: write body to a tmp file, then \`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
19217
- `- Cite a specific message: \`${CLI} message send --target <ref> --reply "#37" --text "on it"\` ` + "`--reply` takes the `#N` seq (within `--target`) of the message you're answering.",
19202
+ "Free-form message bodies never go in command arguments.",
19203
+ "",
19204
+ `- Short reply: use \`${CLI} message send --target <ref> --stdin\` with the quoted-heredoc form shown ` + "under *Message formatting*. Choose a fresh quoted delimiter that does not occur as a standalone line in the body.",
19205
+ `- Long or complicated: write the body to a temporary file with a filesystem tool, then ` + `\`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
19206
+ `- Cite a specific message: add \`--reply "#37"\` to either form — \`--reply\` takes the \`#N\` seq ` + "(within `--target`) of the message you're answering.",
19218
19207
  "",
19219
19208
  "### Context refs",
19220
19209
  "",
@@ -19254,8 +19243,10 @@ function messagingSection() {
19254
19243
  "- **Mentions** — `@name#NNNN` calls that person's attention specifically. In a private " + "channel, first verify they " + `are a member with \`${CLI} channel member --channel <ref>\` before mentioning them.`,
19255
19244
  "",
19256
19245
  "```bash",
19257
- "# Notify Alice and point her to a message for context.",
19258
- `${CLI} message send --target "/demo#1234/general" --text "@alice#0001 Please review /demo#1234/general#42"`,
19246
+ "# Choose a fresh quoted delimiter that does not occur as a standalone line in the body.",
19247
+ `${CLI} message send --target "/demo#1234/general" --stdin <<'ALOOK_MESSAGE_7F3C'`,
19248
+ "@alice#0001 Please review /demo#1234/general#42",
19249
+ "ALOOK_MESSAGE_7F3C",
19259
19250
  "```",
19260
19251
  "",
19261
19252
  "### Pulled messages",
@@ -19321,7 +19312,7 @@ function utilsSection() {
19321
19312
  "",
19322
19313
  `Use \`message send --remind-after <duration>\` when you send something that may need a later ` + "follow-up — for example, a question, approval request, handoff, or blocker — and silence would " + "leave the work unfinished. If no newer message appears in that channel, thread, or DM during " + "the duration, you'll receive a reminder to return and decide what to do next. Don't add it to " + "ordinary messages that need no follow-up.",
19323
19314
  "",
19324
- `Example: \`${CLI} message send --target /demo#1234/team --text "Please review the rollout plan" ` + `--remind-after 1m\`.`
19315
+ `Example: ${CLI} message send --target "/demo#1234/team" --remind-after 1m --file ./message.md`
19325
19316
  ].join(`
19326
19317
  `);
19327
19318
  }
@@ -24031,7 +24022,7 @@ var MODEL_SWITCH_REWAKE_PROMPT = "You were just switched to a different model. C
24031
24022
  function buildNapRewakePrompt(handoff) {
24032
24023
  return "You took a nap: you reset your own session, so prior conversation context " + `is gone. Before sleeping you left yourself this handoff —
24033
24024
 
24034
- ` + handoff.trim() + `
24025
+ ` + handoff + `
24035
24026
 
24036
24027
  Then read @memory.md and your .context_timeline for durable context, and pull ` + "your inbox before doing anything else. If it reports marked messages, run `$ALOOK_CLI " + "message mark list` and resume that outstanding work.";
24037
24028
  }
@@ -25611,7 +25602,10 @@ async function createDaemon(opts) {
25611
25602
  let statusTimer = null;
25612
25603
  if (opts.statusFilePath) {
25613
25604
  const statusPath = opts.statusFilePath;
25614
- const writeStatus = () => writeStatusFile(statusPath, { writtenAt: Date.now(), agents: manager.statusProjection(Date.now()) });
25605
+ const writeStatus = () => {
25606
+ const nowMs = Date.now();
25607
+ writeStatusFile(statusPath, { writtenAt: nowMs, agents: manager.statusProjection(nowMs) });
25608
+ };
25615
25609
  writeStatus();
25616
25610
  statusTimer = setInterval(writeStatus, STATUS_WRITE_INTERVAL_MS);
25617
25611
  statusTimer.unref?.();
@@ -27856,6 +27850,22 @@ function reconcileLegacyMachineKeyOwnership(baseDir, machineKey) {
27856
27850
  removePidFileIfMatches(candidate, current);
27857
27851
  }
27858
27852
  }
27853
+ function daemonLastActiveMs(snapshot, nowMs) {
27854
+ const writtenAt = snapshot.writtenAt;
27855
+ if (writtenAt == null || !Number.isFinite(writtenAt) || writtenAt < 0)
27856
+ return null;
27857
+ let latest = null;
27858
+ for (const agent2 of snapshot.agents) {
27859
+ const sinceProgressMs = agent2.sinceProgressMs;
27860
+ if (!Number.isFinite(sinceProgressMs) || sinceProgressMs < 0)
27861
+ continue;
27862
+ const progressAt = writtenAt - sinceProgressMs;
27863
+ if (progressAt <= 0 || progressAt > nowMs)
27864
+ continue;
27865
+ latest = latest == null ? progressAt : Math.max(latest, progressAt);
27866
+ }
27867
+ return latest;
27868
+ }
27859
27869
  function daemonList(opts) {
27860
27870
  const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
27861
27871
  const dir = daemonsDir(baseDir);
@@ -27879,7 +27889,7 @@ function daemonList(opts) {
27879
27889
  if (s.found) {
27880
27890
  agents = s.agents.length;
27881
27891
  running = s.agents.filter((a) => a.derivedActivity === "running").length;
27882
- lastActiveMs = s.writtenAt;
27892
+ lastActiveMs = daemonLastActiveMs(s, now);
27883
27893
  }
27884
27894
  }
27885
27895
  results.push({ id, pid: data.pid, alive, agents, running, lastActiveMs });
@@ -28502,7 +28512,38 @@ var TEXT_ESCAPE_MAP = { n: `
28502
28512
  function decodeTextEscapes(s) {
28503
28513
  return s.replace(/\\(.)/g, (m, c) => TEXT_ESCAPE_MAP[c] ?? m);
28504
28514
  }
28505
- var CLIENT_MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
28515
+ async function readLiteralInput(args) {
28516
+ const { command, stdinSelected, stdin, filePath, fileOption } = args;
28517
+ if (stdinSelected && filePath !== undefined) {
28518
+ throw new CliError(`${command}: --stdin and ${fileOption} are mutually exclusive`);
28519
+ }
28520
+ if (stdinSelected) {
28521
+ if (!stdin)
28522
+ throw new CliError(`${command}: stdin is unavailable`);
28523
+ if (stdin.isTTY === true) {
28524
+ throw new CliError(`${command}: --stdin requires piped input; use ${fileOption} in an interactive terminal`);
28525
+ }
28526
+ try {
28527
+ const chunks = [];
28528
+ for await (const chunk2 of stdin) {
28529
+ chunks.push(typeof chunk2 === "string" ? Buffer.from(chunk2, "utf8") : Buffer.from(chunk2));
28530
+ }
28531
+ return Buffer.concat(chunks).toString("utf8");
28532
+ } catch (err) {
28533
+ throw new CliError(`${command}: cannot read stdin: ${err.message}`);
28534
+ }
28535
+ }
28536
+ if (filePath !== undefined) {
28537
+ const fs13 = await import("fs/promises");
28538
+ try {
28539
+ return await fs13.readFile(filePath, "utf8");
28540
+ } catch (err) {
28541
+ throw new CliError(`${command}: cannot read file: ${err.message}`);
28542
+ }
28543
+ }
28544
+ return;
28545
+ }
28546
+ var CLIENT_MAX_ATTACHMENT_BYTES = 26214400;
28506
28547
  function contentTypeFromFilename(filename) {
28507
28548
  const ext = filename.slice(filename.lastIndexOf(".") + 1).toLowerCase();
28508
28549
  switch (ext) {
@@ -28560,7 +28601,7 @@ async function withTransientMutationRetry(mutation) {
28560
28601
  async function sendWithRetry(api2, req) {
28561
28602
  return withTransientMutationRetry(() => api2.send(req));
28562
28603
  }
28563
- async function cmdMessageSend(opts) {
28604
+ async function cmdMessageSend(opts, stdin) {
28564
28605
  const remindAfterFlag = opts.remindAfter;
28565
28606
  const remindAfterMs = remindAfterFlag === undefined ? undefined : parseRemindAfter(remindAfterFlag);
28566
28607
  const api2 = getApi();
@@ -28568,21 +28609,18 @@ async function cmdMessageSend(opts) {
28568
28609
  const channel2 = opts.target;
28569
28610
  if (!channel2)
28570
28611
  throw new CliError("message send: --target <ref> is required (e.g. /demo-workspace#1234/general)");
28571
- let text2;
28572
28612
  const fileFlag = opts.file;
28573
- const textFlag = opts.text;
28574
- if (fileFlag) {
28575
- const fs13 = await import("fs");
28576
- if (!fs13.existsSync(fileFlag))
28577
- throw new CliError(`message send: file not found: ${fileFlag}`);
28578
- text2 = fs13.readFileSync(fileFlag, "utf8").trim();
28579
- } else if (typeof textFlag === "string") {
28580
- text2 = decodeTextEscapes(textFlag);
28581
- }
28613
+ const text2 = await readLiteralInput({
28614
+ command: "message send",
28615
+ stdinSelected: opts.stdin === true,
28616
+ stdin,
28617
+ filePath: fileFlag,
28618
+ fileOption: "--file <path>"
28619
+ });
28582
28620
  const attachmentIds = Array.isArray(opts.attachment) ? opts.attachment : [];
28583
28621
  const hasText = typeof text2 === "string" && text2.trim().length > 0;
28584
28622
  if (!hasText && attachmentIds.length === 0) {
28585
- throw new CliError("message send: --text <text>, --file <path>, or --attachment <id> is required");
28623
+ throw new CliError("message send: --stdin, --file <path>, or --attachment <id> is required");
28586
28624
  }
28587
28625
  let replyToSeq;
28588
28626
  const replyFlag = opts.reply;
@@ -28626,45 +28664,6 @@ async function cmdMessageSend(opts) {
28626
28664
  return { sent, reminder: { armed: false, reason: "local reminder request failed" } };
28627
28665
  }
28628
28666
  }
28629
- async function createPostWithRetry(api2, req) {
28630
- return withTransientMutationRetry(() => api2.createPost(req));
28631
- }
28632
- async function cmdMessagePost(opts) {
28633
- const api2 = getApi();
28634
- const agent2 = agentId(opts);
28635
- const forum = opts.target;
28636
- if (!forum)
28637
- throw new CliError("message post: --target <forum-ref> is required (e.g. /demo#1234/ideas)");
28638
- const title = opts.title;
28639
- if (!title || title.trim().length === 0)
28640
- throw new CliError("message post: --title <name> is required");
28641
- let text2;
28642
- const fileFlag = opts.file;
28643
- const textFlag = opts.text;
28644
- if (fileFlag) {
28645
- const fs13 = await import("fs");
28646
- if (!fs13.existsSync(fileFlag))
28647
- throw new CliError(`message post: file not found: ${fileFlag}`);
28648
- text2 = fs13.readFileSync(fileFlag, "utf8").trim();
28649
- } else if (typeof textFlag === "string") {
28650
- text2 = decodeTextEscapes(textFlag);
28651
- }
28652
- const attachmentIds = Array.isArray(opts.attachment) ? opts.attachment : [];
28653
- const hasText = typeof text2 === "string" && text2.trim().length > 0;
28654
- if (!hasText && attachmentIds.length === 0) {
28655
- throw new CliError("message post: --text <text>, --file <path>, or --attachment <id> is required");
28656
- }
28657
- const nonce = randomUUID6();
28658
- const res = await createPostWithRetry(api2, {
28659
- agentId: agent2,
28660
- forum,
28661
- title: title.trim(),
28662
- content: { text: text2 ?? "" },
28663
- attachments: attachmentIds.length > 0 ? attachmentIds : undefined,
28664
- nonce
28665
- });
28666
- return { posted: res.ref };
28667
- }
28668
28667
  async function cmdMessageEmoji(opts) {
28669
28668
  const api2 = getApi();
28670
28669
  const target = opts.target;
@@ -28846,6 +28845,7 @@ async function cmdInboxPull(opts) {
28846
28845
  const pulledAt = nowLocalISO();
28847
28846
  let acked = 0;
28848
28847
  let ackError;
28848
+ let failed;
28849
28849
  if (opts.ack !== false && messages.length > 0) {
28850
28850
  const latest = new Map;
28851
28851
  for (const m of messages) {
@@ -28855,8 +28855,9 @@ async function cmdInboxPull(opts) {
28855
28855
  latest.set(m.channel, { channel: m.channel, seq: seqN });
28856
28856
  }
28857
28857
  try {
28858
- await api2.ack({ agentId: agent2, cursors: [...latest.values()] });
28859
- acked = latest.size;
28858
+ const result = await api2.ack({ agentId: agent2, cursors: [...latest.values()] });
28859
+ acked = result.applied.length;
28860
+ failed = result.failed;
28860
28861
  } catch (err) {
28861
28862
  ackError = err instanceof Error ? err.message : String(err);
28862
28863
  }
@@ -28866,6 +28867,7 @@ async function cmdInboxPull(opts) {
28866
28867
  hasMore,
28867
28868
  acked,
28868
28869
  pulledAt,
28870
+ ...failed ? { failed } : {},
28869
28871
  ...ackError ? { ackError } : {},
28870
28872
  ...markedCount > 0 ? {
28871
28873
  markedReminder: `You have ${markedCount} marked ${markedCount === 1 ? "message" : "messages"}. Resolve ${markedCount === 1 ? "it" : "them"} before going dark unless blocked.`
@@ -28961,38 +28963,28 @@ async function cmdFriendList(opts) {
28961
28963
  async function cmdNap(opts) {
28962
28964
  const api2 = getApi();
28963
28965
  const fileFlag = opts.handoff;
28964
- const textFlag = opts.text;
28965
- let handoff;
28966
- if (fileFlag) {
28967
- const fs13 = await import("fs");
28968
- if (!fs13.existsSync(fileFlag))
28969
- throw new CliError(`nap: handoff file not found: ${fileFlag}`);
28970
- handoff = fs13.readFileSync(fileFlag, "utf8").trim();
28971
- } else if (typeof textFlag === "string") {
28972
- handoff = decodeTextEscapes(textFlag).trim();
28973
- }
28974
- if (!handoff) {
28975
- throw new CliError("nap: a handoff is required — pass --handoff <file> or --text <note>");
28966
+ const handoff = await readLiteralInput({
28967
+ command: "nap",
28968
+ stdinSelected: false,
28969
+ filePath: fileFlag,
28970
+ fileOption: "--handoff <file>"
28971
+ });
28972
+ if (handoff === undefined || handoff.trim().length === 0) {
28973
+ throw new CliError("nap: a handoff is required — pass --handoff <file>");
28976
28974
  }
28977
28975
  return await api2.nap({ handoff });
28978
28976
  }
28979
- function buildProgram() {
28977
+ function buildProgram(stdin) {
28980
28978
  const program = new Command("alook").description("agent CLI").exitOverride().configureOutput({
28981
28979
  writeOut: () => {},
28982
28980
  writeErr: () => {}
28983
28981
  }).option("--agent <id>", "agent identity (or ALOOK_AGENT_ID env)");
28984
28982
  const message2 = program.command("message").description("message operations").exitOverride();
28985
28983
  message2.configureOutput({ writeOut: () => {}, writeErr: () => {} });
28986
- message2.command("send").description("send a message to a channel, DM, or thread").option("--target <ref>", "destination (path-style ref, e.g. /demo-workspace#1234/general)").option("--text <text>", "inline message body (short messages)").option("--file <path>", "read message body from a file (long messages)").option("-a, --attachment <id>", "attach an uploaded file by id (repeatable — order = message order)", (v, prev = []) => [...prev, v], []).option("--reply <seq>", 'reply to a message by its seq in --target (e.g. "#37" or 37)').option("--remind-after <duration>", "optionally arm one local follow-up wake after 1m..24h; a newer same-scope message or daemon restart cancels it").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
28987
- const localOpts = this.opts();
28988
- const globalOpts = program.opts();
28989
- const result = await cmdMessageSend({ ...globalOpts, ...localOpts });
28990
- printEnvelope({ success: result });
28991
- });
28992
- message2.command("post").description("create a new forum post in a forum").option("--target <forum-ref>", "the forum to post in (path-style ref, e.g. /demo#1234/ideas)").option("--title <name>", "the post title (its slug becomes the post's address)").option("--text <text>", "inline post body (short)").option("--file <path>", "read post body from a file (long)").option("-a, --attachment <id>", "attach an uploaded file by id (repeatable — order = body order)", (v, prev = []) => [...prev, v], []).exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
28984
+ message2.command("send").description("send a message to a channel, DM, or thread").option("--target <ref>", "destination (path-style ref, e.g. /demo-workspace#1234/general)").option("--stdin", "read the literal UTF-8 message body from non-TTY stdin").option("--file <path>", "read the literal UTF-8 message body from a file").option("-a, --attachment <id>", "attach an uploaded file by id (repeatable — order = message order)", (v, prev = []) => [...prev, v], []).option("--reply <seq>", 'reply to a message by its seq in --target (e.g. "#37" or 37)').option("--remind-after <duration>", "optionally arm one local follow-up wake after 1m..24h; a newer same-scope message or daemon restart cancels it").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
28993
28985
  const localOpts = this.opts();
28994
28986
  const globalOpts = program.opts();
28995
- const result = await cmdMessagePost({ ...globalOpts, ...localOpts });
28987
+ const result = await cmdMessageSend({ ...globalOpts, ...localOpts }, stdin);
28996
28988
  printEnvelope({ success: result });
28997
28989
  });
28998
28990
  message2.command("emoji").description("react to a message with a single emoji").requiredOption("--target <ref>", "message ref (path-style, e.g. /demo#1234/general#42 or /.dm/peer#0007#42)").requiredOption("--emoji <string>", "single emoji character").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
@@ -29097,7 +29089,7 @@ function buildProgram() {
29097
29089
  const result = await cmdSettingProfile({ ...program.opts(), ...this.opts() });
29098
29090
  printEnvelope({ success: result });
29099
29091
  });
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() {
29092
+ 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)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
29101
29093
  const localOpts = this.opts();
29102
29094
  const globalOpts = program.opts();
29103
29095
  const result = await cmdNap({ ...globalOpts, ...localOpts });
@@ -29178,8 +29170,9 @@ function buildProgram() {
29178
29170
  });
29179
29171
  return program;
29180
29172
  }
29181
- async function main(argv = process.argv.slice(2)) {
29182
- const program = buildProgram();
29173
+ async function main(argv = process.argv.slice(2), io = {}) {
29174
+ const stdin = io.stdin ?? process.stdin;
29175
+ const program = buildProgram(stdin);
29183
29176
  let internalExitCode = 0;
29184
29177
  try {
29185
29178
  await program.parseAsync(argv, { from: "user" });
package/dist/index.js CHANGED
@@ -57,7 +57,7 @@ function cliCommandsSection() {
57
57
  "### Messaging",
58
58
  "",
59
59
  `1. \`${CLI} inbox pull\` — fetch unread messages (advances your read waterline by default, ` + `so they won't re-pull; \`--no-ack\` to peek without advancing).`,
60
- `2. \`${CLI} message send\` — send to a channel, DM, or thread. Attach with ` + `\`--attachment <id>\` (repeatable, order matters). Optionally add ` + `\`--remind-after <duration>\` to be reminded if the same channel, thread, or DM receives no ` + `newer message after your send. Use a whole number of minutes or hours from \`1m\` to \`24h\`, ` + `such as \`15m\` or \`2h\`.`,
60
+ `2. \`${CLI} message send\` — send to a channel, DM, or thread. ` + `For a short body, use explicit \`--stdin\` with a quoted heredoc; for a long or complicated body, ` + `use \`--file <path>\`. Attach with \`--attachment <id>\` (repeatable, order matters). Optionally add ` + `\`--remind-after <duration>\` to be reminded if the same channel, thread, or DM receives no ` + `newer message after your send. Use a whole number of minutes or hours from \`1m\` to \`24h\`, ` + `such as \`15m\` or \`2h\`.`,
61
61
  `3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted. Feed it into ` + `\`message send --attachment <id>\`.`,
62
62
  `4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
63
63
  `5. \`${CLI} message emoji --target <ref> --emoji <e>\` — react with a single emoji. ` + `Works on channel messages (\`/<server>/<channel>#N\`), DM messages ` + `(\`/.dm/<peer>#N\`), and thread-reply messages (\`/<server>/<channel>/#N#M\`).`,
@@ -88,7 +88,7 @@ function cliCommandsSection() {
88
88
  "",
89
89
  "### Context Lifecycle",
90
90
  "",
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.`,
91
+ `1. \`${CLI} nap --handoff <file>\` — 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.`,
92
92
  "",
93
93
  "### Output format",
94
94
  "",
@@ -107,9 +107,11 @@ function messagingSection() {
107
107
  "You can initiate conversations — send to any channel or DM someone directly. You're not " + "limited to replying. Use the same `message send` command whether you're replying or " + "starting a conversation.",
108
108
  "",
109
109
  "- Reply where the message came from. Post results in the channel that owns the topic. " + "When uncertain, read history (below) or DM the relevant people.",
110
- `- Short reply: \`${CLI} message send --target <ref> --text "brief reply"\`.`,
111
- `- Long or complicated: write body to a tmp file, then \`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
112
- `- Cite a specific message: \`${CLI} message send --target <ref> --reply "#37" --text "on it"\` ` + "`--reply` takes the `#N` seq (within `--target`) of the message you're answering.",
110
+ "Free-form message bodies never go in command arguments.",
111
+ "",
112
+ `- Short reply: use \`${CLI} message send --target <ref> --stdin\` with the quoted-heredoc form shown ` + "under *Message formatting*. Choose a fresh quoted delimiter that does not occur as a standalone line in the body.",
113
+ `- Long or complicated: write the body to a temporary file with a filesystem tool, then ` + `\`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
114
+ `- Cite a specific message: add \`--reply "#37"\` to either form — \`--reply\` takes the \`#N\` seq ` + "(within `--target`) of the message you're answering.",
113
115
  "",
114
116
  "### Context refs",
115
117
  "",
@@ -149,8 +151,10 @@ function messagingSection() {
149
151
  "- **Mentions** — `@name#NNNN` calls that person's attention specifically. In a private " + "channel, first verify they " + `are a member with \`${CLI} channel member --channel <ref>\` before mentioning them.`,
150
152
  "",
151
153
  "```bash",
152
- "# Notify Alice and point her to a message for context.",
153
- `${CLI} message send --target "/demo#1234/general" --text "@alice#0001 Please review /demo#1234/general#42"`,
154
+ "# Choose a fresh quoted delimiter that does not occur as a standalone line in the body.",
155
+ `${CLI} message send --target "/demo#1234/general" --stdin <<'ALOOK_MESSAGE_7F3C'`,
156
+ "@alice#0001 Please review /demo#1234/general#42",
157
+ "ALOOK_MESSAGE_7F3C",
154
158
  "```",
155
159
  "",
156
160
  "### Pulled messages",
@@ -216,7 +220,7 @@ function utilsSection() {
216
220
  "",
217
221
  `Use \`message send --remind-after <duration>\` when you send something that may need a later ` + "follow-up — for example, a question, approval request, handoff, or blocker — and silence would " + "leave the work unfinished. If no newer message appears in that channel, thread, or DM during " + "the duration, you'll receive a reminder to return and decide what to do next. Don't add it to " + "ordinary messages that need no follow-up.",
218
222
  "",
219
- `Example: \`${CLI} message send --target /demo#1234/team --text "Please review the rollout plan" ` + `--remind-after 1m\`.`
223
+ `Example: ${CLI} message send --target "/demo#1234/team" --remind-after 1m --file ./message.md`
220
224
  ].join(`
221
225
  `);
222
226
  }
@@ -4863,7 +4867,7 @@ var MODEL_SWITCH_REWAKE_PROMPT = "You were just switched to a different model. C
4863
4867
  function buildNapRewakePrompt(handoff) {
4864
4868
  return "You took a nap: you reset your own session, so prior conversation context " + `is gone. Before sleeping you left yourself this handoff —
4865
4869
 
4866
- ` + handoff.trim() + `
4870
+ ` + handoff + `
4867
4871
 
4868
4872
  Then read @memory.md and your .context_timeline for durable context, and pull ` + "your inbox before doing anything else. If it reports marked messages, run `$ALOOK_CLI " + "message mark list` and resume that outstanding work.";
4869
4873
  }
@@ -24028,7 +24032,10 @@ async function createDaemon(opts) {
24028
24032
  let statusTimer = null;
24029
24033
  if (opts.statusFilePath) {
24030
24034
  const statusPath = opts.statusFilePath;
24031
- const writeStatus = () => writeStatusFile(statusPath, { writtenAt: Date.now(), agents: manager.statusProjection(Date.now()) });
24035
+ const writeStatus = () => {
24036
+ const nowMs = Date.now();
24037
+ writeStatusFile(statusPath, { writtenAt: nowMs, agents: manager.statusProjection(nowMs) });
24038
+ };
24032
24039
  writeStatus();
24033
24040
  statusTimer = setInterval(writeStatus, STATUS_WRITE_INTERVAL_MS);
24034
24041
  statusTimer.unref?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/daemon",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
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",