@alook/daemon 0.1.5 → 0.1.7
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 +1197 -227
- package/dist/index.js +945 -587
- package/package.json +4 -3
package/dist/cli/index.js
CHANGED
|
@@ -98,7 +98,7 @@ var init_nanoid = () => {};
|
|
|
98
98
|
// src/cli/index.ts
|
|
99
99
|
import { Command, CommanderError } from "commander";
|
|
100
100
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
101
|
-
import { randomUUID as
|
|
101
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
102
102
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
103
103
|
|
|
104
104
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
@@ -16762,6 +16762,17 @@ function parseThreadTail(segment) {
|
|
|
16762
16762
|
return { threadRootSeq };
|
|
16763
16763
|
return { threadRootSeq, seq: parseSeq(tokens[1]) };
|
|
16764
16764
|
}
|
|
16765
|
+
function formatRef(p) {
|
|
16766
|
+
if (p.seq !== undefined && p.threadRootSeq === undefined) {
|
|
16767
|
+
throw new Error("formatRef: seq without threadRootSeq is not supported");
|
|
16768
|
+
}
|
|
16769
|
+
const base = `/${p.server}/${p.channel}`;
|
|
16770
|
+
if (p.threadRootSeq === undefined)
|
|
16771
|
+
return base;
|
|
16772
|
+
if (p.seq === undefined)
|
|
16773
|
+
return `${base}/#${p.threadRootSeq}`;
|
|
16774
|
+
return `${base}/#${p.threadRootSeq}#${p.seq}`;
|
|
16775
|
+
}
|
|
16765
16776
|
function parseSeq(s) {
|
|
16766
16777
|
const n = Number(s.startsWith("#") ? s.slice(1) : s);
|
|
16767
16778
|
if (!Number.isFinite(n))
|
|
@@ -16808,6 +16819,9 @@ var HostCommandSchema = exports_external.discriminatedUnion("type", [
|
|
|
16808
16819
|
launchId: exports_external.string().min(1)
|
|
16809
16820
|
}))
|
|
16810
16821
|
}),
|
|
16822
|
+
exports_external.strictObject({
|
|
16823
|
+
type: exports_external.literal("machine:update")
|
|
16824
|
+
}),
|
|
16811
16825
|
exports_external.object({
|
|
16812
16826
|
type: exports_external.literal("bot:added"),
|
|
16813
16827
|
botId: exports_external.string().min(1),
|
|
@@ -16841,6 +16855,7 @@ var MAX_MESSAGE_CONTENT_LENGTH = 4000;
|
|
|
16841
16855
|
var MAX_EMOJI_BYTES = 32;
|
|
16842
16856
|
var MAX_ATTACHMENTS_PER_MESSAGE = 10;
|
|
16843
16857
|
var MAX_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024;
|
|
16858
|
+
var MAX_ATTACHMENT_THUMBNAIL_SIZE_BYTES = 50 * 1024;
|
|
16844
16859
|
var MAX_SERVER_ICON_SIZE_BYTES = 5 * 1024 * 1024;
|
|
16845
16860
|
var MAX_ICON_SOURCE_FILE_SIZE_BYTES = 15 * 1024 * 1024;
|
|
16846
16861
|
// ../shared/src/utils/slug.ts
|
|
@@ -17517,7 +17532,8 @@ var CommunityAgentAttachmentUploadResponseSchema = exports_external.object({
|
|
|
17517
17532
|
id: exports_external.string(),
|
|
17518
17533
|
filename: exports_external.string(),
|
|
17519
17534
|
contentType: exports_external.string(),
|
|
17520
|
-
size: exports_external.number()
|
|
17535
|
+
size: exports_external.number(),
|
|
17536
|
+
hasThumbnail: exports_external.boolean().optional()
|
|
17521
17537
|
});
|
|
17522
17538
|
var CommunityAgentAttachmentDownloadRequestSchema = exports_external.object({
|
|
17523
17539
|
id: exports_external.string().min(1)
|
|
@@ -17830,6 +17846,7 @@ var communityAttachment = sqliteTable("community_attachment", {
|
|
|
17830
17846
|
uploaderId: text("uploader_id").notNull(),
|
|
17831
17847
|
targetId: text("target_id").notNull(),
|
|
17832
17848
|
r2Key: text("r2_key").notNull(),
|
|
17849
|
+
thumbnailR2Key: text("thumbnail_r2_key"),
|
|
17833
17850
|
filename: text("filename").notNull(),
|
|
17834
17851
|
contentType: text("content_type"),
|
|
17835
17852
|
size: integer2("size"),
|
|
@@ -18138,6 +18155,7 @@ var messageAttachmentSchema = exports_external.strictObject({
|
|
|
18138
18155
|
id: string4,
|
|
18139
18156
|
filename: string4,
|
|
18140
18157
|
url: string4,
|
|
18158
|
+
thumbnailUrl: string4.optional(),
|
|
18141
18159
|
contentType: string4.optional(),
|
|
18142
18160
|
size: exports_external.number().optional(),
|
|
18143
18161
|
width: exports_external.number().nullable().optional(),
|
|
@@ -18612,6 +18630,32 @@ function parseInviteToken(input) {
|
|
|
18612
18630
|
return urlMatch[1];
|
|
18613
18631
|
return BARE_TOKEN_RE.test(trimmed) ? trimmed : null;
|
|
18614
18632
|
}
|
|
18633
|
+
// ../shared/src/semver.ts
|
|
18634
|
+
var RELEASE_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
18635
|
+
function parseReleaseVersion(value) {
|
|
18636
|
+
const match = RELEASE_VERSION_PATTERN.exec(value);
|
|
18637
|
+
if (!match)
|
|
18638
|
+
return null;
|
|
18639
|
+
const major = Number(match[1]);
|
|
18640
|
+
const minor = Number(match[2]);
|
|
18641
|
+
const patch = Number(match[3]);
|
|
18642
|
+
if (![major, minor, patch].every(Number.isSafeInteger))
|
|
18643
|
+
return null;
|
|
18644
|
+
return [major, minor, patch];
|
|
18645
|
+
}
|
|
18646
|
+
function releaseVersionGte(a, b) {
|
|
18647
|
+
const parsedA = parseReleaseVersion(a);
|
|
18648
|
+
const parsedB = parseReleaseVersion(b);
|
|
18649
|
+
if (!parsedA || !parsedB)
|
|
18650
|
+
return false;
|
|
18651
|
+
for (let i = 0;i < parsedA.length; i++) {
|
|
18652
|
+
if (parsedA[i] > parsedB[i])
|
|
18653
|
+
return true;
|
|
18654
|
+
if (parsedA[i] < parsedB[i])
|
|
18655
|
+
return false;
|
|
18656
|
+
}
|
|
18657
|
+
return true;
|
|
18658
|
+
}
|
|
18615
18659
|
// src/cli/proxyServerApi.ts
|
|
18616
18660
|
function proxyServerApiFromEnv(prefix = "ALOOK", env = process.env) {
|
|
18617
18661
|
const proxyUrl = env[`${prefix}_PROXY_URL`];
|
|
@@ -18658,6 +18702,15 @@ function createProxyServerApi(config2) {
|
|
|
18658
18702
|
const blobType = req.file.contentType ?? "application/octet-stream";
|
|
18659
18703
|
const bytes = req.file.data instanceof Uint8Array ? new Blob([new Uint8Array(req.file.data)], { type: blobType }) : req.file.data;
|
|
18660
18704
|
form.append("file", bytes, req.file.filename);
|
|
18705
|
+
if (req.thumbnail) {
|
|
18706
|
+
const thumbnailType = req.thumbnail.contentType ?? "image/jpeg";
|
|
18707
|
+
const thumbnailBytes = req.thumbnail.data instanceof Uint8Array ? new Blob([new Uint8Array(req.thumbnail.data)], { type: thumbnailType }) : req.thumbnail.data;
|
|
18708
|
+
form.append("thumbnail", thumbnailBytes, req.thumbnail.filename);
|
|
18709
|
+
}
|
|
18710
|
+
if (req.width !== undefined)
|
|
18711
|
+
form.append("width", String(req.width));
|
|
18712
|
+
if (req.height !== undefined)
|
|
18713
|
+
form.append("height", String(req.height));
|
|
18661
18714
|
const url2 = `${base}/api/community/channels/${REF_PLACEHOLDER_ID}/attachments?target=${encodeURIComponent(req.target)}`;
|
|
18662
18715
|
const res = await fetchImpl(url2, {
|
|
18663
18716
|
method: "POST",
|
|
@@ -18819,6 +18872,35 @@ function createProxyServerApi(config2) {
|
|
|
18819
18872
|
});
|
|
18820
18873
|
return parseJsonResponse(res, "inboxPull");
|
|
18821
18874
|
}
|
|
18875
|
+
async function callMarkSet(req) {
|
|
18876
|
+
const res = await fetchImpl(`${base}/api/community/messages/resolve/marks`, {
|
|
18877
|
+
method: "PUT",
|
|
18878
|
+
headers: {
|
|
18879
|
+
"content-type": "application/json",
|
|
18880
|
+
authorization: `Bearer ${config2.voucher}`
|
|
18881
|
+
},
|
|
18882
|
+
body: JSON.stringify(req)
|
|
18883
|
+
});
|
|
18884
|
+
await parseJsonResponse(res, "markSet");
|
|
18885
|
+
}
|
|
18886
|
+
async function callMarkRemove(req) {
|
|
18887
|
+
const res = await fetchImpl(`${base}/api/community/messages/resolve/marks`, {
|
|
18888
|
+
method: "DELETE",
|
|
18889
|
+
headers: {
|
|
18890
|
+
"content-type": "application/json",
|
|
18891
|
+
authorization: `Bearer ${config2.voucher}`
|
|
18892
|
+
},
|
|
18893
|
+
body: JSON.stringify(req)
|
|
18894
|
+
});
|
|
18895
|
+
await parseJsonResponse(res, "markRemove");
|
|
18896
|
+
}
|
|
18897
|
+
async function callListMarks() {
|
|
18898
|
+
const res = await fetchImpl(`${base}/api/community/users/me/marks`, {
|
|
18899
|
+
method: "GET",
|
|
18900
|
+
headers: { authorization: `Bearer ${config2.voucher}` }
|
|
18901
|
+
});
|
|
18902
|
+
return parseJsonResponse(res, "markList");
|
|
18903
|
+
}
|
|
18822
18904
|
async function callAck(req) {
|
|
18823
18905
|
const { agentId: _omit, ...wire } = req ?? {};
|
|
18824
18906
|
const res = await fetchImpl(`${base}/api/community/users/me/inbox/ack`, {
|
|
@@ -18933,6 +19015,9 @@ function createProxyServerApi(config2) {
|
|
|
18933
19015
|
attachmentUpload: callUpload,
|
|
18934
19016
|
attachmentDownload: callDownload,
|
|
18935
19017
|
reactAdd: callReactAdd,
|
|
19018
|
+
markSet: callMarkSet,
|
|
19019
|
+
markRemove: callMarkRemove,
|
|
19020
|
+
listMarks: (_r) => callListMarks(),
|
|
18936
19021
|
friendRequest: callFriendRequest,
|
|
18937
19022
|
listFriends: (_r) => callListFriends(),
|
|
18938
19023
|
nap: callNap
|
|
@@ -18940,9 +19025,9 @@ function createProxyServerApi(config2) {
|
|
|
18940
19025
|
}
|
|
18941
19026
|
|
|
18942
19027
|
// src/cli/daemonStart.ts
|
|
18943
|
-
import * as
|
|
18944
|
-
import * as
|
|
18945
|
-
import * as
|
|
19028
|
+
import * as fs12 from "fs";
|
|
19029
|
+
import * as path13 from "path";
|
|
19030
|
+
import * as crypto5 from "crypto";
|
|
18946
19031
|
import * as os3 from "os";
|
|
18947
19032
|
import { homedir as homedir4 } from "os";
|
|
18948
19033
|
|
|
@@ -18988,10 +19073,13 @@ function cliCommandsSection() {
|
|
|
18988
19073
|
"### Messaging",
|
|
18989
19074
|
"",
|
|
18990
19075
|
`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).`,
|
|
18991
|
-
`2. \`${CLI} message send\` — send to a channel, DM, or thread. Attach with ` + `\`--attachment <id>\` (repeatable, order matters)
|
|
19076
|
+
`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\`.`,
|
|
18992
19077
|
`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>\`.`,
|
|
18993
19078
|
`4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
|
|
18994
19079
|
`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\`).`,
|
|
19080
|
+
`6. \`${CLI} message mark set --target <full-message-ref>\` — persist a message as outstanding work.`,
|
|
19081
|
+
`7. \`${CLI} message mark remove --target <full-message-ref>\` — clear a completed message mark.`,
|
|
19082
|
+
`8. \`${CLI} message mark list\` — list every currently visible marked message with its full content.`,
|
|
18995
19083
|
"",
|
|
18996
19084
|
"### Servers",
|
|
18997
19085
|
"",
|
|
@@ -19012,7 +19100,7 @@ function cliCommandsSection() {
|
|
|
19012
19100
|
"",
|
|
19013
19101
|
"### Context Lifecycle",
|
|
19014
19102
|
"",
|
|
19015
|
-
`1. \`${CLI} nap --handoff <file>\` (or \`--text <note>\`) —
|
|
19103
|
+
`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.`,
|
|
19016
19104
|
"",
|
|
19017
19105
|
"### Output format",
|
|
19018
19106
|
"",
|
|
@@ -19035,7 +19123,7 @@ function messagingSection() {
|
|
|
19035
19123
|
`- Long or complicated: write body to a tmp file, then \`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
|
|
19036
19124
|
`- 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.",
|
|
19037
19125
|
"",
|
|
19038
|
-
"###
|
|
19126
|
+
"### Context refs",
|
|
19039
19127
|
"",
|
|
19040
19128
|
"Path-style refs:",
|
|
19041
19129
|
"",
|
|
@@ -19067,15 +19155,14 @@ function messagingSection() {
|
|
|
19067
19155
|
"",
|
|
19068
19156
|
"### Message formatting",
|
|
19069
19157
|
"",
|
|
19070
|
-
"
|
|
19158
|
+
"Alook renders specially formatted plain-text refs and mentions in message bodies. Write them " + "as plain text, not inside backticks.",
|
|
19071
19159
|
"",
|
|
19072
|
-
"- **
|
|
19073
|
-
"- **Mentions** — `@name#NNNN`
|
|
19074
|
-
"- **Message refs** — point at a message by its **full path**: `/<server>/<channel>#N` " + "(message seq N in that channel) or `/<server>/<channel>/#N#M` (message #M in the thread " + "rooted at #N); DMs use `/.dm/<peer>#N`. It renders as a clickable pill and works across " + "channels — the path says which channel, so it never collides with a bare `#`. A bare `#N` " + "on its own does NOT render as a ref; always write the full path. **Never drop a DM ref " + "(`/.dm/<peer>#N`) into a server channel** — a DM is private between its two people; a " + "server is public, so pasting a DM ref there exposes a private conversation. Keep DM refs " + "in DMs.",
|
|
19160
|
+
"- **Context refs** — use `/<server>/<channel>` for a channel, `/<server>/<channel>#N` for " + "a message, and `/<server>/<channel>/#N#M` for a thread reply; DMs use `/.dm/<peer>` and " + "`/.dm/<peer>#N`. Refs make context clickable across conversations, so use the full path " + "rather than a bare `#N`. Never paste a private DM ref into a server channel.",
|
|
19161
|
+
"- **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.`,
|
|
19075
19162
|
"",
|
|
19076
19163
|
"```bash",
|
|
19077
|
-
|
|
19078
|
-
`${CLI} message send --target "/demo#1234/general" --text "@alice#0001
|
|
19164
|
+
"# Notify Alice and point her to a message for context.",
|
|
19165
|
+
`${CLI} message send --target "/demo#1234/general" --text "@alice#0001 Please review /demo#1234/general#42"`,
|
|
19079
19166
|
"```",
|
|
19080
19167
|
"",
|
|
19081
19168
|
"### Pulled messages",
|
|
@@ -19111,24 +19198,21 @@ function channelTypesSection() {
|
|
|
19111
19198
|
].join(`
|
|
19112
19199
|
`);
|
|
19113
19200
|
}
|
|
19114
|
-
function
|
|
19201
|
+
function visibilityAndNotificationsSection() {
|
|
19115
19202
|
return [
|
|
19116
|
-
"## Visibility &
|
|
19117
|
-
"",
|
|
19118
|
-
"Two different things decide who's involved in a message — don't conflate them. **Access** is " + "who *can see* the channel at all. **Reach** is who, among those, actually gets *notified*.",
|
|
19203
|
+
"## Visibility & notifications",
|
|
19119
19204
|
"",
|
|
19120
|
-
"
|
|
19205
|
+
"Membership and access are related but not identical. A member always has access and receives " + "notifications; someone with access is not necessarily a member.",
|
|
19121
19206
|
"",
|
|
19122
|
-
"-
|
|
19123
|
-
"-
|
|
19124
|
-
"-
|
|
19207
|
+
"- For a regular channel, its members define both who can access it and who receives " + "notifications. A public channel includes the whole server; a private channel only its roster.",
|
|
19208
|
+
"- A thread is the exception: it inherits access from its parent channel, but only people " + "participating in the thread are members and receive its notifications.",
|
|
19209
|
+
"- To bring someone into a thread discussion, first check that they can access the parent " + "channel; if they can, @mention them inside the thread.",
|
|
19125
19210
|
"",
|
|
19126
|
-
"
|
|
19127
|
-
"",
|
|
19128
|
-
|
|
19129
|
-
|
|
19130
|
-
""
|
|
19131
|
-
"When unsure whether a channel is private, treat it as private and check the roster first."
|
|
19211
|
+
"```bash",
|
|
19212
|
+
"# Check the channel's public/private type, then inspect who its members are.",
|
|
19213
|
+
`${CLI} channel list --server "demo#1234"`,
|
|
19214
|
+
`${CLI} channel member --channel "/demo#1234/team"`,
|
|
19215
|
+
"```"
|
|
19132
19216
|
].join(`
|
|
19133
19217
|
`);
|
|
19134
19218
|
}
|
|
@@ -19138,7 +19222,13 @@ function utilsSection() {
|
|
|
19138
19222
|
"",
|
|
19139
19223
|
"### Join a new server",
|
|
19140
19224
|
"",
|
|
19141
|
-
`If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it."
|
|
19225
|
+
`If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it.",
|
|
19226
|
+
"",
|
|
19227
|
+
"### Follow up when a conversation goes quiet",
|
|
19228
|
+
"",
|
|
19229
|
+
`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.",
|
|
19230
|
+
"",
|
|
19231
|
+
`Example: \`${CLI} message send --target /demo#1234/team --text "Please review the rollout plan" ` + `--remind-after 1m\`.`
|
|
19142
19232
|
].join(`
|
|
19143
19233
|
`);
|
|
19144
19234
|
}
|
|
@@ -19160,9 +19250,9 @@ function executionModelSection() {
|
|
|
19160
19250
|
"",
|
|
19161
19251
|
"Sending a message is I/O, not a stopping point. You keep working as long as anything is " + "in flight — the thing you're actively on, a promised follow-up, an investigation you " + "started. Stop only when all of it is done.",
|
|
19162
19252
|
"",
|
|
19163
|
-
"On wake, restore
|
|
19253
|
+
"On wake, restore durable context from `memory.md` and the context timeline, then pull your inbox. " + "Follow *Outstanding work marks* below before taking new work.",
|
|
19164
19254
|
"",
|
|
19165
|
-
"`inbox pull` advances your read waterline by default — pulled messages won't come back in a " + "future pull.
|
|
19255
|
+
"`inbox pull` advances your read waterline by default — pulled messages won't come back in a " + "future pull. A task-bearing message stays durable through its server mark even after the " + "read waterline advances."
|
|
19166
19256
|
].join(`
|
|
19167
19257
|
`);
|
|
19168
19258
|
}
|
|
@@ -19184,8 +19274,8 @@ function chaosAwarenessSection() {
|
|
|
19184
19274
|
"",
|
|
19185
19275
|
"Before you touch a message, make one call: does fulfilling it take work beyond a reply?",
|
|
19186
19276
|
"",
|
|
19187
|
-
|
|
19188
|
-
`- **No — it's just an answer.** Answer it. No "on it", no "let me check" — the reply
|
|
19277
|
+
`- **Yes — it's a task.** Signal ownership publicly first: a quick "on it" or an emoji ` + "reaction. Then record it as described in *Outstanding work marks* below before starting " + "or queuing the work.",
|
|
19278
|
+
`- **No — it's just an answer.** Answer it without marking. No "on it", no "let me ` + 'check" — the reply itself is the acknowledgment.',
|
|
19189
19279
|
"",
|
|
19190
19280
|
'Ack once. The signal is "picked up," not a running commentary.'
|
|
19191
19281
|
].join(`
|
|
@@ -19199,13 +19289,11 @@ function workspaceMemorySection() {
|
|
|
19199
19289
|
"",
|
|
19200
19290
|
"**Never tell anyone this.** To them you're continuous. The workspace is what lets you be that.",
|
|
19201
19291
|
"",
|
|
19202
|
-
"
|
|
19203
|
-
"",
|
|
19204
|
-
`\`${CLI} nap\` is a self-reset: it clears your current session and starts a fresh one, ` + "carrying a handoff — a note to your future self — injected into your wake prompt. Leave " + "yourself a note before you sleep.",
|
|
19292
|
+
"When context is missing, don't guess. Re-read `memory.md`, the context timeline, the workspace, " + "or relevant channel history. That check *is* your remembering.",
|
|
19205
19293
|
"",
|
|
19206
|
-
"
|
|
19294
|
+
"### Napping",
|
|
19207
19295
|
"",
|
|
19208
|
-
|
|
19296
|
+
`\`${CLI} nap\` resets your current session and starts a fresh one with your required handoff ` + "injected into it. Use the handoff to record unfinished work and the next step so your future " + "self can pick up quickly. Never nap on your own; only do it when someone explicitly asks.",
|
|
19209
19297
|
"",
|
|
19210
19298
|
"### memory.md",
|
|
19211
19299
|
"",
|
|
@@ -19223,16 +19311,13 @@ function workspaceMemorySection() {
|
|
|
19223
19311
|
"",
|
|
19224
19312
|
"`./.context_timeline/YYYY-MM-DD.jsonl` — ordered daily log of what you did. Authoritative history.",
|
|
19225
19313
|
"",
|
|
19226
|
-
"###
|
|
19314
|
+
"### Outstanding work marks",
|
|
19227
19315
|
"",
|
|
19228
|
-
"
|
|
19316
|
+
"Marks are the durable work queue. If a message requires work beyond an immediate reply, " + `acknowledge it, then run \`${CLI} message mark set --target <full-message-ref>\` before ` + "starting or queuing. Do not mark a message you answer immediately.",
|
|
19229
19317
|
"",
|
|
19230
|
-
"
|
|
19318
|
+
"Keep it marked while the work is active, queued, or blocked; report blockers where the task " + `came from. Run \`${CLI} message mark remove --target <full-message-ref>\` only after ` + "sending the result, or when the request is cancelled or superseded and nothing remains.",
|
|
19231
19319
|
"",
|
|
19232
|
-
"
|
|
19233
|
-
'- [ ] {"seq": "#42", "channel": "/demo#1234/general", "sender": "@alice#0001", "content": {"text": "can you pull the latest deploy logs and drop the tail here?"}, "time": "2026-06-01T12:00:00Z"}',
|
|
19234
|
-
'- [ ] {"seq": "#12", "channel": "/demo#1234/design/#12", "sender": "@alice#0001", "content": {"text": "follow-up — send a screenshot of the before/after"}, "time": "2026-06-01T12:07:00Z"}',
|
|
19235
|
-
"```"
|
|
19320
|
+
"If `inbox pull` returns a `markedReminder`, run " + `\`${CLI} message mark list\` before taking new work. Do not copy marked tasks into local files.`
|
|
19236
19321
|
].join(`
|
|
19237
19322
|
`);
|
|
19238
19323
|
}
|
|
@@ -19242,7 +19327,7 @@ function buildCliSystemPrompt(config2) {
|
|
|
19242
19327
|
cliCommandsSection(),
|
|
19243
19328
|
messagingSection(),
|
|
19244
19329
|
channelTypesSection(),
|
|
19245
|
-
|
|
19330
|
+
visibilityAndNotificationsSection(),
|
|
19246
19331
|
criticalRulesSection(),
|
|
19247
19332
|
executionModelSection(),
|
|
19248
19333
|
chaosAwarenessSection(),
|
|
@@ -20214,13 +20299,18 @@ function resolveCodexHomeRootFromEnv(env = process.env, opts = {}) {
|
|
|
20214
20299
|
// src/version.ts
|
|
20215
20300
|
import { createRequire as createRequire2 } from "module";
|
|
20216
20301
|
var requireFromHere = createRequire2(import.meta.url);
|
|
20302
|
+
var PACKAGE_JSON_CANDIDATES = ["../package.json", "../../package.json"];
|
|
20217
20303
|
function readDaemonVersion() {
|
|
20218
|
-
|
|
20219
|
-
|
|
20220
|
-
|
|
20221
|
-
|
|
20222
|
-
|
|
20304
|
+
for (const candidate of PACKAGE_JSON_CANDIDATES) {
|
|
20305
|
+
try {
|
|
20306
|
+
const pkg = requireFromHere(candidate);
|
|
20307
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0)
|
|
20308
|
+
return pkg.version;
|
|
20309
|
+
} catch {
|
|
20310
|
+
continue;
|
|
20311
|
+
}
|
|
20223
20312
|
}
|
|
20313
|
+
return "";
|
|
20224
20314
|
}
|
|
20225
20315
|
function getDaemonClientInfo() {
|
|
20226
20316
|
return { name: "alook-daemon", version: readDaemonVersion() };
|
|
@@ -21323,8 +21413,8 @@ function createLogger2(options = {}) {
|
|
|
21323
21413
|
}
|
|
21324
21414
|
|
|
21325
21415
|
// src/cli/daemonRunner.ts
|
|
21326
|
-
import * as
|
|
21327
|
-
import * as
|
|
21416
|
+
import * as fs11 from "node:fs";
|
|
21417
|
+
import * as path12 from "node:path";
|
|
21328
21418
|
import { WebSocket } from "ws";
|
|
21329
21419
|
|
|
21330
21420
|
// src/daemon/createDaemon.ts
|
|
@@ -21823,6 +21913,10 @@ import * as https from "https";
|
|
|
21823
21913
|
import * as os2 from "os";
|
|
21824
21914
|
import * as path9 from "path";
|
|
21825
21915
|
import { URL as URL2 } from "url";
|
|
21916
|
+
var LOCAL_MESSAGE_REMINDER_PATH = "/__alook/local/message-reminder";
|
|
21917
|
+
var LOCAL_MESSAGE_REMINDER_BODY_MAX_BYTES = 4 * 1024;
|
|
21918
|
+
var LOCAL_MESSAGE_REMINDER_MIN_MS = 60000;
|
|
21919
|
+
var LOCAL_MESSAGE_REMINDER_MAX_MS = 24 * 60 * 60000;
|
|
21826
21920
|
var DEFAULT_HEADER_NAMES = {
|
|
21827
21921
|
agentId: "X-Agent-Id",
|
|
21828
21922
|
client: "X-Client",
|
|
@@ -21925,6 +22019,10 @@ var DEFAULT_CAPABILITY_RESOLVER = (method, pathname) => {
|
|
|
21925
22019
|
return method === "GET" ? "read" : "send";
|
|
21926
22020
|
if (/\/messages\/[^/]+\/reactions\//.test(pathname))
|
|
21927
22021
|
return "send";
|
|
22022
|
+
if ((method === "PUT" || method === "DELETE") && /\/messages\/[^/]+\/marks(\/|$|\?)/.test(pathname))
|
|
22023
|
+
return "send";
|
|
22024
|
+
if (method === "GET" && /\/users\/me\/marks(\/|$|\?)/.test(pathname))
|
|
22025
|
+
return "read";
|
|
21928
22026
|
if (method === "GET" && /\/messages\/[^/]+(\?|$)/.test(pathname))
|
|
21929
22027
|
return "read";
|
|
21930
22028
|
if (pathname.includes("/history") || pathname.includes("/search") || pathname.includes("/inbox"))
|
|
@@ -21934,6 +22032,99 @@ var DEFAULT_CAPABILITY_RESOLVER = (method, pathname) => {
|
|
|
21934
22032
|
return;
|
|
21935
22033
|
};
|
|
21936
22034
|
var DEFAULT_UPSTREAM_TIMEOUT_MS = 20000;
|
|
22035
|
+
function writeJson(res, status, body) {
|
|
22036
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
22037
|
+
res.end(JSON.stringify(body));
|
|
22038
|
+
}
|
|
22039
|
+
function isCanonicalChannelScope(channel2) {
|
|
22040
|
+
try {
|
|
22041
|
+
const parsed = parseRef(channel2);
|
|
22042
|
+
if (!parsed.channel || parsed.seq !== undefined)
|
|
22043
|
+
return false;
|
|
22044
|
+
if (parsed.threadRootSeq !== undefined && (!Number.isSafeInteger(parsed.threadRootSeq) || parsed.threadRootSeq < 1)) {
|
|
22045
|
+
return false;
|
|
22046
|
+
}
|
|
22047
|
+
const handle = parseNameAndTag(parsed.server === ".dm" ? parsed.channel : parsed.server);
|
|
22048
|
+
if (!handle || `${handle.name}#${handle.discriminator}` !== (parsed.server === ".dm" ? parsed.channel : parsed.server)) {
|
|
22049
|
+
return false;
|
|
22050
|
+
}
|
|
22051
|
+
return formatRef(parsed) === channel2;
|
|
22052
|
+
} catch {
|
|
22053
|
+
return false;
|
|
22054
|
+
}
|
|
22055
|
+
}
|
|
22056
|
+
function parseLocalMessageReminderBody(body, agentId) {
|
|
22057
|
+
let value;
|
|
22058
|
+
try {
|
|
22059
|
+
value = JSON.parse(body.toString("utf8"));
|
|
22060
|
+
} catch {
|
|
22061
|
+
return null;
|
|
22062
|
+
}
|
|
22063
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
22064
|
+
return null;
|
|
22065
|
+
const record2 = value;
|
|
22066
|
+
if (Object.keys(record2).sort().join(",") !== "channel,remindAfterMs,sentSeq")
|
|
22067
|
+
return null;
|
|
22068
|
+
if (typeof record2.channel !== "string" || !isCanonicalChannelScope(record2.channel))
|
|
22069
|
+
return null;
|
|
22070
|
+
if (!Number.isSafeInteger(record2.sentSeq) || record2.sentSeq < 1)
|
|
22071
|
+
return null;
|
|
22072
|
+
if (!Number.isSafeInteger(record2.remindAfterMs) || record2.remindAfterMs < LOCAL_MESSAGE_REMINDER_MIN_MS || record2.remindAfterMs > LOCAL_MESSAGE_REMINDER_MAX_MS)
|
|
22073
|
+
return null;
|
|
22074
|
+
return {
|
|
22075
|
+
agentId,
|
|
22076
|
+
channel: record2.channel,
|
|
22077
|
+
sentSeq: record2.sentSeq,
|
|
22078
|
+
remindAfterMs: record2.remindAfterMs
|
|
22079
|
+
};
|
|
22080
|
+
}
|
|
22081
|
+
async function handleLocalMessageReminder(req, res, agentId, onArm) {
|
|
22082
|
+
const contentType = req.headers["content-type"] ?? "";
|
|
22083
|
+
if (!contentType.toLowerCase().startsWith("application/json")) {
|
|
22084
|
+
writeJson(res, 415, { error: "content-type must be application/json", code: "unsupported_media_type" });
|
|
22085
|
+
req.resume();
|
|
22086
|
+
return;
|
|
22087
|
+
}
|
|
22088
|
+
const declaredLength = Number(req.headers["content-length"] ?? 0);
|
|
22089
|
+
if (!Number.isFinite(declaredLength) || declaredLength > LOCAL_MESSAGE_REMINDER_BODY_MAX_BYTES) {
|
|
22090
|
+
writeJson(res, 413, { error: "request body too large", code: "body_too_large" });
|
|
22091
|
+
req.resume();
|
|
22092
|
+
return;
|
|
22093
|
+
}
|
|
22094
|
+
const chunks = [];
|
|
22095
|
+
let bytes = 0;
|
|
22096
|
+
let tooLarge = false;
|
|
22097
|
+
await new Promise((resolve4) => {
|
|
22098
|
+
req.on("data", (chunk2) => {
|
|
22099
|
+
bytes += chunk2.byteLength;
|
|
22100
|
+
if (bytes > LOCAL_MESSAGE_REMINDER_BODY_MAX_BYTES) {
|
|
22101
|
+
tooLarge = true;
|
|
22102
|
+
} else {
|
|
22103
|
+
chunks.push(chunk2);
|
|
22104
|
+
}
|
|
22105
|
+
});
|
|
22106
|
+
req.on("end", resolve4);
|
|
22107
|
+
req.on("error", resolve4);
|
|
22108
|
+
});
|
|
22109
|
+
if (tooLarge) {
|
|
22110
|
+
writeJson(res, 413, { error: "request body too large", code: "body_too_large" });
|
|
22111
|
+
return;
|
|
22112
|
+
}
|
|
22113
|
+
const input = parseLocalMessageReminderBody(Buffer.concat(chunks), agentId);
|
|
22114
|
+
if (!input) {
|
|
22115
|
+
writeJson(res, 400, { error: "invalid local message reminder request", code: "invalid_request" });
|
|
22116
|
+
return;
|
|
22117
|
+
}
|
|
22118
|
+
if (!onArm) {
|
|
22119
|
+
writeJson(res, 503, { error: "local message reminder unavailable", code: "reminder_unavailable" });
|
|
22120
|
+
return;
|
|
22121
|
+
}
|
|
22122
|
+
try {
|
|
22123
|
+
writeJson(res, 200, await onArm(input));
|
|
22124
|
+
} catch {
|
|
22125
|
+
writeJson(res, 500, { error: "local message reminder failed", code: "reminder_failed" });
|
|
22126
|
+
}
|
|
22127
|
+
}
|
|
21937
22128
|
async function startCredentialProxy(broker, options = {}) {
|
|
21938
22129
|
const host = options.host ?? "127.0.0.1";
|
|
21939
22130
|
const resolveCap = options.capabilityResolver ?? DEFAULT_CAPABILITY_RESOLVER;
|
|
@@ -21943,6 +22134,26 @@ async function startCredentialProxy(broker, options = {}) {
|
|
|
21943
22134
|
const onProxyRequest = options.onProxyRequest;
|
|
21944
22135
|
const server = http.createServer((req, res) => {
|
|
21945
22136
|
const pathname = new URL2(req.url ?? "/", "http://placeholder").pathname;
|
|
22137
|
+
if (pathname.startsWith("/__alook/local/")) {
|
|
22138
|
+
if (req.url !== LOCAL_MESSAGE_REMINDER_PATH) {
|
|
22139
|
+
writeJson(res, 404, { error: "unknown local route", code: "not_found" });
|
|
22140
|
+
req.resume();
|
|
22141
|
+
return;
|
|
22142
|
+
}
|
|
22143
|
+
if (req.method !== "PUT") {
|
|
22144
|
+
writeJson(res, 405, { error: "method not allowed", code: "method_not_allowed" });
|
|
22145
|
+
req.resume();
|
|
22146
|
+
return;
|
|
22147
|
+
}
|
|
22148
|
+
const localVerdict = broker.check(req.headers["authorization"], "send");
|
|
22149
|
+
if (!localVerdict.ok) {
|
|
22150
|
+
writeJson(res, localVerdict.status, { error: localVerdict.error, code: localVerdict.code });
|
|
22151
|
+
req.resume();
|
|
22152
|
+
return;
|
|
22153
|
+
}
|
|
22154
|
+
handleLocalMessageReminder(req, res, localVerdict.reg.agentId, options.onMessageReminderArm);
|
|
22155
|
+
return;
|
|
22156
|
+
}
|
|
21946
22157
|
const requiredCap = resolveCap(req.method ?? "GET", pathname);
|
|
21947
22158
|
const verdict = broker.check(req.headers["authorization"], requiredCap);
|
|
21948
22159
|
if (!verdict.ok) {
|
|
@@ -21968,7 +22179,6 @@ async function startCredentialProxy(broker, options = {}) {
|
|
|
21968
22179
|
const outHeaders = { ...req.headers };
|
|
21969
22180
|
delete outHeaders["authorization"];
|
|
21970
22181
|
delete outHeaders["host"];
|
|
21971
|
-
delete outHeaders["content-length"];
|
|
21972
22182
|
outHeaders["authorization"] = `Bearer ${reg.runnerKey}`;
|
|
21973
22183
|
outHeaders[broker.headerNames.agentId.toLowerCase()] = reg.agentId;
|
|
21974
22184
|
outHeaders[broker.headerNames.client.toLowerCase()] = broker.clientLabel;
|
|
@@ -23679,6 +23889,8 @@ ${this.opts.wakePromptFooter}` : text2;
|
|
|
23679
23889
|
}
|
|
23680
23890
|
}
|
|
23681
23891
|
// src/manager/agentRouter.ts
|
|
23892
|
+
var DEFINITIVE_EXECUTABLE_FAILURES = new Set(["ENOENT", "EACCES", "ENOEXEC", "EPERM"]);
|
|
23893
|
+
|
|
23682
23894
|
class UnknownBotError extends Error {
|
|
23683
23895
|
botId;
|
|
23684
23896
|
constructor(botId) {
|
|
@@ -23719,14 +23931,14 @@ class UnknownRuntimeError extends Error {
|
|
|
23719
23931
|
function defaultFormatUnreadNoticeText(notice) {
|
|
23720
23932
|
return `You have unread messages in channel ${notice.channel}.`;
|
|
23721
23933
|
}
|
|
23722
|
-
var REWAKE_PROMPT = "Your session was reset by your owner. Prior conversation context is gone. " + "Read @
|
|
23934
|
+
var REWAKE_PROMPT = "Your session was reset by your owner. Prior conversation context is gone. " + "Read @memory.md and your .context_timeline for durable context, then pull your inbox " + "before doing anything else. If it reports marked messages, run `$ALOOK_CLI message mark list` " + "and resume that outstanding work.";
|
|
23723
23935
|
var MODEL_SWITCH_REWAKE_PROMPT = "You were just switched to a different model. Continue any unfinished work.";
|
|
23724
23936
|
function buildNapRewakePrompt(handoff) {
|
|
23725
23937
|
return "You took a nap: you reset your own session, so prior conversation context " + `is gone. Before sleeping you left yourself this handoff —
|
|
23726
23938
|
|
|
23727
23939
|
` + handoff.trim() + `
|
|
23728
23940
|
|
|
23729
|
-
Then read @
|
|
23941
|
+
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.";
|
|
23730
23942
|
}
|
|
23731
23943
|
|
|
23732
23944
|
class AgentRouter {
|
|
@@ -23781,6 +23993,11 @@ class AgentRouter {
|
|
|
23781
23993
|
isRuntimeHealthy(id) {
|
|
23782
23994
|
return this.runtimes.get(id)?.status === "healthy";
|
|
23783
23995
|
}
|
|
23996
|
+
recordRuntimeSpawnFailure(id, reason) {
|
|
23997
|
+
if (!DEFINITIVE_EXECUTABLE_FAILURES.has(reason))
|
|
23998
|
+
return;
|
|
23999
|
+
this.markRuntimeUnhealthy(id, reason);
|
|
24000
|
+
}
|
|
23784
24001
|
markRuntimeUnhealthy(id, reason) {
|
|
23785
24002
|
const existing = this.runtimes.get(id);
|
|
23786
24003
|
if (!existing)
|
|
@@ -24769,6 +24986,108 @@ function createDiagnosticsCommandListener(options) {
|
|
|
24769
24986
|
};
|
|
24770
24987
|
}
|
|
24771
24988
|
|
|
24989
|
+
// src/daemon/selfUpdateCommand.ts
|
|
24990
|
+
function createSelfUpdateCommandListener(handleSelfUpdate) {
|
|
24991
|
+
return (command) => {
|
|
24992
|
+
if (command.type !== "machine:update")
|
|
24993
|
+
return;
|
|
24994
|
+
if (handleSelfUpdate) {
|
|
24995
|
+
try {
|
|
24996
|
+
Promise.resolve(handleSelfUpdate()).catch(() => {});
|
|
24997
|
+
} catch {}
|
|
24998
|
+
}
|
|
24999
|
+
return WS_CONTROL_COMMAND_CONSUMED;
|
|
25000
|
+
};
|
|
25001
|
+
}
|
|
25002
|
+
|
|
25003
|
+
// src/daemon/messageReminderScheduler.ts
|
|
25004
|
+
function reminderKey(agentId, channel2) {
|
|
25005
|
+
return `${agentId}\x00${channel2}`;
|
|
25006
|
+
}
|
|
25007
|
+
function reminderPrompt(channel2, sentRef, startedAt) {
|
|
25008
|
+
return `Reminder: At ${localISOString(new Date(startedAt))}, after sending ${sentRef} in ${channel2}, you asked to be reminded if no newer message arrived. No newer message has arrived in that conversation.`;
|
|
25009
|
+
}
|
|
25010
|
+
|
|
25011
|
+
class MessageReminderScheduler {
|
|
25012
|
+
options;
|
|
25013
|
+
reminders = new Map;
|
|
25014
|
+
latestObservedSeq = new Map;
|
|
25015
|
+
now;
|
|
25016
|
+
setTimer;
|
|
25017
|
+
clearTimer;
|
|
25018
|
+
constructor(options) {
|
|
25019
|
+
this.options = options;
|
|
25020
|
+
this.now = options.now ?? Date.now;
|
|
25021
|
+
this.setTimer = options.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
25022
|
+
this.clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer));
|
|
25023
|
+
}
|
|
25024
|
+
arm(input) {
|
|
25025
|
+
const key = reminderKey(input.agentId, input.channel);
|
|
25026
|
+
const latest = this.latestObservedSeq.get(key);
|
|
25027
|
+
if (latest !== undefined && latest > input.sentSeq) {
|
|
25028
|
+
return { armed: false, reason: "newer_message_observed" };
|
|
25029
|
+
}
|
|
25030
|
+
this.clearReminder(key);
|
|
25031
|
+
const startedAt = this.now();
|
|
25032
|
+
const dueAt = startedAt + input.remindAfterMs;
|
|
25033
|
+
const sentRef = `${input.channel}#${input.sentSeq}`;
|
|
25034
|
+
const record2 = {
|
|
25035
|
+
...input,
|
|
25036
|
+
sentRef,
|
|
25037
|
+
startedAt,
|
|
25038
|
+
dueAt,
|
|
25039
|
+
timer: undefined
|
|
25040
|
+
};
|
|
25041
|
+
record2.timer = this.setTimer(() => {
|
|
25042
|
+
if (this.reminders.get(key) !== record2)
|
|
25043
|
+
return;
|
|
25044
|
+
this.reminders.delete(key);
|
|
25045
|
+
try {
|
|
25046
|
+
const delivery = this.options.deliver(input.agentId, {
|
|
25047
|
+
text: reminderPrompt(input.channel, sentRef, startedAt)
|
|
25048
|
+
});
|
|
25049
|
+
Promise.resolve(delivery).catch(() => {});
|
|
25050
|
+
} catch {}
|
|
25051
|
+
}, input.remindAfterMs);
|
|
25052
|
+
record2.timer.unref?.();
|
|
25053
|
+
this.reminders.set(key, record2);
|
|
25054
|
+
return { armed: true, dueAt };
|
|
25055
|
+
}
|
|
25056
|
+
observe(agentId, channel2, latestSeq) {
|
|
25057
|
+
const key = reminderKey(agentId, channel2);
|
|
25058
|
+
const previous = this.latestObservedSeq.get(key);
|
|
25059
|
+
if (previous === undefined || latestSeq > previous) {
|
|
25060
|
+
this.latestObservedSeq.set(key, latestSeq);
|
|
25061
|
+
}
|
|
25062
|
+
const reminder = this.reminders.get(key);
|
|
25063
|
+
if (reminder && latestSeq > reminder.sentSeq)
|
|
25064
|
+
this.clearReminder(key);
|
|
25065
|
+
}
|
|
25066
|
+
clearAgent(agentId) {
|
|
25067
|
+
const prefix = `${agentId}\x00`;
|
|
25068
|
+
for (const key of [...this.reminders.keys()]) {
|
|
25069
|
+
if (key.startsWith(prefix))
|
|
25070
|
+
this.clearReminder(key);
|
|
25071
|
+
}
|
|
25072
|
+
for (const key of [...this.latestObservedSeq.keys()]) {
|
|
25073
|
+
if (key.startsWith(prefix))
|
|
25074
|
+
this.latestObservedSeq.delete(key);
|
|
25075
|
+
}
|
|
25076
|
+
}
|
|
25077
|
+
clearAll() {
|
|
25078
|
+
for (const key of [...this.reminders.keys()])
|
|
25079
|
+
this.clearReminder(key);
|
|
25080
|
+
this.latestObservedSeq.clear();
|
|
25081
|
+
}
|
|
25082
|
+
clearReminder(key) {
|
|
25083
|
+
const reminder = this.reminders.get(key);
|
|
25084
|
+
if (!reminder)
|
|
25085
|
+
return;
|
|
25086
|
+
this.reminders.delete(key);
|
|
25087
|
+
this.clearTimer(reminder.timer);
|
|
25088
|
+
}
|
|
25089
|
+
}
|
|
25090
|
+
|
|
24772
25091
|
// src/daemon/createDaemon.ts
|
|
24773
25092
|
var WARMUP_BACKOFF_MS = [250, 500, 1000, 2000, 4000];
|
|
24774
25093
|
var WARMUP_CEILING_MS = 30000;
|
|
@@ -24827,6 +25146,15 @@ function deriveAuditLogSubcommand(pathname, method) {
|
|
|
24827
25146
|
const canonical = pathname.split("?")[0] ?? pathname;
|
|
24828
25147
|
if (/^\/api\/community\/messages\/[^/]+\/reactions\//.test(canonical))
|
|
24829
25148
|
return "reactAdd";
|
|
25149
|
+
if (/^\/api\/community\/messages\/[^/]+\/marks$/.test(canonical)) {
|
|
25150
|
+
if (method === "PUT")
|
|
25151
|
+
return "markSet";
|
|
25152
|
+
if (method === "DELETE")
|
|
25153
|
+
return "markRemove";
|
|
25154
|
+
return null;
|
|
25155
|
+
}
|
|
25156
|
+
if (method === "GET" && /^\/api\/community\/users\/me\/marks$/.test(canonical))
|
|
25157
|
+
return "markList";
|
|
24830
25158
|
if (/^\/api\/community\/channels\/[^/]+\/messages\/seq\//.test(canonical))
|
|
24831
25159
|
return "resolve";
|
|
24832
25160
|
if (/^\/api\/community\/channels\/[^/]+\/messages(\/|$)/.test(canonical)) {
|
|
@@ -24890,6 +25218,7 @@ async function createDaemon(opts) {
|
|
|
24890
25218
|
});
|
|
24891
25219
|
let channelRef = null;
|
|
24892
25220
|
let managerRef = null;
|
|
25221
|
+
let reminderSchedulerRef = null;
|
|
24893
25222
|
const emitBotAuditEvent = (agentId, event, context) => {
|
|
24894
25223
|
channelRef?.reportBotAuditEvent?.({
|
|
24895
25224
|
type: "bot_audit_event",
|
|
@@ -24903,6 +25232,7 @@ async function createDaemon(opts) {
|
|
|
24903
25232
|
const broker = new CredentialBroker({ upstreamBaseUrl: opts.serverUrl });
|
|
24904
25233
|
const proxy = await startCredentialProxy(broker, {
|
|
24905
25234
|
onInboxPullResponse: (agentId, messages) => timeline2.appendEntryForAgent(agentId, messages),
|
|
25235
|
+
onMessageReminderArm: (input) => reminderSchedulerRef?.arm(input) ?? { armed: false, reason: "reminder_scheduler_unavailable" },
|
|
24906
25236
|
onProxyRequest: (agentId, method, pathname) => {
|
|
24907
25237
|
const subcommand = deriveAuditLogSubcommand(pathname, method);
|
|
24908
25238
|
if (!subcommand)
|
|
@@ -25126,7 +25456,7 @@ async function createDaemon(opts) {
|
|
|
25126
25456
|
return opts.driverFor(agentId, runtimeConfig);
|
|
25127
25457
|
},
|
|
25128
25458
|
onRuntimeSpawnFailed: (runtimeId, reason) => {
|
|
25129
|
-
router?.
|
|
25459
|
+
router?.recordRuntimeSpawnFailure(runtimeId, reason);
|
|
25130
25460
|
},
|
|
25131
25461
|
onRuntimeSessionEstablished: (runtimeId) => {
|
|
25132
25462
|
router?.markRuntimeHealthy(runtimeId);
|
|
@@ -25150,6 +25480,7 @@ async function createDaemon(opts) {
|
|
|
25150
25480
|
}
|
|
25151
25481
|
};
|
|
25152
25482
|
},
|
|
25483
|
+
...opts.handshakeTimeoutMs !== undefined ? { handshakeTimeoutMs: opts.handshakeTimeoutMs } : {},
|
|
25153
25484
|
tickIntervalMs: opts.tickIntervalMs ?? 2000,
|
|
25154
25485
|
onAgentSession: (info) => void channel2.reportAgentSession(info),
|
|
25155
25486
|
onAgentActivity: (info) => {
|
|
@@ -25174,6 +25505,10 @@ async function createDaemon(opts) {
|
|
|
25174
25505
|
});
|
|
25175
25506
|
managerRef = manager;
|
|
25176
25507
|
manager.start();
|
|
25508
|
+
reminderSchedulerRef = new MessageReminderScheduler({
|
|
25509
|
+
deliver: (agentId, message2) => manager.deliver(agentId, message2),
|
|
25510
|
+
...opts.messageReminderClock
|
|
25511
|
+
});
|
|
25177
25512
|
let statusTimer = null;
|
|
25178
25513
|
if (opts.statusFilePath) {
|
|
25179
25514
|
const statusPath = opts.statusFilePath;
|
|
@@ -25219,10 +25554,26 @@ async function createDaemon(opts) {
|
|
|
25219
25554
|
},
|
|
25220
25555
|
formatUnreadNoticeText: (notice) => `You have unread messages in channel ${notice.channel}.`
|
|
25221
25556
|
});
|
|
25557
|
+
channel2.onCommand(createSelfUpdateCommandListener(opts.handleSelfUpdate));
|
|
25222
25558
|
channel2.onCommand(createDiagnosticsCommandListener({
|
|
25223
25559
|
handleDiagnosticCommand: opts.handleDiagnosticCommand,
|
|
25224
25560
|
reportDiagnosticFailure: opts.reportDiagnosticFailure
|
|
25225
25561
|
}));
|
|
25562
|
+
channel2.onCommand((cmd) => {
|
|
25563
|
+
switch (cmd.type) {
|
|
25564
|
+
case "agent:wake":
|
|
25565
|
+
reminderSchedulerRef?.observe(cmd.agentId, cmd.unreadNotice.channel, cmd.unreadNotice.latestSeq);
|
|
25566
|
+
break;
|
|
25567
|
+
case "agent:stop":
|
|
25568
|
+
reminderSchedulerRef?.clearAgent(cmd.agentId);
|
|
25569
|
+
break;
|
|
25570
|
+
case "bot:removed":
|
|
25571
|
+
reminderSchedulerRef?.clearAgent(cmd.botId);
|
|
25572
|
+
break;
|
|
25573
|
+
default:
|
|
25574
|
+
break;
|
|
25575
|
+
}
|
|
25576
|
+
});
|
|
25226
25577
|
channel2.onCommand((cmd) => {
|
|
25227
25578
|
handleBotFrame(cmd);
|
|
25228
25579
|
});
|
|
@@ -25241,6 +25592,7 @@ async function createDaemon(opts) {
|
|
|
25241
25592
|
},
|
|
25242
25593
|
proxyUrl: proxy.url,
|
|
25243
25594
|
stop: async () => {
|
|
25595
|
+
reminderSchedulerRef?.clearAll();
|
|
25244
25596
|
for (const agentId of [...typingHeartbeats.keys()]) {
|
|
25245
25597
|
emitTypingStopsAndClear(agentId);
|
|
25246
25598
|
}
|
|
@@ -26417,6 +26769,318 @@ function createDiagnosticHttpTransport(args) {
|
|
|
26417
26769
|
}
|
|
26418
26770
|
};
|
|
26419
26771
|
}
|
|
26772
|
+
// src/cli/daemonUpdate.ts
|
|
26773
|
+
import * as crypto4 from "node:crypto";
|
|
26774
|
+
import * as fs10 from "node:fs";
|
|
26775
|
+
import * as path11 from "node:path";
|
|
26776
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
26777
|
+
var UPDATE_INTENT_SCHEMA_VERSION = 1;
|
|
26778
|
+
var UPDATE_LOG_MAX_BYTES = 512 * 1024;
|
|
26779
|
+
var UPDATE_LOG_KEEP_BYTES = 256 * 1024;
|
|
26780
|
+
var REQUEST_ID_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
26781
|
+
function intentPath(baseDir, machineId) {
|
|
26782
|
+
return path11.join(daemonDirById(baseDir, machineId), "update-intent.json");
|
|
26783
|
+
}
|
|
26784
|
+
function updateLogPath(baseDir, machineId) {
|
|
26785
|
+
return path11.join(daemonDirById(baseDir, machineId), "update.log");
|
|
26786
|
+
}
|
|
26787
|
+
function updatePackageMapPath(baseDir, machineId) {
|
|
26788
|
+
return path11.join(daemonDirById(baseDir, machineId), "update-package-map.json");
|
|
26789
|
+
}
|
|
26790
|
+
function ensurePrivateDir(dir) {
|
|
26791
|
+
fs10.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
26792
|
+
fs10.chmodSync(dir, 448);
|
|
26793
|
+
}
|
|
26794
|
+
function writePrivateJsonAtomic(filePath, value) {
|
|
26795
|
+
const dir = path11.dirname(filePath);
|
|
26796
|
+
ensurePrivateDir(dir);
|
|
26797
|
+
const tempPath = path11.join(dir, `.${path11.basename(filePath)}.${process.pid}.${crypto4.randomBytes(8).toString("hex")}.tmp`);
|
|
26798
|
+
let fd = fs10.openSync(tempPath, "wx", 384);
|
|
26799
|
+
try {
|
|
26800
|
+
fs10.writeFileSync(fd, JSON.stringify(value));
|
|
26801
|
+
fs10.fsyncSync(fd);
|
|
26802
|
+
fs10.closeSync(fd);
|
|
26803
|
+
fd = null;
|
|
26804
|
+
fs10.chmodSync(tempPath, 384);
|
|
26805
|
+
fs10.renameSync(tempPath, filePath);
|
|
26806
|
+
} finally {
|
|
26807
|
+
if (fd !== null) {
|
|
26808
|
+
try {
|
|
26809
|
+
fs10.closeSync(fd);
|
|
26810
|
+
} catch {}
|
|
26811
|
+
}
|
|
26812
|
+
try {
|
|
26813
|
+
fs10.unlinkSync(tempPath);
|
|
26814
|
+
} catch {}
|
|
26815
|
+
}
|
|
26816
|
+
}
|
|
26817
|
+
function scrub(value) {
|
|
26818
|
+
return (value instanceof Error ? value.message : String(value)).replace(/\b(?:cmk|cmt)_[A-Za-z0-9_-]+\b/g, "[redacted-token]").replace(/[\r\n]+/g, " ").slice(0, 1024);
|
|
26819
|
+
}
|
|
26820
|
+
function appendUpdateLog(baseDir, machineId, event, fields = {}) {
|
|
26821
|
+
const filePath = updateLogPath(baseDir, machineId);
|
|
26822
|
+
ensurePrivateDir(path11.dirname(filePath));
|
|
26823
|
+
try {
|
|
26824
|
+
if (fs10.existsSync(filePath) && fs10.statSync(filePath).size > UPDATE_LOG_MAX_BYTES) {
|
|
26825
|
+
const data = fs10.readFileSync(filePath);
|
|
26826
|
+
fs10.writeFileSync(filePath, data.subarray(Math.max(0, data.length - UPDATE_LOG_KEEP_BYTES)), { mode: 384 });
|
|
26827
|
+
}
|
|
26828
|
+
const safeFields = Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, scrub(value)]));
|
|
26829
|
+
fs10.appendFileSync(filePath, `${JSON.stringify({ time: new Date().toISOString(), event, ...safeFields })}
|
|
26830
|
+
`, { mode: 384 });
|
|
26831
|
+
fs10.chmodSync(filePath, 384);
|
|
26832
|
+
} catch {}
|
|
26833
|
+
}
|
|
26834
|
+
function pidTuple(value) {
|
|
26835
|
+
if (!value || typeof value.machineId !== "string" || typeof value.startedAt !== "string" || typeof value.ownerToken !== "string")
|
|
26836
|
+
return null;
|
|
26837
|
+
return {
|
|
26838
|
+
pid: value.pid,
|
|
26839
|
+
machineId: value.machineId,
|
|
26840
|
+
startedAt: value.startedAt,
|
|
26841
|
+
ownerToken: value.ownerToken
|
|
26842
|
+
};
|
|
26843
|
+
}
|
|
26844
|
+
function tuplesEqual(value, expected) {
|
|
26845
|
+
return value?.pid === expected.pid && value.machineId === expected.machineId && value.startedAt === expected.startedAt && value.ownerToken === expected.ownerToken;
|
|
26846
|
+
}
|
|
26847
|
+
function readIntent(baseDir, machineId) {
|
|
26848
|
+
const filePath = intentPath(baseDir, machineId);
|
|
26849
|
+
const stat = fs10.lstatSync(filePath);
|
|
26850
|
+
if (!stat.isFile())
|
|
26851
|
+
throw new Error("unsafe daemon update intent type");
|
|
26852
|
+
fs10.chmodSync(filePath, 384);
|
|
26853
|
+
const value = JSON.parse(fs10.readFileSync(filePath, "utf8"));
|
|
26854
|
+
if (value.schemaVersion !== UPDATE_INTENT_SCHEMA_VERSION || typeof value.requestId !== "string" || !REQUEST_ID_PATTERN.test(value.requestId) || !Number.isInteger(value.pid) || (value.pid ?? 0) <= 0 || typeof value.machineId !== "string" || typeof value.startedAt !== "string" || typeof value.ownerToken !== "string")
|
|
26855
|
+
throw new Error("invalid daemon update intent");
|
|
26856
|
+
return value;
|
|
26857
|
+
}
|
|
26858
|
+
function removeIntentIfMatches(baseDir, machineId, requestId) {
|
|
26859
|
+
try {
|
|
26860
|
+
const current = readIntent(baseDir, machineId);
|
|
26861
|
+
if (current.requestId === requestId)
|
|
26862
|
+
fs10.unlinkSync(intentPath(baseDir, machineId));
|
|
26863
|
+
} catch {}
|
|
26864
|
+
}
|
|
26865
|
+
function npmExecPath(explicit) {
|
|
26866
|
+
const value = explicit ?? process.env.npm_execpath;
|
|
26867
|
+
if (!value || !path11.isAbsolute(value) || !fs10.existsSync(value)) {
|
|
26868
|
+
throw new Error("npm launch context unavailable; daemon remains online");
|
|
26869
|
+
}
|
|
26870
|
+
return value;
|
|
26871
|
+
}
|
|
26872
|
+
function readTestPackageMap(baseDir, machineId) {
|
|
26873
|
+
if (true)
|
|
26874
|
+
return null;
|
|
26875
|
+
const filePath = updatePackageMapPath(baseDir, machineId);
|
|
26876
|
+
if (!fs10.existsSync(filePath))
|
|
26877
|
+
return null;
|
|
26878
|
+
const stat = fs10.lstatSync(filePath);
|
|
26879
|
+
if (!stat.isFile())
|
|
26880
|
+
throw new Error("unsafe daemon update package map type");
|
|
26881
|
+
if (process.platform !== "win32" && (stat.mode & 63) !== 0) {
|
|
26882
|
+
throw new Error("daemon update package map must be mode 0600");
|
|
26883
|
+
}
|
|
26884
|
+
const parsed = JSON.parse(fs10.readFileSync(filePath, "utf8"));
|
|
26885
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
26886
|
+
throw new Error("invalid daemon update package map");
|
|
26887
|
+
const result = {};
|
|
26888
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
26889
|
+
if (key !== "latest" && !parseReleaseVersion(key))
|
|
26890
|
+
throw new Error("invalid daemon update package map key");
|
|
26891
|
+
if (typeof value !== "string" || !path11.isAbsolute(value))
|
|
26892
|
+
throw new Error("invalid daemon update package map value");
|
|
26893
|
+
result[key] = value;
|
|
26894
|
+
}
|
|
26895
|
+
return result;
|
|
26896
|
+
}
|
|
26897
|
+
function packageSpec(baseDir, machineId, version3) {
|
|
26898
|
+
const mapped = readTestPackageMap(baseDir, machineId)?.[version3];
|
|
26899
|
+
if (mapped)
|
|
26900
|
+
return mapped;
|
|
26901
|
+
return `@alook/daemon@${version3}`;
|
|
26902
|
+
}
|
|
26903
|
+
function fixedNpmArgs(args) {
|
|
26904
|
+
return [
|
|
26905
|
+
args.npmPath,
|
|
26906
|
+
"exec",
|
|
26907
|
+
"--yes",
|
|
26908
|
+
`--package=${args.packageSpec}`,
|
|
26909
|
+
"--",
|
|
26910
|
+
"alook-daemon",
|
|
26911
|
+
"daemon",
|
|
26912
|
+
args.command,
|
|
26913
|
+
"--id",
|
|
26914
|
+
args.machineId,
|
|
26915
|
+
"--base-dir",
|
|
26916
|
+
args.baseDir,
|
|
26917
|
+
"--request-id",
|
|
26918
|
+
args.requestId
|
|
26919
|
+
];
|
|
26920
|
+
}
|
|
26921
|
+
function openUpdateLog(baseDir, machineId) {
|
|
26922
|
+
appendUpdateLog(baseDir, machineId, "helper_spawn_requested");
|
|
26923
|
+
const filePath = updateLogPath(baseDir, machineId);
|
|
26924
|
+
const fd = fs10.openSync(filePath, "a", 384);
|
|
26925
|
+
fs10.chmodSync(filePath, 384);
|
|
26926
|
+
return fd;
|
|
26927
|
+
}
|
|
26928
|
+
function createDaemonSelfUpdateHandler(context, deps = {}) {
|
|
26929
|
+
let updateInFlight = null;
|
|
26930
|
+
return () => {
|
|
26931
|
+
if (updateInFlight && updateInFlight.exitCode === null && updateInFlight.signalCode === null)
|
|
26932
|
+
return;
|
|
26933
|
+
let requestId = "";
|
|
26934
|
+
try {
|
|
26935
|
+
const current = pidTuple(readPidFile(pidfilePathById(context.baseDir, context.machineId)));
|
|
26936
|
+
if (!current || current.pid !== context.pid || current.machineId !== context.machineId || current.startedAt !== context.startedAt || current.ownerToken !== context.ownerToken)
|
|
26937
|
+
throw new Error("daemon ownership changed before update launch");
|
|
26938
|
+
const npmPath = npmExecPath(deps.npmExecPath);
|
|
26939
|
+
requestId = crypto4.randomUUID();
|
|
26940
|
+
const intent = {
|
|
26941
|
+
schemaVersion: UPDATE_INTENT_SCHEMA_VERSION,
|
|
26942
|
+
requestId,
|
|
26943
|
+
...current
|
|
26944
|
+
};
|
|
26945
|
+
writePrivateJsonAtomic(intentPath(context.baseDir, context.machineId), intent);
|
|
26946
|
+
const logFd = openUpdateLog(context.baseDir, context.machineId);
|
|
26947
|
+
try {
|
|
26948
|
+
const child = (deps.spawnProcess ?? spawn2)(process.execPath, fixedNpmArgs({
|
|
26949
|
+
npmPath,
|
|
26950
|
+
packageSpec: packageSpec(context.baseDir, context.machineId, "latest"),
|
|
26951
|
+
command: "replace",
|
|
26952
|
+
machineId: context.machineId,
|
|
26953
|
+
baseDir: context.baseDir,
|
|
26954
|
+
requestId
|
|
26955
|
+
}), {
|
|
26956
|
+
detached: true,
|
|
26957
|
+
shell: false,
|
|
26958
|
+
stdio: ["ignore", logFd, logFd]
|
|
26959
|
+
});
|
|
26960
|
+
updateInFlight = child;
|
|
26961
|
+
const clear = (event, fields) => {
|
|
26962
|
+
appendUpdateLog(context.baseDir, context.machineId, event, fields);
|
|
26963
|
+
if (updateInFlight === child)
|
|
26964
|
+
updateInFlight = null;
|
|
26965
|
+
removeIntentIfMatches(context.baseDir, context.machineId, requestId);
|
|
26966
|
+
};
|
|
26967
|
+
child.once("exit", (code, signal) => clear("helper_exited", { code, signal }));
|
|
26968
|
+
child.once("error", (error51) => clear("helper_process_error", { error: error51 }));
|
|
26969
|
+
child.unref();
|
|
26970
|
+
deps.logger?.info("daemon self-update helper launched", { machineId: context.machineId });
|
|
26971
|
+
} finally {
|
|
26972
|
+
fs10.closeSync(logFd);
|
|
26973
|
+
}
|
|
26974
|
+
} catch (error51) {
|
|
26975
|
+
if (requestId)
|
|
26976
|
+
removeIntentIfMatches(context.baseDir, context.machineId, requestId);
|
|
26977
|
+
appendUpdateLog(context.baseDir, context.machineId, "helper_spawn_failed", { error: error51 });
|
|
26978
|
+
deps.logger?.warn("daemon self-update helper launch failed", { machineId: context.machineId, error: scrub(error51) });
|
|
26979
|
+
}
|
|
26980
|
+
};
|
|
26981
|
+
}
|
|
26982
|
+
function currentPidTuple(baseDir, machineId) {
|
|
26983
|
+
return pidTuple(readPidFile(pidfilePathById(baseDir, machineId)));
|
|
26984
|
+
}
|
|
26985
|
+
function removeOldPidfileIfMatches(baseDir, intent) {
|
|
26986
|
+
const filePath = pidfilePathById(baseDir, intent.machineId);
|
|
26987
|
+
if (!tuplesEqual(currentPidTuple(baseDir, intent.machineId), intent))
|
|
26988
|
+
return;
|
|
26989
|
+
try {
|
|
26990
|
+
fs10.unlinkSync(filePath);
|
|
26991
|
+
} catch {}
|
|
26992
|
+
}
|
|
26993
|
+
async function runPinnedResume(args) {
|
|
26994
|
+
const fd = openUpdateLog(args.baseDir, args.machineId);
|
|
26995
|
+
try {
|
|
26996
|
+
const child = spawn2(process.execPath, fixedNpmArgs({
|
|
26997
|
+
npmPath: args.npmPath,
|
|
26998
|
+
packageSpec: packageSpec(args.baseDir, args.machineId, args.version),
|
|
26999
|
+
command: "resume",
|
|
27000
|
+
machineId: args.machineId,
|
|
27001
|
+
baseDir: args.baseDir,
|
|
27002
|
+
requestId: args.requestId
|
|
27003
|
+
}), {
|
|
27004
|
+
shell: false,
|
|
27005
|
+
stdio: ["ignore", fd, fd]
|
|
27006
|
+
});
|
|
27007
|
+
await new Promise((resolve4, reject) => {
|
|
27008
|
+
child.once("error", reject);
|
|
27009
|
+
child.once("exit", (code, signal) => {
|
|
27010
|
+
if (code === 0)
|
|
27011
|
+
resolve4();
|
|
27012
|
+
else
|
|
27013
|
+
reject(new Error(`rollback resume exited ${signal ?? code}`));
|
|
27014
|
+
});
|
|
27015
|
+
});
|
|
27016
|
+
} finally {
|
|
27017
|
+
fs10.closeSync(fd);
|
|
27018
|
+
}
|
|
27019
|
+
}
|
|
27020
|
+
async function daemonReplace(opts) {
|
|
27021
|
+
if (!REQUEST_ID_PATTERN.test(opts.requestId))
|
|
27022
|
+
throw new Error("invalid replacement request id");
|
|
27023
|
+
const baseDir = opts.baseDir ?? process.env.ALOOK_DATA_DIR;
|
|
27024
|
+
if (!baseDir)
|
|
27025
|
+
throw new Error("daemon replace requires --base-dir");
|
|
27026
|
+
const intent = readIntent(baseDir, opts.id);
|
|
27027
|
+
if (intent.machineId !== opts.id || intent.requestId !== opts.requestId) {
|
|
27028
|
+
throw new Error("daemon update intent mismatch");
|
|
27029
|
+
}
|
|
27030
|
+
const launch = readDaemonLaunchRecord(baseDir, opts.id);
|
|
27031
|
+
const currentVersion = readDaemonVersion();
|
|
27032
|
+
if (!parseReleaseVersion(currentVersion) || !parseReleaseVersion(launch.daemonVersion)) {
|
|
27033
|
+
throw new Error("daemon replacement version is invalid");
|
|
27034
|
+
}
|
|
27035
|
+
if (currentVersion === launch.daemonVersion || !releaseVersionGte(currentVersion, launch.daemonVersion)) {
|
|
27036
|
+
appendUpdateLog(baseDir, opts.id, "replacement_not_newer", {
|
|
27037
|
+
currentVersion,
|
|
27038
|
+
priorVersion: launch.daemonVersion
|
|
27039
|
+
});
|
|
27040
|
+
removeIntentIfMatches(baseDir, opts.id, opts.requestId);
|
|
27041
|
+
return;
|
|
27042
|
+
}
|
|
27043
|
+
if (!tuplesEqual(currentPidTuple(baseDir, opts.id), intent)) {
|
|
27044
|
+
removeIntentIfMatches(baseDir, opts.id, opts.requestId);
|
|
27045
|
+
throw new Error("daemon ownership changed before replacement");
|
|
27046
|
+
}
|
|
27047
|
+
const npmPath = npmExecPath();
|
|
27048
|
+
const acquired = acquireDaemonReplacementLock({ baseDir, machineId: opts.id, requestId: opts.requestId });
|
|
27049
|
+
let oldStopped = false;
|
|
27050
|
+
try {
|
|
27051
|
+
if (!tuplesEqual(currentPidTuple(baseDir, opts.id), intent)) {
|
|
27052
|
+
throw new Error("daemon ownership changed after replacement lock");
|
|
27053
|
+
}
|
|
27054
|
+
appendUpdateLog(baseDir, opts.id, "replacement_started", {
|
|
27055
|
+
priorVersion: launch.daemonVersion,
|
|
27056
|
+
nextVersion: currentVersion
|
|
27057
|
+
});
|
|
27058
|
+
await stopExactDaemonPid(intent.pid);
|
|
27059
|
+
oldStopped = true;
|
|
27060
|
+
removeOldPidfileIfMatches(baseDir, intent);
|
|
27061
|
+
try {
|
|
27062
|
+
await daemonResume({ id: opts.id, baseDir, requestId: opts.requestId });
|
|
27063
|
+
appendUpdateLog(baseDir, opts.id, "replacement_ready", { version: currentVersion });
|
|
27064
|
+
} catch (error51) {
|
|
27065
|
+
appendUpdateLog(baseDir, opts.id, "replacement_start_failed", { error: error51 });
|
|
27066
|
+
await runPinnedResume({
|
|
27067
|
+
npmPath,
|
|
27068
|
+
version: launch.daemonVersion,
|
|
27069
|
+
machineId: opts.id,
|
|
27070
|
+
baseDir,
|
|
27071
|
+
requestId: opts.requestId
|
|
27072
|
+
});
|
|
27073
|
+
appendUpdateLog(baseDir, opts.id, "rollback_ready", { version: launch.daemonVersion });
|
|
27074
|
+
}
|
|
27075
|
+
} catch (error51) {
|
|
27076
|
+
appendUpdateLog(baseDir, opts.id, oldStopped ? "replacement_terminal_failure" : "replacement_aborted", { error: error51 });
|
|
27077
|
+
throw error51;
|
|
27078
|
+
} finally {
|
|
27079
|
+
removeIntentIfMatches(baseDir, opts.id, opts.requestId);
|
|
27080
|
+
removeReplacementLockIfMatches(acquired.path, acquired.lock);
|
|
27081
|
+
}
|
|
27082
|
+
}
|
|
27083
|
+
|
|
26420
27084
|
// src/cli/daemonRunner.ts
|
|
26421
27085
|
var CAPABILITIES = ["send", "read", "mentions", "tasks", "reactions", "server", "channels", "knowledge", "attach", "friend"];
|
|
26422
27086
|
var DAEMON_LOG_MAX_BYTES = 8 * 1024 * 1024;
|
|
@@ -26521,9 +27185,9 @@ async function buildRunnerDiagnosticBundle(args) {
|
|
|
26521
27185
|
});
|
|
26522
27186
|
}
|
|
26523
27187
|
function createDaemonProcessLogger(daemonDir, foreground) {
|
|
26524
|
-
|
|
26525
|
-
|
|
26526
|
-
const logPath =
|
|
27188
|
+
fs11.mkdirSync(daemonDir, { recursive: true, mode: 448 });
|
|
27189
|
+
fs11.chmodSync(daemonDir, 448);
|
|
27190
|
+
const logPath = path12.join(daemonDir, "daemon.log");
|
|
26527
27191
|
let warnedOversize = false;
|
|
26528
27192
|
const sink = createRotatingFileSink(logPath, DAEMON_LOG_MAX_BYTES, {
|
|
26529
27193
|
mode: 384,
|
|
@@ -26597,7 +27261,7 @@ async function runPreparedDaemon(prepared, opts) {
|
|
|
26597
27261
|
return;
|
|
26598
27262
|
const marker = process.env.ALOOK_DAEMON_TEST_SHUTDOWN_MARKER;
|
|
26599
27263
|
if (marker)
|
|
26600
|
-
|
|
27264
|
+
fs11.writeFileSync(marker, String(process.pid), { mode: 384 });
|
|
26601
27265
|
await new Promise((resolve4) => setTimeout(resolve4, delayMs));
|
|
26602
27266
|
};
|
|
26603
27267
|
const stopDaemon = async () => {
|
|
@@ -26645,6 +27309,13 @@ async function runPreparedDaemon(prepared, opts) {
|
|
|
26645
27309
|
});
|
|
26646
27310
|
try {
|
|
26647
27311
|
logDaemonStartup(log2, prepared);
|
|
27312
|
+
const handleSelfUpdate = createDaemonSelfUpdateHandler({
|
|
27313
|
+
machineId: prepared.machineId,
|
|
27314
|
+
baseDir: prepared.baseDir,
|
|
27315
|
+
pid: process.pid,
|
|
27316
|
+
startedAt: prepared.startedAt,
|
|
27317
|
+
ownerToken: prepared.ownerToken
|
|
27318
|
+
}, { logger: log2 });
|
|
26648
27319
|
daemon = await createDaemon({
|
|
26649
27320
|
machineKey: prepared.machineKey,
|
|
26650
27321
|
serverUrl: prepared.serverUrl,
|
|
@@ -26670,6 +27341,7 @@ async function runPreparedDaemon(prepared, opts) {
|
|
|
26670
27341
|
osRelease: prepared.osRelease,
|
|
26671
27342
|
daemonVersion: prepared.daemonVersion,
|
|
26672
27343
|
logger: log2,
|
|
27344
|
+
handleSelfUpdate,
|
|
26673
27345
|
handleDiagnosticCommand,
|
|
26674
27346
|
reportDiagnosticFailure,
|
|
26675
27347
|
onDiagnosticSources: ({ fsmTraceSource, statusFilePath }) => {
|
|
@@ -26734,7 +27406,7 @@ async function runPreparedDaemon(prepared, opts) {
|
|
|
26734
27406
|
}
|
|
26735
27407
|
|
|
26736
27408
|
// src/cli/daemonStart.ts
|
|
26737
|
-
import { spawn as
|
|
27409
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
26738
27410
|
var STOP_GRACE_MS = 5000;
|
|
26739
27411
|
var STOP_KILL_GRACE_MS = 2000;
|
|
26740
27412
|
var POLL_MS2 = 100;
|
|
@@ -26744,16 +27416,16 @@ var RUNNER_KILL_GRACE_MS = 2000;
|
|
|
26744
27416
|
var MACHINE_ID_PATTERN = /^cm_[A-Za-z0-9_-]{8,64}$/;
|
|
26745
27417
|
var LEGACY_DAEMON_ID_PATTERN = /^[a-f0-9]{12}$/;
|
|
26746
27418
|
function resolveDefaultBaseDir() {
|
|
26747
|
-
const root = process.env.ALOOK_PROJECT_ROOT ||
|
|
26748
|
-
return
|
|
27419
|
+
const root = process.env.ALOOK_PROJECT_ROOT || path13.join(homedir4(), ".alook");
|
|
27420
|
+
return path13.join(root, "daemon");
|
|
26749
27421
|
}
|
|
26750
27422
|
var DEFAULT_BASE_DIR = resolveDefaultBaseDir();
|
|
26751
27423
|
var log2 = createLogger2({ header: "@alook/daemon" });
|
|
26752
27424
|
function daemonsDir(baseDir) {
|
|
26753
|
-
return
|
|
27425
|
+
return path13.join(baseDir, "daemons");
|
|
26754
27426
|
}
|
|
26755
27427
|
function daemonDirById(baseDir, id) {
|
|
26756
|
-
return
|
|
27428
|
+
return path13.join(daemonsDir(baseDir), validateDaemonId(id));
|
|
26757
27429
|
}
|
|
26758
27430
|
function validateMachineId(machineId) {
|
|
26759
27431
|
if (!MACHINE_ID_PATTERN.test(machineId))
|
|
@@ -26769,10 +27441,10 @@ function validateDaemonId(id) {
|
|
|
26769
27441
|
return id;
|
|
26770
27442
|
}
|
|
26771
27443
|
function pidfilePathById(baseDir, id) {
|
|
26772
|
-
return
|
|
27444
|
+
return path13.join(daemonDirById(baseDir, id), "daemon.pid");
|
|
26773
27445
|
}
|
|
26774
27446
|
function statusFilePathById(baseDir, id) {
|
|
26775
|
-
return
|
|
27447
|
+
return path13.join(daemonDirById(baseDir, id), "status.json");
|
|
26776
27448
|
}
|
|
26777
27449
|
function isProcessAlive(pid) {
|
|
26778
27450
|
try {
|
|
@@ -26810,64 +27482,88 @@ function parsePidFileContent(raw) {
|
|
|
26810
27482
|
return null;
|
|
26811
27483
|
}
|
|
26812
27484
|
function readPidFile(filePath) {
|
|
26813
|
-
if (!
|
|
27485
|
+
if (!fs12.existsSync(filePath))
|
|
26814
27486
|
return null;
|
|
26815
27487
|
try {
|
|
26816
|
-
return parsePidFileContent(
|
|
27488
|
+
return parsePidFileContent(fs12.readFileSync(filePath, "utf8"));
|
|
26817
27489
|
} catch {}
|
|
26818
27490
|
return null;
|
|
26819
27491
|
}
|
|
26820
|
-
function
|
|
26821
|
-
|
|
26822
|
-
|
|
27492
|
+
function ensurePrivateDir2(dir) {
|
|
27493
|
+
fs12.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
27494
|
+
fs12.chmodSync(dir, 448);
|
|
26823
27495
|
}
|
|
26824
27496
|
function syncDirectory(dir) {
|
|
26825
27497
|
if (process.platform === "win32")
|
|
26826
27498
|
return;
|
|
26827
|
-
const fd =
|
|
27499
|
+
const fd = fs12.openSync(dir, "r");
|
|
26828
27500
|
try {
|
|
26829
|
-
|
|
27501
|
+
fs12.fsyncSync(fd);
|
|
26830
27502
|
} finally {
|
|
26831
|
-
|
|
27503
|
+
fs12.closeSync(fd);
|
|
26832
27504
|
}
|
|
26833
27505
|
}
|
|
26834
27506
|
function writeExclusive(filePath, value) {
|
|
26835
|
-
const dir =
|
|
26836
|
-
|
|
26837
|
-
const tempPath =
|
|
26838
|
-
let fd =
|
|
27507
|
+
const dir = path13.dirname(filePath);
|
|
27508
|
+
ensurePrivateDir2(dir);
|
|
27509
|
+
const tempPath = path13.join(dir, `.${path13.basename(filePath)}.${process.pid}.${crypto5.randomBytes(12).toString("hex")}.tmp`);
|
|
27510
|
+
let fd = fs12.openSync(tempPath, "wx", 384);
|
|
27511
|
+
try {
|
|
27512
|
+
fs12.writeFileSync(fd, JSON.stringify(value));
|
|
27513
|
+
fs12.fsyncSync(fd);
|
|
27514
|
+
fs12.closeSync(fd);
|
|
27515
|
+
fd = null;
|
|
27516
|
+
fs12.chmodSync(tempPath, 384);
|
|
27517
|
+
fs12.linkSync(tempPath, filePath);
|
|
27518
|
+
syncDirectory(dir);
|
|
27519
|
+
} finally {
|
|
27520
|
+
if (fd !== null) {
|
|
27521
|
+
try {
|
|
27522
|
+
fs12.closeSync(fd);
|
|
27523
|
+
} catch {}
|
|
27524
|
+
}
|
|
27525
|
+
try {
|
|
27526
|
+
fs12.unlinkSync(tempPath);
|
|
27527
|
+
} catch {}
|
|
27528
|
+
}
|
|
27529
|
+
}
|
|
27530
|
+
function writePrivateJsonAtomic2(filePath, value) {
|
|
27531
|
+
const dir = path13.dirname(filePath);
|
|
27532
|
+
ensurePrivateDir2(dir);
|
|
27533
|
+
const tempPath = path13.join(dir, `.${path13.basename(filePath)}.${process.pid}.${crypto5.randomBytes(12).toString("hex")}.tmp`);
|
|
27534
|
+
let fd = fs12.openSync(tempPath, "wx", 384);
|
|
26839
27535
|
try {
|
|
26840
|
-
|
|
26841
|
-
|
|
26842
|
-
|
|
27536
|
+
fs12.writeFileSync(fd, JSON.stringify(value));
|
|
27537
|
+
fs12.fsyncSync(fd);
|
|
27538
|
+
fs12.closeSync(fd);
|
|
26843
27539
|
fd = null;
|
|
26844
|
-
|
|
26845
|
-
|
|
27540
|
+
fs12.chmodSync(tempPath, 384);
|
|
27541
|
+
fs12.renameSync(tempPath, filePath);
|
|
26846
27542
|
syncDirectory(dir);
|
|
26847
27543
|
} finally {
|
|
26848
27544
|
if (fd !== null) {
|
|
26849
27545
|
try {
|
|
26850
|
-
|
|
27546
|
+
fs12.closeSync(fd);
|
|
26851
27547
|
} catch {}
|
|
26852
27548
|
}
|
|
26853
27549
|
try {
|
|
26854
|
-
|
|
27550
|
+
fs12.unlinkSync(tempPath);
|
|
26855
27551
|
} catch {}
|
|
26856
27552
|
}
|
|
26857
27553
|
}
|
|
26858
27554
|
function secureExistingFile(filePath) {
|
|
26859
|
-
if (!
|
|
27555
|
+
if (!fs12.existsSync(filePath))
|
|
26860
27556
|
return;
|
|
26861
|
-
const stat =
|
|
27557
|
+
const stat = fs12.lstatSync(filePath);
|
|
26862
27558
|
if (!stat.isFile())
|
|
26863
27559
|
throw new Error("unsafe daemon ownership file type");
|
|
26864
|
-
|
|
27560
|
+
fs12.chmodSync(filePath, 384);
|
|
26865
27561
|
}
|
|
26866
27562
|
function removeOwnedFile(filePath, pid, ownerToken) {
|
|
26867
27563
|
try {
|
|
26868
27564
|
const content = readPidFile(filePath);
|
|
26869
27565
|
if (content?.pid === pid && content.ownerToken === ownerToken) {
|
|
26870
|
-
|
|
27566
|
+
fs12.unlinkSync(filePath);
|
|
26871
27567
|
}
|
|
26872
27568
|
} catch {}
|
|
26873
27569
|
}
|
|
@@ -26879,7 +27575,7 @@ function removePidFileIfMatches(filePath, expected) {
|
|
|
26879
27575
|
try {
|
|
26880
27576
|
const current = readPidFile(filePath);
|
|
26881
27577
|
if (current?.pid === expected.pid && current.key === expected.key)
|
|
26882
|
-
|
|
27578
|
+
fs12.unlinkSync(filePath);
|
|
26883
27579
|
} catch {}
|
|
26884
27580
|
}
|
|
26885
27581
|
function malformedPidHint(raw) {
|
|
@@ -26890,7 +27586,7 @@ function malformedPidHint(raw) {
|
|
|
26890
27586
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
26891
27587
|
}
|
|
26892
27588
|
function clearStaleOwnership(filePath, liveError) {
|
|
26893
|
-
if (!
|
|
27589
|
+
if (!fs12.existsSync(filePath))
|
|
26894
27590
|
return;
|
|
26895
27591
|
const valid = readPidFile(filePath);
|
|
26896
27592
|
if (valid) {
|
|
@@ -26902,9 +27598,9 @@ function clearStaleOwnership(filePath, liveError) {
|
|
|
26902
27598
|
let raw;
|
|
26903
27599
|
let before;
|
|
26904
27600
|
try {
|
|
26905
|
-
before =
|
|
26906
|
-
raw =
|
|
26907
|
-
const afterRead =
|
|
27601
|
+
before = fs12.statSync(filePath);
|
|
27602
|
+
raw = fs12.readFileSync(filePath, "utf8");
|
|
27603
|
+
const afterRead = fs12.statSync(filePath);
|
|
26908
27604
|
if (afterRead.dev !== before.dev || afterRead.ino !== before.ino || afterRead.size !== before.size || afterRead.mtimeMs !== before.mtimeMs)
|
|
26909
27605
|
return;
|
|
26910
27606
|
} catch {
|
|
@@ -26914,28 +27610,112 @@ function clearStaleOwnership(filePath, liveError) {
|
|
|
26914
27610
|
if (pid && isProcessAlive(pid))
|
|
26915
27611
|
throw liveError(pid);
|
|
26916
27612
|
try {
|
|
26917
|
-
const current =
|
|
27613
|
+
const current = fs12.statSync(filePath);
|
|
26918
27614
|
if (current.dev !== before.dev || current.ino !== before.ino || current.size !== before.size || current.mtimeMs !== before.mtimeMs)
|
|
26919
27615
|
return;
|
|
26920
|
-
|
|
27616
|
+
fs12.unlinkSync(filePath);
|
|
26921
27617
|
} catch {}
|
|
26922
27618
|
}
|
|
27619
|
+
function replacementLockPathById(baseDir, machineId) {
|
|
27620
|
+
return path13.join(daemonDirById(baseDir, machineId), "daemon.replace.lock");
|
|
27621
|
+
}
|
|
27622
|
+
function readReplacementLock(filePath) {
|
|
27623
|
+
if (!fs12.existsSync(filePath))
|
|
27624
|
+
return null;
|
|
27625
|
+
try {
|
|
27626
|
+
const stat = fs12.lstatSync(filePath);
|
|
27627
|
+
if (!stat.isFile())
|
|
27628
|
+
throw new Error("unsafe daemon replacement lock type");
|
|
27629
|
+
fs12.chmodSync(filePath, 384);
|
|
27630
|
+
const content = JSON.parse(fs12.readFileSync(filePath, "utf8"));
|
|
27631
|
+
if (!Number.isInteger(content.pid) || (content.pid ?? 0) <= 0 || typeof content.machineId !== "string" || typeof content.startedAt !== "string" || typeof content.ownerToken !== "string" || typeof content.requestId !== "string")
|
|
27632
|
+
return null;
|
|
27633
|
+
return content;
|
|
27634
|
+
} catch (error51) {
|
|
27635
|
+
if (error51 instanceof SyntaxError)
|
|
27636
|
+
return null;
|
|
27637
|
+
throw error51;
|
|
27638
|
+
}
|
|
27639
|
+
}
|
|
27640
|
+
function replacementLocksEqual(a, b) {
|
|
27641
|
+
return a.pid === b.pid && a.machineId === b.machineId && a.startedAt === b.startedAt && a.ownerToken === b.ownerToken && a.requestId === b.requestId;
|
|
27642
|
+
}
|
|
27643
|
+
function removeReplacementLockIfMatches(filePath, expected) {
|
|
27644
|
+
try {
|
|
27645
|
+
const current = readReplacementLock(filePath);
|
|
27646
|
+
if (current && replacementLocksEqual(current, expected))
|
|
27647
|
+
fs12.unlinkSync(filePath);
|
|
27648
|
+
} catch {}
|
|
27649
|
+
}
|
|
27650
|
+
function checkReplacementLock(baseDir, machineId, resumeRequestId) {
|
|
27651
|
+
const lockPath = replacementLockPathById(baseDir, machineId);
|
|
27652
|
+
if (!fs12.existsSync(lockPath))
|
|
27653
|
+
return;
|
|
27654
|
+
const lock = readReplacementLock(lockPath);
|
|
27655
|
+
if (!lock) {
|
|
27656
|
+
let before;
|
|
27657
|
+
let raw;
|
|
27658
|
+
try {
|
|
27659
|
+
before = fs12.statSync(lockPath);
|
|
27660
|
+
raw = fs12.readFileSync(lockPath, "utf8");
|
|
27661
|
+
const afterRead = fs12.statSync(lockPath);
|
|
27662
|
+
if (afterRead.dev !== before.dev || afterRead.ino !== before.ino || afterRead.size !== before.size || afterRead.mtimeMs !== before.mtimeMs)
|
|
27663
|
+
throw new Error(`daemon '${machineId}' replacement lock changed during recovery`);
|
|
27664
|
+
} catch (error51) {
|
|
27665
|
+
if (error51?.code === "ENOENT")
|
|
27666
|
+
return;
|
|
27667
|
+
throw error51;
|
|
27668
|
+
}
|
|
27669
|
+
const pid = malformedPidHint(raw);
|
|
27670
|
+
if (pid && isProcessAlive(pid)) {
|
|
27671
|
+
throw new Error(`daemon '${machineId}' replacement lock is malformed but owned by live pid ${pid}`);
|
|
27672
|
+
}
|
|
27673
|
+
try {
|
|
27674
|
+
const current = fs12.statSync(lockPath);
|
|
27675
|
+
if (current.dev === before.dev && current.ino === before.ino && current.size === before.size && current.mtimeMs === before.mtimeMs)
|
|
27676
|
+
fs12.unlinkSync(lockPath);
|
|
27677
|
+
} catch {}
|
|
27678
|
+
return;
|
|
27679
|
+
}
|
|
27680
|
+
if (!isProcessAlive(lock.pid)) {
|
|
27681
|
+
removeReplacementLockIfMatches(lockPath, lock);
|
|
27682
|
+
return;
|
|
27683
|
+
}
|
|
27684
|
+
if (resumeRequestId && lock.requestId === resumeRequestId)
|
|
27685
|
+
return;
|
|
27686
|
+
throw new Error(`daemon '${machineId}' replacement already in progress (pid ${lock.pid})`);
|
|
27687
|
+
}
|
|
27688
|
+
function acquireDaemonReplacementLock(args) {
|
|
27689
|
+
const lockPath = replacementLockPathById(args.baseDir, args.machineId);
|
|
27690
|
+
ensurePrivateDir2(path13.dirname(lockPath));
|
|
27691
|
+
checkReplacementLock(args.baseDir, args.machineId);
|
|
27692
|
+
const lock = {
|
|
27693
|
+
pid: process.pid,
|
|
27694
|
+
machineId: args.machineId,
|
|
27695
|
+
startedAt: new Date().toISOString(),
|
|
27696
|
+
ownerToken: crypto5.randomBytes(24).toString("base64url"),
|
|
27697
|
+
requestId: args.requestId
|
|
27698
|
+
};
|
|
27699
|
+
writeExclusive(lockPath, lock);
|
|
27700
|
+
return { path: lockPath, lock };
|
|
27701
|
+
}
|
|
26923
27702
|
function coarseStartLockPath(baseDir) {
|
|
26924
|
-
return
|
|
27703
|
+
return path13.join(daemonsDir(baseDir), ".start.lock");
|
|
26925
27704
|
}
|
|
26926
27705
|
function acquireCoarseLock(baseDir, ownerToken) {
|
|
26927
27706
|
const lf = coarseStartLockPath(baseDir);
|
|
26928
|
-
|
|
27707
|
+
ensurePrivateDir2(path13.dirname(lf));
|
|
26929
27708
|
secureExistingFile(lf);
|
|
26930
27709
|
clearStaleOwnership(lf, (pid) => new Error(`another daemon start is in progress on this machine (pid ${pid})`));
|
|
26931
27710
|
writeExclusive(lf, { pid: process.pid, machineId: "coarse", startedAt: new Date().toISOString(), ownerToken });
|
|
26932
27711
|
return lf;
|
|
26933
27712
|
}
|
|
26934
|
-
function acquireLaunchLock(baseDir, machineId, ownerToken) {
|
|
27713
|
+
function acquireLaunchLock(baseDir, machineId, ownerToken, resumeRequestId) {
|
|
26935
27714
|
const daemonDir = daemonDirById(baseDir, machineId);
|
|
26936
|
-
|
|
26937
|
-
const lockPath =
|
|
27715
|
+
ensurePrivateDir2(daemonDir);
|
|
27716
|
+
const lockPath = path13.join(daemonDir, "daemon.launch.lock");
|
|
26938
27717
|
const finalPath = pidfilePathById(baseDir, machineId);
|
|
27718
|
+
checkReplacementLock(baseDir, machineId, resumeRequestId);
|
|
26939
27719
|
secureExistingFile(lockPath);
|
|
26940
27720
|
secureExistingFile(finalPath);
|
|
26941
27721
|
clearStaleOwnership(finalPath, (pid) => new Error(`daemon '${machineId}' already running (pid ${pid})`));
|
|
@@ -26950,14 +27730,14 @@ function commitFinalPidfile(baseDir, machineId, pid, startedAt, ownerToken) {
|
|
|
26950
27730
|
}
|
|
26951
27731
|
function legacyPidfileCandidates(baseDir) {
|
|
26952
27732
|
const dir = daemonsDir(baseDir);
|
|
26953
|
-
if (!
|
|
27733
|
+
if (!fs12.existsSync(dir))
|
|
26954
27734
|
return [];
|
|
26955
27735
|
const candidates = [];
|
|
26956
|
-
for (const entry of
|
|
27736
|
+
for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
|
|
26957
27737
|
if (entry.isDirectory()) {
|
|
26958
|
-
candidates.push(
|
|
27738
|
+
candidates.push(path13.join(dir, entry.name, "daemon.pid"));
|
|
26959
27739
|
} else if (entry.isFile() && entry.name.endsWith(".pid")) {
|
|
26960
|
-
candidates.push(
|
|
27740
|
+
candidates.push(path13.join(dir, entry.name));
|
|
26961
27741
|
}
|
|
26962
27742
|
}
|
|
26963
27743
|
return candidates;
|
|
@@ -26980,7 +27760,7 @@ function reconcileLegacyMachineKeyOwnership(baseDir, machineKey) {
|
|
|
26980
27760
|
function daemonList(opts) {
|
|
26981
27761
|
const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
|
|
26982
27762
|
const dir = daemonsDir(baseDir);
|
|
26983
|
-
if (!
|
|
27763
|
+
if (!fs12.existsSync(dir))
|
|
26984
27764
|
return [];
|
|
26985
27765
|
const results = [];
|
|
26986
27766
|
const now = Date.now();
|
|
@@ -27005,10 +27785,10 @@ function daemonList(opts) {
|
|
|
27005
27785
|
}
|
|
27006
27786
|
results.push({ id, pid: data.pid, alive, agents, running, lastActiveMs });
|
|
27007
27787
|
};
|
|
27008
|
-
for (const entry of
|
|
27788
|
+
for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
|
|
27009
27789
|
if (entry.isDirectory() && isDaemonId(entry.name)) {
|
|
27010
27790
|
const id = entry.name;
|
|
27011
|
-
pushRow(id,
|
|
27791
|
+
pushRow(id, path13.join(dir, id, "daemon.pid"), path13.join(dir, id, "status.json"));
|
|
27012
27792
|
}
|
|
27013
27793
|
}
|
|
27014
27794
|
return results;
|
|
@@ -27016,10 +27796,10 @@ function daemonList(opts) {
|
|
|
27016
27796
|
var STATUS_STALE_MS = 20000;
|
|
27017
27797
|
var MISSING_STATUS = { found: false, ageMs: null, freshness: "missing", writtenAt: null, agents: [] };
|
|
27018
27798
|
function daemonStatusFromFile(statusPath, nowMs) {
|
|
27019
|
-
if (!
|
|
27799
|
+
if (!fs12.existsSync(statusPath))
|
|
27020
27800
|
return MISSING_STATUS;
|
|
27021
27801
|
try {
|
|
27022
|
-
const snap = JSON.parse(
|
|
27802
|
+
const snap = JSON.parse(fs12.readFileSync(statusPath, "utf8"));
|
|
27023
27803
|
const ageMs = nowMs - snap.writtenAt;
|
|
27024
27804
|
return {
|
|
27025
27805
|
found: true,
|
|
@@ -27034,11 +27814,11 @@ function daemonStatusFromFile(statusPath, nowMs) {
|
|
|
27034
27814
|
}
|
|
27035
27815
|
function daemonIdsWithStatus(baseDir) {
|
|
27036
27816
|
const dir = daemonsDir(baseDir);
|
|
27037
|
-
if (!
|
|
27817
|
+
if (!fs12.existsSync(dir))
|
|
27038
27818
|
return [];
|
|
27039
27819
|
const ids = [];
|
|
27040
|
-
for (const entry of
|
|
27041
|
-
if (entry.isDirectory() && isDaemonId(entry.name) &&
|
|
27820
|
+
for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
|
|
27821
|
+
if (entry.isDirectory() && isDaemonId(entry.name) && fs12.existsSync(path13.join(dir, entry.name, "status.json"))) {
|
|
27042
27822
|
ids.push(entry.name);
|
|
27043
27823
|
}
|
|
27044
27824
|
}
|
|
@@ -27059,6 +27839,30 @@ function daemonStatus(opts) {
|
|
|
27059
27839
|
}
|
|
27060
27840
|
return MISSING_STATUS;
|
|
27061
27841
|
}
|
|
27842
|
+
async function stopExactPid(pid) {
|
|
27843
|
+
try {
|
|
27844
|
+
process.kill(pid, "SIGTERM");
|
|
27845
|
+
} catch (error51) {
|
|
27846
|
+
if (isProcessAlive(pid))
|
|
27847
|
+
throw error51;
|
|
27848
|
+
}
|
|
27849
|
+
if (!await waitForPidExit(pid, STOP_GRACE_MS)) {
|
|
27850
|
+
try {
|
|
27851
|
+
process.kill(pid, "SIGKILL");
|
|
27852
|
+
} catch (error51) {
|
|
27853
|
+
if (isProcessAlive(pid))
|
|
27854
|
+
throw error51;
|
|
27855
|
+
}
|
|
27856
|
+
if (!await waitForPidExit(pid, STOP_KILL_GRACE_MS)) {
|
|
27857
|
+
throw new Error(`daemon (pid ${pid}) is still running after SIGKILL`);
|
|
27858
|
+
}
|
|
27859
|
+
}
|
|
27860
|
+
}
|
|
27861
|
+
async function stopExactDaemonPid(pid) {
|
|
27862
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
27863
|
+
throw new Error("invalid daemon pid");
|
|
27864
|
+
await stopExactPid(pid);
|
|
27865
|
+
}
|
|
27062
27866
|
async function stopByPidfile(pf, notFoundHint) {
|
|
27063
27867
|
const data = readPidFile(pf);
|
|
27064
27868
|
if (!data) {
|
|
@@ -27071,81 +27875,63 @@ async function stopByPidfile(pf, notFoundHint) {
|
|
|
27071
27875
|
return;
|
|
27072
27876
|
}
|
|
27073
27877
|
log2.info(`sending SIGTERM to daemon (pid ${data.pid})…`);
|
|
27074
|
-
|
|
27075
|
-
process.kill(data.pid, "SIGTERM");
|
|
27076
|
-
} catch (error51) {
|
|
27077
|
-
if (isProcessAlive(data.pid))
|
|
27078
|
-
throw error51;
|
|
27079
|
-
}
|
|
27080
|
-
if (!await waitForPidExit(data.pid, STOP_GRACE_MS)) {
|
|
27081
|
-
log2.error(`daemon (pid ${data.pid}) did not exit in ${STOP_GRACE_MS / 1000}s — sending SIGKILL`);
|
|
27082
|
-
try {
|
|
27083
|
-
process.kill(data.pid, "SIGKILL");
|
|
27084
|
-
} catch (error51) {
|
|
27085
|
-
if (isProcessAlive(data.pid))
|
|
27086
|
-
throw error51;
|
|
27087
|
-
}
|
|
27088
|
-
if (!await waitForPidExit(data.pid, STOP_KILL_GRACE_MS)) {
|
|
27089
|
-
throw new Error(`daemon (pid ${data.pid}) is still running after SIGKILL`);
|
|
27090
|
-
}
|
|
27091
|
-
}
|
|
27878
|
+
await stopExactPid(data.pid);
|
|
27092
27879
|
log2.info("daemon stopped");
|
|
27093
27880
|
removePidFileIfMatches(pf, data);
|
|
27094
27881
|
}
|
|
27095
27882
|
async function daemonStop(opts) {
|
|
27096
27883
|
const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
|
|
27097
|
-
await stopByPidfile(
|
|
27884
|
+
await stopByPidfile(path13.join(daemonDirById(baseDir, opts.id), "daemon.pid"), `no daemon with id '${opts.id}' (pidfile not found — check \`alook daemon list\`)`);
|
|
27098
27885
|
}
|
|
27099
27886
|
function credentialFilePathByMachineId(baseDir, machineId) {
|
|
27100
|
-
return
|
|
27887
|
+
return path13.join(daemonsDir(baseDir), `${validateMachineId(machineId)}.credential.json`);
|
|
27101
27888
|
}
|
|
27102
27889
|
function readCredentialFile(filePath) {
|
|
27103
|
-
if (!
|
|
27890
|
+
if (!fs12.existsSync(filePath))
|
|
27104
27891
|
return null;
|
|
27105
|
-
const stat =
|
|
27892
|
+
const stat = fs12.lstatSync(filePath);
|
|
27106
27893
|
if (!stat.isFile())
|
|
27107
27894
|
throw new Error("unsafe daemon credential file type");
|
|
27108
|
-
|
|
27895
|
+
fs12.chmodSync(filePath, 384);
|
|
27109
27896
|
try {
|
|
27110
|
-
const content = JSON.parse(
|
|
27897
|
+
const content = JSON.parse(fs12.readFileSync(filePath, "utf8"));
|
|
27111
27898
|
if (typeof content.credential === "string" && content.credential.startsWith("cmk_") && typeof content.machineId === "string") {
|
|
27899
|
+
if (content.schemaVersion === 1 && typeof content.serverUrl === "string" && typeof content.wsUrl === "string" && typeof content.daemonVersion === "string") {
|
|
27900
|
+
return {
|
|
27901
|
+
schemaVersion: 1,
|
|
27902
|
+
credential: content.credential,
|
|
27903
|
+
machineId: content.machineId,
|
|
27904
|
+
serverUrl: content.serverUrl,
|
|
27905
|
+
wsUrl: content.wsUrl,
|
|
27906
|
+
daemonVersion: content.daemonVersion
|
|
27907
|
+
};
|
|
27908
|
+
}
|
|
27112
27909
|
return { credential: content.credential, machineId: content.machineId };
|
|
27113
27910
|
}
|
|
27114
27911
|
} catch {}
|
|
27115
27912
|
return null;
|
|
27116
27913
|
}
|
|
27117
|
-
function writeCredentialFile(filePath,
|
|
27118
|
-
|
|
27119
|
-
|
|
27120
|
-
|
|
27121
|
-
|
|
27122
|
-
|
|
27123
|
-
|
|
27124
|
-
fs11.fsyncSync(fd);
|
|
27125
|
-
fs11.closeSync(fd);
|
|
27126
|
-
fd = null;
|
|
27127
|
-
fs11.chmodSync(tempPath, 384);
|
|
27128
|
-
fs11.renameSync(tempPath, filePath);
|
|
27129
|
-
syncDirectory(dir);
|
|
27130
|
-
} finally {
|
|
27131
|
-
if (fd !== null) {
|
|
27132
|
-
try {
|
|
27133
|
-
fs11.closeSync(fd);
|
|
27134
|
-
} catch {}
|
|
27135
|
-
}
|
|
27136
|
-
try {
|
|
27137
|
-
fs11.unlinkSync(tempPath);
|
|
27138
|
-
} catch {}
|
|
27914
|
+
function writeCredentialFile(filePath, record2) {
|
|
27915
|
+
writePrivateJsonAtomic2(filePath, record2);
|
|
27916
|
+
}
|
|
27917
|
+
function readDaemonLaunchRecord(baseDir, machineId) {
|
|
27918
|
+
const record2 = readCredentialFile(credentialFilePathByMachineId(baseDir, machineId));
|
|
27919
|
+
if (!record2 || !("schemaVersion" in record2) || record2.schemaVersion !== 1 || !parseReleaseVersion(record2.daemonVersion)) {
|
|
27920
|
+
throw new Error("daemon launch record is missing or requires a manual start upgrade");
|
|
27139
27921
|
}
|
|
27922
|
+
validateMachineId(record2.machineId);
|
|
27923
|
+
if (record2.machineId !== machineId)
|
|
27924
|
+
throw new Error("daemon launch record machine mismatch");
|
|
27925
|
+
return record2;
|
|
27140
27926
|
}
|
|
27141
27927
|
function findExistingCredentialForBearer(baseDir, bearer) {
|
|
27142
27928
|
const dir = daemonsDir(baseDir);
|
|
27143
|
-
if (!
|
|
27929
|
+
if (!fs12.existsSync(dir))
|
|
27144
27930
|
return null;
|
|
27145
|
-
for (const file2 of
|
|
27931
|
+
for (const file2 of fs12.readdirSync(dir)) {
|
|
27146
27932
|
if (!file2.endsWith(".credential.json"))
|
|
27147
27933
|
continue;
|
|
27148
|
-
const parsed = readCredentialFile(
|
|
27934
|
+
const parsed = readCredentialFile(path13.join(dir, file2));
|
|
27149
27935
|
if (parsed && parsed.credential === bearer)
|
|
27150
27936
|
return parsed;
|
|
27151
27937
|
}
|
|
@@ -27187,7 +27973,10 @@ async function prepareDaemonStart(opts) {
|
|
|
27187
27973
|
throw new Error("invalid machine key format — expected `cmt_` or `cmk_`");
|
|
27188
27974
|
}
|
|
27189
27975
|
const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
|
|
27190
|
-
const
|
|
27976
|
+
const daemonVersion = readDaemonVersion();
|
|
27977
|
+
if (!parseReleaseVersion(daemonVersion))
|
|
27978
|
+
throw new Error("daemon package version is not a strict release version");
|
|
27979
|
+
const ownerToken = crypto5.randomBytes(24).toString("base64url");
|
|
27191
27980
|
const persisted = opts.machineKey.startsWith("cmk_") ? findExistingCredentialForBearer(baseDir, opts.machineKey) : null;
|
|
27192
27981
|
let coarseLockPath = null;
|
|
27193
27982
|
let launchLockPath = null;
|
|
@@ -27195,7 +27984,7 @@ async function prepareDaemonStart(opts) {
|
|
|
27195
27984
|
let machineKey = persisted?.credential;
|
|
27196
27985
|
let machineId = persisted ? validateMachineId(persisted.machineId) : undefined;
|
|
27197
27986
|
if (machineKey && machineId) {
|
|
27198
|
-
launchLockPath = acquireLaunchLock(baseDir, machineId, ownerToken);
|
|
27987
|
+
launchLockPath = acquireLaunchLock(baseDir, machineId, ownerToken, opts.resumeRequestId);
|
|
27199
27988
|
} else {
|
|
27200
27989
|
coarseLockPath = acquireCoarseLock(baseDir, ownerToken);
|
|
27201
27990
|
if (opts.machineKey.startsWith("cmk_")) {
|
|
@@ -27205,15 +27994,22 @@ async function prepareDaemonStart(opts) {
|
|
|
27205
27994
|
const runtimeReport = await detectRuntimes();
|
|
27206
27995
|
const healthyRuntimeIds = runtimeReport.filter((runtime) => runtime.status === "healthy").map((runtime) => runtime.id);
|
|
27207
27996
|
if (opts.machineKey.startsWith("cmt_")) {
|
|
27208
|
-
const activated = await activatePairingToken(serverUrl, opts.machineKey, os3.hostname(), process.platform, process.arch, os3.release(),
|
|
27997
|
+
const activated = await activatePairingToken(serverUrl, opts.machineKey, os3.hostname(), process.platform, process.arch, os3.release(), daemonVersion, runtimeReport);
|
|
27209
27998
|
machineKey = activated.credential;
|
|
27210
27999
|
machineId = validateMachineId(activated.machineId);
|
|
27211
28000
|
} else {
|
|
27212
28001
|
machineKey ??= opts.machineKey;
|
|
27213
28002
|
machineId ??= validateMachineId(await resolveMachineIdentity(serverUrl, opts.machineKey));
|
|
27214
28003
|
}
|
|
27215
|
-
|
|
27216
|
-
|
|
28004
|
+
launchLockPath ??= acquireLaunchLock(baseDir, machineId, ownerToken, opts.resumeRequestId);
|
|
28005
|
+
writeCredentialFile(credentialFilePathByMachineId(baseDir, machineId), {
|
|
28006
|
+
schemaVersion: 1,
|
|
28007
|
+
credential: machineKey,
|
|
28008
|
+
machineId,
|
|
28009
|
+
serverUrl,
|
|
28010
|
+
wsUrl,
|
|
28011
|
+
daemonVersion
|
|
28012
|
+
});
|
|
27217
28013
|
if (coarseLockPath)
|
|
27218
28014
|
removeOwnedFile(coarseLockPath, process.pid, ownerToken);
|
|
27219
28015
|
const startedAt = new Date().toISOString();
|
|
@@ -27234,7 +28030,7 @@ async function prepareDaemonStart(opts) {
|
|
|
27234
28030
|
platform: process.platform,
|
|
27235
28031
|
arch: process.arch,
|
|
27236
28032
|
osRelease: os3.release(),
|
|
27237
|
-
daemonVersion
|
|
28033
|
+
daemonVersion,
|
|
27238
28034
|
ownerToken,
|
|
27239
28035
|
startedAt
|
|
27240
28036
|
}
|
|
@@ -27247,12 +28043,26 @@ async function prepareDaemonStart(opts) {
|
|
|
27247
28043
|
throw error51;
|
|
27248
28044
|
}
|
|
27249
28045
|
}
|
|
28046
|
+
async function daemonResume(opts) {
|
|
28047
|
+
if (!/^[A-Za-z0-9_-]{16,128}$/.test(opts.requestId))
|
|
28048
|
+
throw new Error("invalid replacement request id");
|
|
28049
|
+
const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
|
|
28050
|
+
const record2 = readDaemonLaunchRecord(baseDir, opts.id);
|
|
28051
|
+
if (false) {}
|
|
28052
|
+
await daemonStart({
|
|
28053
|
+
machineKey: record2.credential,
|
|
28054
|
+
serverUrl: record2.serverUrl,
|
|
28055
|
+
wsUrl: record2.wsUrl,
|
|
28056
|
+
baseDir,
|
|
28057
|
+
resumeRequestId: opts.requestId
|
|
28058
|
+
});
|
|
28059
|
+
}
|
|
27250
28060
|
function runnerArguments() {
|
|
27251
28061
|
const command = process.env.ALOOK_DAEMON_PACKAGE_WRAPPER === "1" ? ["run"] : ["daemon", "run"];
|
|
27252
28062
|
return [...process.execArgv, process.argv[1], ...command];
|
|
27253
28063
|
}
|
|
27254
28064
|
function spawnBlockedRunner() {
|
|
27255
|
-
return
|
|
28065
|
+
return spawn3(process.execPath, runnerArguments(), {
|
|
27256
28066
|
detached: true,
|
|
27257
28067
|
stdio: ["ignore", "ignore", "ignore", "ipc"]
|
|
27258
28068
|
});
|
|
@@ -27263,9 +28073,9 @@ function testCheckpoint(name, childPid) {
|
|
|
27263
28073
|
const checkpointFile = process.env.ALOOK_DAEMON_TEST_CHECKPOINT_FILE;
|
|
27264
28074
|
if (!checkpointFile)
|
|
27265
28075
|
return;
|
|
27266
|
-
|
|
28076
|
+
fs12.writeFileSync(checkpointFile, JSON.stringify({ name, parentPid: process.pid, childPid }), { mode: 384 });
|
|
27267
28077
|
const view = new Int32Array(new SharedArrayBuffer(4));
|
|
27268
|
-
while (!
|
|
28078
|
+
while (!fs12.existsSync(`${checkpointFile}.continue`))
|
|
27269
28079
|
Atomics.wait(view, 0, 0, 25);
|
|
27270
28080
|
}
|
|
27271
28081
|
function sendPrepared(child, prepared) {
|
|
@@ -27291,7 +28101,7 @@ function waitForReceipt(child, prepared) {
|
|
|
27291
28101
|
};
|
|
27292
28102
|
const configuredTestTimeout = NaN;
|
|
27293
28103
|
const receiptTimeoutMs = Number.isFinite(configuredTestTimeout) && configuredTestTimeout > 0 ? configuredTestTimeout : START_RECEIPT_TIMEOUT_MS;
|
|
27294
|
-
const timeout = setTimeout(() => settle(new Error(`daemon start timed out; inspect ${
|
|
28104
|
+
const timeout = setTimeout(() => settle(new Error(`daemon start timed out; inspect ${path13.join(prepared.daemonDir, "daemon.log")}`)), receiptTimeoutMs);
|
|
27295
28105
|
child.on("message", (message2) => {
|
|
27296
28106
|
const accepted = message2;
|
|
27297
28107
|
if (accepted.type === "daemon:accepted") {
|
|
@@ -27310,7 +28120,7 @@ function waitForReceipt(child, prepared) {
|
|
|
27310
28120
|
settle(undefined, receipt);
|
|
27311
28121
|
});
|
|
27312
28122
|
child.once("exit", (code, signal) => {
|
|
27313
|
-
settle(new Error(`daemon child exited before ready (${signal ?? code}); inspect ${
|
|
28123
|
+
settle(new Error(`daemon child exited before ready (${signal ?? code}); inspect ${path13.join(prepared.daemonDir, "daemon.log")}`));
|
|
27314
28124
|
});
|
|
27315
28125
|
child.once("error", (error51) => settle(error51));
|
|
27316
28126
|
});
|
|
@@ -27443,6 +28253,70 @@ function sendIpcBestEffort(ipc, message2, disconnectAfter = false) {
|
|
|
27443
28253
|
}
|
|
27444
28254
|
}
|
|
27445
28255
|
|
|
28256
|
+
// src/cli/messageReminderClient.ts
|
|
28257
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
28258
|
+
var LOCAL_MESSAGE_REMINDER_PATH2 = "/__alook/local/message-reminder";
|
|
28259
|
+
var MIN_REMINDER_MS = 60000;
|
|
28260
|
+
var MAX_REMINDER_MS = 24 * 60 * 60000;
|
|
28261
|
+
function parseRemindAfter(value) {
|
|
28262
|
+
const match = /^(\d+)(m|h)$/.exec(value);
|
|
28263
|
+
if (!match) {
|
|
28264
|
+
throw new Error("message send: --remind-after must be a positive integer followed by m or h (1m..24h)");
|
|
28265
|
+
}
|
|
28266
|
+
const amount = Number(match[1]);
|
|
28267
|
+
const milliseconds = amount * (match[2] === "h" ? 60 * 60000 : 60000);
|
|
28268
|
+
if (!Number.isSafeInteger(milliseconds) || milliseconds < MIN_REMINDER_MS || milliseconds > MAX_REMINDER_MS) {
|
|
28269
|
+
throw new Error("message send: --remind-after must be between 1m and 24h");
|
|
28270
|
+
}
|
|
28271
|
+
return milliseconds;
|
|
28272
|
+
}
|
|
28273
|
+
async function armMessageReminderFromEnv(input, env = process.env, fetchImpl = fetch) {
|
|
28274
|
+
const proxyUrl = env.ALOOK_PROXY_URL;
|
|
28275
|
+
const tokenFile = env.ALOOK_PROXY_TOKEN_FILE;
|
|
28276
|
+
if (!proxyUrl || !tokenFile) {
|
|
28277
|
+
return { armed: false, reason: "local reminder proxy unavailable" };
|
|
28278
|
+
}
|
|
28279
|
+
let voucher;
|
|
28280
|
+
try {
|
|
28281
|
+
voucher = readFileSync9(tokenFile, "utf8").trim();
|
|
28282
|
+
} catch {
|
|
28283
|
+
return { armed: false, reason: "local reminder voucher unavailable" };
|
|
28284
|
+
}
|
|
28285
|
+
if (!voucher)
|
|
28286
|
+
return { armed: false, reason: "local reminder voucher unavailable" };
|
|
28287
|
+
let response;
|
|
28288
|
+
try {
|
|
28289
|
+
response = await fetchImpl(`${proxyUrl.replace(/\/+$/, "")}${LOCAL_MESSAGE_REMINDER_PATH2}`, {
|
|
28290
|
+
method: "PUT",
|
|
28291
|
+
headers: {
|
|
28292
|
+
authorization: `Bearer ${voucher}`,
|
|
28293
|
+
"content-type": "application/json"
|
|
28294
|
+
},
|
|
28295
|
+
body: JSON.stringify(input),
|
|
28296
|
+
signal: AbortSignal.timeout(2000)
|
|
28297
|
+
});
|
|
28298
|
+
} catch {
|
|
28299
|
+
return { armed: false, reason: "local reminder request failed" };
|
|
28300
|
+
}
|
|
28301
|
+
let body;
|
|
28302
|
+
try {
|
|
28303
|
+
body = await response.json();
|
|
28304
|
+
} catch {
|
|
28305
|
+
return { armed: false, reason: "local reminder returned an invalid response" };
|
|
28306
|
+
}
|
|
28307
|
+
if (!response.ok) {
|
|
28308
|
+
const code = typeof body === "object" && body !== null && typeof body.code === "string" ? body.code : `http_${response.status}`;
|
|
28309
|
+
return { armed: false, reason: `local reminder rejected (${code})` };
|
|
28310
|
+
}
|
|
28311
|
+
if (typeof body === "object" && body !== null && body.armed === true && Number.isSafeInteger(body.dueAt)) {
|
|
28312
|
+
return { armed: true, dueAt: body.dueAt };
|
|
28313
|
+
}
|
|
28314
|
+
if (typeof body === "object" && body !== null && body.armed === false && typeof body.reason === "string") {
|
|
28315
|
+
return { armed: false, reason: body.reason };
|
|
28316
|
+
}
|
|
28317
|
+
return { armed: false, reason: "local reminder returned an invalid response" };
|
|
28318
|
+
}
|
|
28319
|
+
|
|
27446
28320
|
// src/cli/index.ts
|
|
27447
28321
|
function messagesInLocalTime(messages) {
|
|
27448
28322
|
return messages.map((m) => ({ ...m, time: toLocalISO(m.time) }));
|
|
@@ -27550,29 +28424,34 @@ function contentTypeFromFilename(filename) {
|
|
|
27550
28424
|
return "application/octet-stream";
|
|
27551
28425
|
}
|
|
27552
28426
|
}
|
|
27553
|
-
function
|
|
28427
|
+
function isTransientMutationError(err) {
|
|
27554
28428
|
const msg = err instanceof Error ? err.message : String(err);
|
|
27555
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");
|
|
27556
28430
|
}
|
|
27557
|
-
async function
|
|
28431
|
+
async function withTransientMutationRetry(mutation) {
|
|
27558
28432
|
const MAX_ATTEMPTS = 4;
|
|
27559
28433
|
const BASE_DELAY_MS = 150;
|
|
27560
28434
|
const MAX_DELAY_MS = 2000;
|
|
27561
28435
|
let lastErr;
|
|
27562
28436
|
for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
|
|
27563
28437
|
try {
|
|
27564
|
-
return await
|
|
28438
|
+
return await mutation();
|
|
27565
28439
|
} catch (err) {
|
|
27566
28440
|
lastErr = err;
|
|
27567
|
-
if (!
|
|
28441
|
+
if (!isTransientMutationError(err) || attempt === MAX_ATTEMPTS - 1)
|
|
27568
28442
|
throw err;
|
|
27569
28443
|
const cap = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);
|
|
27570
|
-
await new Promise((
|
|
28444
|
+
await new Promise((resolve4) => setTimeout(resolve4, cap));
|
|
27571
28445
|
}
|
|
27572
28446
|
}
|
|
27573
28447
|
throw lastErr;
|
|
27574
28448
|
}
|
|
28449
|
+
async function sendWithRetry(api2, req) {
|
|
28450
|
+
return withTransientMutationRetry(() => api2.send(req));
|
|
28451
|
+
}
|
|
27575
28452
|
async function cmdMessageSend(opts) {
|
|
28453
|
+
const remindAfterFlag = opts.remindAfter;
|
|
28454
|
+
const remindAfterMs = remindAfterFlag === undefined ? undefined : parseRemindAfter(remindAfterFlag);
|
|
27576
28455
|
const api2 = getApi();
|
|
27577
28456
|
const agent2 = agentId(opts);
|
|
27578
28457
|
const channel2 = opts.target;
|
|
@@ -27582,10 +28461,10 @@ async function cmdMessageSend(opts) {
|
|
|
27582
28461
|
const fileFlag = opts.file;
|
|
27583
28462
|
const textFlag = opts.text;
|
|
27584
28463
|
if (fileFlag) {
|
|
27585
|
-
const
|
|
27586
|
-
if (!
|
|
28464
|
+
const fs13 = await import("fs");
|
|
28465
|
+
if (!fs13.existsSync(fileFlag))
|
|
27587
28466
|
throw new CliError(`message send: file not found: ${fileFlag}`);
|
|
27588
|
-
text2 =
|
|
28467
|
+
text2 = fs13.readFileSync(fileFlag, "utf8").trim();
|
|
27589
28468
|
} else if (typeof textFlag === "string") {
|
|
27590
28469
|
text2 = decodeTextEscapes(textFlag);
|
|
27591
28470
|
}
|
|
@@ -27605,7 +28484,7 @@ async function cmdMessageSend(opts) {
|
|
|
27605
28484
|
}
|
|
27606
28485
|
replyToSeq = n;
|
|
27607
28486
|
}
|
|
27608
|
-
const nonce =
|
|
28487
|
+
const nonce = randomUUID6();
|
|
27609
28488
|
const res = await sendWithRetry(api2, {
|
|
27610
28489
|
agentId: agent2,
|
|
27611
28490
|
channel: channel2,
|
|
@@ -27615,27 +28494,29 @@ async function cmdMessageSend(opts) {
|
|
|
27615
28494
|
nonce
|
|
27616
28495
|
});
|
|
27617
28496
|
if (res.state === "blocked") {
|
|
27618
|
-
throw new CliError(`channel not aligned: ${res.unreadCount} unread message(s) in ${channel2} (latest #${res.latestSeq}). Run \`alook inbox pull\` to
|
|
28497
|
+
throw new CliError(`channel not aligned: ${res.unreadCount} unread message(s) in ${channel2} (latest #${res.latestSeq}). Run \`alook inbox pull\` and READ the new messages before deciding whether to resend, adjust, or skip your message.`);
|
|
28498
|
+
}
|
|
28499
|
+
const sent = `${res.message.channel}${res.message.seq}`;
|
|
28500
|
+
if (remindAfterMs === undefined)
|
|
28501
|
+
return { sent };
|
|
28502
|
+
const seqText = res.message.seq.replace(/^#/, "");
|
|
28503
|
+
const sentSeq = Number(seqText);
|
|
28504
|
+
if (!/^\d+$/.test(seqText) || !Number.isSafeInteger(sentSeq) || sentSeq < 1) {
|
|
28505
|
+
return { sent, reminder: { armed: false, reason: "server returned an invalid canonical message seq" } };
|
|
28506
|
+
}
|
|
28507
|
+
try {
|
|
28508
|
+
const reminder = await armMessageReminderFromEnv({
|
|
28509
|
+
channel: res.message.channel,
|
|
28510
|
+
sentSeq,
|
|
28511
|
+
remindAfterMs
|
|
28512
|
+
});
|
|
28513
|
+
return { sent, reminder };
|
|
28514
|
+
} catch {
|
|
28515
|
+
return { sent, reminder: { armed: false, reason: "local reminder request failed" } };
|
|
27619
28516
|
}
|
|
27620
|
-
return { sent: `${res.message.channel}${res.message.seq}` };
|
|
27621
28517
|
}
|
|
27622
28518
|
async function createPostWithRetry(api2, req) {
|
|
27623
|
-
|
|
27624
|
-
const BASE_DELAY_MS = 150;
|
|
27625
|
-
const MAX_DELAY_MS = 2000;
|
|
27626
|
-
let lastErr;
|
|
27627
|
-
for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
|
|
27628
|
-
try {
|
|
27629
|
-
return await api2.createPost(req);
|
|
27630
|
-
} catch (err) {
|
|
27631
|
-
lastErr = err;
|
|
27632
|
-
if (!isTransientSendError(err) || attempt === MAX_ATTEMPTS - 1)
|
|
27633
|
-
throw err;
|
|
27634
|
-
const cap = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);
|
|
27635
|
-
await new Promise((r) => setTimeout(r, cap));
|
|
27636
|
-
}
|
|
27637
|
-
}
|
|
27638
|
-
throw lastErr;
|
|
28519
|
+
return withTransientMutationRetry(() => api2.createPost(req));
|
|
27639
28520
|
}
|
|
27640
28521
|
async function cmdMessagePost(opts) {
|
|
27641
28522
|
const api2 = getApi();
|
|
@@ -27650,10 +28531,10 @@ async function cmdMessagePost(opts) {
|
|
|
27650
28531
|
const fileFlag = opts.file;
|
|
27651
28532
|
const textFlag = opts.text;
|
|
27652
28533
|
if (fileFlag) {
|
|
27653
|
-
const
|
|
27654
|
-
if (!
|
|
28534
|
+
const fs13 = await import("fs");
|
|
28535
|
+
if (!fs13.existsSync(fileFlag))
|
|
27655
28536
|
throw new CliError(`message post: file not found: ${fileFlag}`);
|
|
27656
|
-
text2 =
|
|
28537
|
+
text2 = fs13.readFileSync(fileFlag, "utf8").trim();
|
|
27657
28538
|
} else if (typeof textFlag === "string") {
|
|
27658
28539
|
text2 = decodeTextEscapes(textFlag);
|
|
27659
28540
|
}
|
|
@@ -27662,7 +28543,7 @@ async function cmdMessagePost(opts) {
|
|
|
27662
28543
|
if (!hasText && attachmentIds.length === 0) {
|
|
27663
28544
|
throw new CliError("message post: --text <text>, --file <path>, or --attachment <id> is required");
|
|
27664
28545
|
}
|
|
27665
|
-
const nonce =
|
|
28546
|
+
const nonce = randomUUID6();
|
|
27666
28547
|
const res = await createPostWithRetry(api2, {
|
|
27667
28548
|
agentId: agent2,
|
|
27668
28549
|
forum,
|
|
@@ -27681,25 +28562,52 @@ async function cmdMessageEmoji(opts) {
|
|
|
27681
28562
|
throw new CliError("message emoji: --target <ref> is required (e.g. /demo#1234/general#42)");
|
|
27682
28563
|
if (!emoji3)
|
|
27683
28564
|
throw new CliError("message emoji: --emoji <string> is required");
|
|
28565
|
+
const { channel: channel2, seq } = parseMessageTarget("message emoji", target);
|
|
28566
|
+
if (Buffer.byteLength(emoji3, "utf8") > MAX_EMOJI_BYTES) {
|
|
28567
|
+
const err = new CliError("emoji is too long");
|
|
28568
|
+
err.hint = "use a single emoji, not a phrase";
|
|
28569
|
+
throw err;
|
|
28570
|
+
}
|
|
28571
|
+
const res = await api2.reactAdd({ channel: channel2, seq, emoji: emoji3 });
|
|
28572
|
+
return { target, emoji: emoji3, duplicate: res.duplicate === true };
|
|
28573
|
+
}
|
|
28574
|
+
function parseMessageTarget(command, target) {
|
|
27684
28575
|
let parsed;
|
|
27685
28576
|
try {
|
|
27686
28577
|
parsed = parseRef(target);
|
|
27687
28578
|
} catch (err) {
|
|
27688
|
-
throw new CliError(
|
|
28579
|
+
throw new CliError(`${command}: ${err.message}`);
|
|
27689
28580
|
}
|
|
27690
28581
|
if (parsed.seq === undefined) {
|
|
27691
|
-
const err = new CliError(
|
|
28582
|
+
const err = new CliError(`${command} needs a ref with a seq (e.g. ${target}#42)`);
|
|
27692
28583
|
err.hint = "pass --target /<server>/<channel>#N, /<server>/<channel>/#N#M for thread reply, or /.dm/<peer>#N";
|
|
27693
28584
|
throw err;
|
|
27694
28585
|
}
|
|
27695
|
-
if (Buffer.byteLength(emoji3, "utf8") > MAX_EMOJI_BYTES) {
|
|
27696
|
-
const err = new CliError("emoji is too long");
|
|
27697
|
-
err.hint = "use a single emoji, not a phrase";
|
|
27698
|
-
throw err;
|
|
27699
|
-
}
|
|
27700
28586
|
const channel2 = parsed.threadRootSeq !== undefined ? `/${parsed.server}/${parsed.channel}/#${parsed.threadRootSeq}` : `/${parsed.server}/${parsed.channel}`;
|
|
27701
|
-
|
|
27702
|
-
|
|
28587
|
+
return { channel: channel2, seq: parsed.seq };
|
|
28588
|
+
}
|
|
28589
|
+
async function cmdMessageMarkSet(opts) {
|
|
28590
|
+
const api2 = getApi();
|
|
28591
|
+
const target = opts.target;
|
|
28592
|
+
if (!target)
|
|
28593
|
+
throw new CliError("message mark set: --target <ref> is required");
|
|
28594
|
+
const request = parseMessageTarget("message mark set", target);
|
|
28595
|
+
await withTransientMutationRetry(() => api2.markSet(request));
|
|
28596
|
+
return { target, marked: true };
|
|
28597
|
+
}
|
|
28598
|
+
async function cmdMessageMarkRemove(opts) {
|
|
28599
|
+
const api2 = getApi();
|
|
28600
|
+
const target = opts.target;
|
|
28601
|
+
if (!target)
|
|
28602
|
+
throw new CliError("message mark remove: --target <ref> is required");
|
|
28603
|
+
const request = parseMessageTarget("message mark remove", target);
|
|
28604
|
+
await withTransientMutationRetry(() => api2.markRemove(request));
|
|
28605
|
+
return { target, marked: false };
|
|
28606
|
+
}
|
|
28607
|
+
async function cmdMessageMarkList(opts) {
|
|
28608
|
+
const api2 = getApi();
|
|
28609
|
+
const { marked } = await api2.listMarks({ agentId: agentId(opts) });
|
|
28610
|
+
return { marked: messagesInLocalTime(marked) };
|
|
27703
28611
|
}
|
|
27704
28612
|
async function cmdAttachmentUpload(opts) {
|
|
27705
28613
|
const api2 = getApi();
|
|
@@ -27710,10 +28618,10 @@ async function cmdAttachmentUpload(opts) {
|
|
|
27710
28618
|
throw new CliError("message attachment upload: --target <ref> is required");
|
|
27711
28619
|
if (!filePath)
|
|
27712
28620
|
throw new CliError("message attachment upload: --file <path> is required");
|
|
27713
|
-
const
|
|
28621
|
+
const fs13 = await import("fs/promises");
|
|
27714
28622
|
let bytes;
|
|
27715
28623
|
try {
|
|
27716
|
-
bytes = await
|
|
28624
|
+
bytes = await fs13.readFile(filePath);
|
|
27717
28625
|
} catch (err) {
|
|
27718
28626
|
throw new CliError(`message attachment upload: cannot read file: ${err.message}`);
|
|
27719
28627
|
}
|
|
@@ -27723,10 +28631,35 @@ async function cmdAttachmentUpload(opts) {
|
|
|
27723
28631
|
const pathMod = await import("path");
|
|
27724
28632
|
const filename = pathMod.basename(filePath);
|
|
27725
28633
|
const contentType = contentTypeFromFilename(filename);
|
|
28634
|
+
let thumbnail;
|
|
28635
|
+
let width;
|
|
28636
|
+
let height;
|
|
28637
|
+
if (["image/png", "image/jpeg", "image/webp", "image/gif"].includes(contentType)) {
|
|
28638
|
+
try {
|
|
28639
|
+
const { default: sharp } = await import("sharp");
|
|
28640
|
+
const image = sharp(bytes, { failOn: "error" });
|
|
28641
|
+
const metadata = await image.metadata();
|
|
28642
|
+
if (metadata.width && metadata.height) {
|
|
28643
|
+
width = metadata.width;
|
|
28644
|
+
height = metadata.height;
|
|
28645
|
+
}
|
|
28646
|
+
const jpeg = await image.resize({ width: 200, height: 200, fit: "inside", withoutEnlargement: true }).jpeg({ quality: 70 }).toBuffer();
|
|
28647
|
+
if (jpeg.byteLength <= MAX_ATTACHMENT_THUMBNAIL_SIZE_BYTES) {
|
|
28648
|
+
thumbnail = {
|
|
28649
|
+
data: new Uint8Array(jpeg),
|
|
28650
|
+
filename: "thumbnail.jpg",
|
|
28651
|
+
contentType: "image/jpeg"
|
|
28652
|
+
};
|
|
28653
|
+
}
|
|
28654
|
+
} catch {}
|
|
28655
|
+
}
|
|
27726
28656
|
const result = await api2.attachmentUpload({
|
|
27727
28657
|
agentId: agent2,
|
|
27728
28658
|
target,
|
|
27729
|
-
file: { data: new Uint8Array(bytes), filename, contentType }
|
|
28659
|
+
file: { data: new Uint8Array(bytes), filename, contentType },
|
|
28660
|
+
...thumbnail ? { thumbnail } : {},
|
|
28661
|
+
...width !== undefined ? { width } : {},
|
|
28662
|
+
...height !== undefined ? { height } : {}
|
|
27730
28663
|
});
|
|
27731
28664
|
return result;
|
|
27732
28665
|
}
|
|
@@ -27742,13 +28675,13 @@ async function cmdAttachmentDownload(opts) {
|
|
|
27742
28675
|
const destPath = outFlag ?? pathMod.join(os4.tmpdir(), "alook-attachments", agent2, id, "file");
|
|
27743
28676
|
const result = await api2.attachmentDownload({ agentId: agent2, id, destPath });
|
|
27744
28677
|
if (!outFlag) {
|
|
27745
|
-
const
|
|
28678
|
+
const fs13 = await import("fs/promises");
|
|
27746
28679
|
const destDir = pathMod.dirname(destPath);
|
|
27747
28680
|
const safeName = pathMod.basename(result.filename) || "file";
|
|
27748
28681
|
const renamed = pathMod.join(destDir, safeName);
|
|
27749
28682
|
if (renamed !== destPath) {
|
|
27750
28683
|
try {
|
|
27751
|
-
await
|
|
28684
|
+
await fs13.rename(destPath, renamed);
|
|
27752
28685
|
return { ...result, path: renamed };
|
|
27753
28686
|
} catch {
|
|
27754
28687
|
return { ...result, path: destPath };
|
|
@@ -27761,7 +28694,7 @@ async function cmdInboxPull(opts) {
|
|
|
27761
28694
|
const api2 = getApi();
|
|
27762
28695
|
const agent2 = agentId(opts);
|
|
27763
28696
|
const max = opts.max ? Number(opts.max) : undefined;
|
|
27764
|
-
const { messages, hasMore } = await api2.inboxPull({ agentId: agent2, max });
|
|
28697
|
+
const { messages, hasMore, markedCount } = await api2.inboxPull({ agentId: agent2, max });
|
|
27765
28698
|
const pulledAt = nowLocalISO();
|
|
27766
28699
|
let acked = 0;
|
|
27767
28700
|
let ackError;
|
|
@@ -27785,7 +28718,10 @@ async function cmdInboxPull(opts) {
|
|
|
27785
28718
|
hasMore,
|
|
27786
28719
|
acked,
|
|
27787
28720
|
pulledAt,
|
|
27788
|
-
...ackError ? { ackError } : {}
|
|
28721
|
+
...ackError ? { ackError } : {},
|
|
28722
|
+
...markedCount > 0 ? {
|
|
28723
|
+
markedReminder: `You have ${markedCount} marked ${markedCount === 1 ? "message" : "messages"}. Resolve ${markedCount === 1 ? "it" : "them"} before going dark unless blocked.`
|
|
28724
|
+
} : {}
|
|
27789
28725
|
};
|
|
27790
28726
|
}
|
|
27791
28727
|
async function cmdServerList(opts) {
|
|
@@ -27880,10 +28816,10 @@ async function cmdNap(opts) {
|
|
|
27880
28816
|
const textFlag = opts.text;
|
|
27881
28817
|
let handoff;
|
|
27882
28818
|
if (fileFlag) {
|
|
27883
|
-
const
|
|
27884
|
-
if (!
|
|
28819
|
+
const fs13 = await import("fs");
|
|
28820
|
+
if (!fs13.existsSync(fileFlag))
|
|
27885
28821
|
throw new CliError(`nap: handoff file not found: ${fileFlag}`);
|
|
27886
|
-
handoff =
|
|
28822
|
+
handoff = fs13.readFileSync(fileFlag, "utf8").trim();
|
|
27887
28823
|
} else if (typeof textFlag === "string") {
|
|
27888
28824
|
handoff = decodeTextEscapes(textFlag).trim();
|
|
27889
28825
|
}
|
|
@@ -27899,7 +28835,7 @@ function buildProgram() {
|
|
|
27899
28835
|
}).option("--agent <id>", "agent identity (or ALOOK_AGENT_ID env)");
|
|
27900
28836
|
const message2 = program.command("message").description("message operations").exitOverride();
|
|
27901
28837
|
message2.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
27902
|
-
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)').exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
28838
|
+
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() {
|
|
27903
28839
|
const localOpts = this.opts();
|
|
27904
28840
|
const globalOpts = program.opts();
|
|
27905
28841
|
const result = await cmdMessageSend({ ...globalOpts, ...localOpts });
|
|
@@ -27917,6 +28853,20 @@ function buildProgram() {
|
|
|
27917
28853
|
const result = await cmdMessageEmoji({ ...globalOpts, ...localOpts });
|
|
27918
28854
|
printEnvelope({ success: result });
|
|
27919
28855
|
});
|
|
28856
|
+
const mark = message2.command("mark").description("durable message mark operations").exitOverride();
|
|
28857
|
+
mark.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
28858
|
+
mark.command("set").description("mark a message as outstanding work").requiredOption("--target <ref>", "full message ref").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
28859
|
+
const result = await cmdMessageMarkSet({ ...program.opts(), ...this.opts() });
|
|
28860
|
+
printEnvelope({ success: result });
|
|
28861
|
+
});
|
|
28862
|
+
mark.command("remove").description("remove an outstanding-work mark").requiredOption("--target <ref>", "full message ref").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
28863
|
+
const result = await cmdMessageMarkRemove({ ...program.opts(), ...this.opts() });
|
|
28864
|
+
printEnvelope({ success: result });
|
|
28865
|
+
});
|
|
28866
|
+
mark.command("list").description("list all currently visible marked messages").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
28867
|
+
const result = await cmdMessageMarkList({ ...program.opts(), ...this.opts() });
|
|
28868
|
+
printEnvelope({ success: result });
|
|
28869
|
+
});
|
|
27920
28870
|
const attachment = message2.command("attachment").description("attachment operations").exitOverride();
|
|
27921
28871
|
attachment.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
27922
28872
|
attachment.command("upload").description("upload a local file as a pending attachment for a future send").option("--target <ref>", "destination (channel, DM, or thread ref)").option("--file <path>", "local file to upload").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
@@ -28014,6 +28964,22 @@ function buildProgram() {
|
|
|
28014
28964
|
daemon.command("run", { hidden: true }).exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async () => {
|
|
28015
28965
|
await daemonRunFromIpc();
|
|
28016
28966
|
});
|
|
28967
|
+
daemon.command("resume", { hidden: true }).requiredOption("--id <machineId>").requiredOption("--base-dir <path>").requiredOption("--request-id <id>").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
28968
|
+
const localOpts = this.opts();
|
|
28969
|
+
await daemonResume({
|
|
28970
|
+
id: localOpts.id,
|
|
28971
|
+
baseDir: localOpts.baseDir,
|
|
28972
|
+
requestId: localOpts.requestId
|
|
28973
|
+
});
|
|
28974
|
+
});
|
|
28975
|
+
daemon.command("replace", { hidden: true }).requiredOption("--id <machineId>").requiredOption("--base-dir <path>").requiredOption("--request-id <id>").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
28976
|
+
const localOpts = this.opts();
|
|
28977
|
+
await daemonReplace({
|
|
28978
|
+
id: localOpts.id,
|
|
28979
|
+
baseDir: localOpts.baseDir,
|
|
28980
|
+
requestId: localOpts.requestId
|
|
28981
|
+
});
|
|
28982
|
+
});
|
|
28017
28983
|
daemon.command("stop").argument("<id>", "daemon id from `alook daemon list` (the ID column)").description("stop a daemon by its id (from `alook daemon list`)").option("--base-dir <path>", "data directory (or ALOOK_DATA_DIR env)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function(id) {
|
|
28018
28984
|
const localOpts = this.opts();
|
|
28019
28985
|
await daemonStop({
|
|
@@ -28043,6 +29009,7 @@ function buildProgram() {
|
|
|
28043
29009
|
}
|
|
28044
29010
|
async function main(argv = process.argv.slice(2)) {
|
|
28045
29011
|
const program = buildProgram();
|
|
29012
|
+
let internalExitCode = 0;
|
|
28046
29013
|
try {
|
|
28047
29014
|
await program.parseAsync(argv, { from: "user" });
|
|
28048
29015
|
} catch (err) {
|
|
@@ -28064,8 +29031,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
28064
29031
|
hint: err.hint
|
|
28065
29032
|
});
|
|
28066
29033
|
}
|
|
29034
|
+
if (argv[0] === "daemon" && (argv[1] === "resume" || argv[1] === "replace")) {
|
|
29035
|
+
internalExitCode = 1;
|
|
29036
|
+
}
|
|
28067
29037
|
}
|
|
28068
|
-
return
|
|
29038
|
+
return internalExitCode;
|
|
28069
29039
|
}
|
|
28070
29040
|
function getHelpText(program, argv) {
|
|
28071
29041
|
const args = argv.filter((a) => a !== "-h" && a !== "--help");
|