allaboard 0.0.0-stage → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -2
  3. package/dist/agent/claude/backend.js +124 -0
  4. package/dist/agent/claude/capabilities.js +30 -0
  5. package/dist/agent/claude/chrome.js +42 -0
  6. package/dist/agent/claude/hooks.js +37 -0
  7. package/dist/agent/claude/info.js +61 -0
  8. package/dist/agent/claude/listing.js +230 -0
  9. package/dist/agent/claude/module.js +82 -0
  10. package/dist/agent/claude/prompt.js +24 -0
  11. package/dist/agent/claude/record.js +9 -0
  12. package/dist/agent/claude/requests.js +122 -0
  13. package/dist/agent/claude/session.js +441 -0
  14. package/dist/agent/claude/translate.js +581 -0
  15. package/dist/agent/claude/trust.js +576 -0
  16. package/dist/agent/claude/usage-probe.js +103 -0
  17. package/dist/agent/claude/usage.js +110 -0
  18. package/dist/agent/claude/wire.js +22 -0
  19. package/dist/agent/seam.js +21 -0
  20. package/dist/chat/seam.js +10 -0
  21. package/dist/chat/slack/app/answers.js +209 -0
  22. package/dist/chat/slack/app/app.js +262 -0
  23. package/dist/chat/slack/app/clip.js +191 -0
  24. package/dist/chat/slack/app/guards.js +162 -0
  25. package/dist/chat/slack/app/home-controls.js +91 -0
  26. package/dist/chat/slack/app/messages.js +181 -0
  27. package/dist/chat/slack/app/open.js +392 -0
  28. package/dist/chat/slack/app/order.js +37 -0
  29. package/dist/chat/slack/app/requests.js +482 -0
  30. package/dist/chat/slack/app/resume-bind.js +71 -0
  31. package/dist/chat/slack/app/wire.js +83 -0
  32. package/dist/chat/slack/attachments.js +348 -0
  33. package/dist/chat/slack/bind.js +67 -0
  34. package/dist/chat/slack/channel.js +67 -0
  35. package/dist/chat/slack/channels.js +43 -0
  36. package/dist/chat/slack/delete.js +278 -0
  37. package/dist/chat/slack/footer.js +43 -0
  38. package/dist/chat/slack/hold.js +37 -0
  39. package/dist/chat/slack/home.js +1069 -0
  40. package/dist/chat/slack/module.js +174 -0
  41. package/dist/chat/slack/notice.js +64 -0
  42. package/dist/chat/slack/openfile/modal.js +362 -0
  43. package/dist/chat/slack/openfile/read.js +29 -0
  44. package/dist/chat/slack/quiet-logger.js +52 -0
  45. package/dist/chat/slack/repair.js +123 -0
  46. package/dist/chat/slack/reply/blocks.js +372 -0
  47. package/dist/chat/slack/reply/chars.js +59 -0
  48. package/dist/chat/slack/reply/clients.js +171 -0
  49. package/dist/chat/slack/reply/errors.js +125 -0
  50. package/dist/chat/slack/reply/escape.js +25 -0
  51. package/dist/chat/slack/reply/limiter.js +86 -0
  52. package/dist/chat/slack/reply/markdown.js +210 -0
  53. package/dist/chat/slack/reply/places.js +262 -0
  54. package/dist/chat/slack/reply/sinks.js +2403 -0
  55. package/dist/chat/slack/reply/status.js +433 -0
  56. package/dist/chat/slack/reply/tasks.js +11 -0
  57. package/dist/chat/slack/requests.js +552 -0
  58. package/dist/chat/slack/resume.js +127 -0
  59. package/dist/chat/slack/setup.js +114 -0
  60. package/dist/chat/slack/texts.js +185 -0
  61. package/dist/chat/slack/thread.js +537 -0
  62. package/dist/chat/slack/update.js +71 -0
  63. package/dist/chat/slack/voice.js +84 -0
  64. package/dist/cli.js +326 -0
  65. package/dist/clock.js +21 -0
  66. package/dist/core/cleanup.js +145 -0
  67. package/dist/core/commands.js +172 -0
  68. package/dist/core/config.js +174 -0
  69. package/dist/core/files/changed.js +125 -0
  70. package/dist/core/files/listing.js +382 -0
  71. package/dist/core/files/paths.js +9 -0
  72. package/dist/core/files/read.js +191 -0
  73. package/dist/core/files/waiting.js +34 -0
  74. package/dist/core/flows/answers.js +133 -0
  75. package/dist/core/flows/ask-owner.js +95 -0
  76. package/dist/core/flows/asking.js +14 -0
  77. package/dist/core/flows/bind.js +95 -0
  78. package/dist/core/flows/contract.js +1 -0
  79. package/dist/core/flows/flows.js +35 -0
  80. package/dist/core/flows/hold.js +22 -0
  81. package/dist/core/flows/inbound.js +24 -0
  82. package/dist/core/flows/lists.js +21 -0
  83. package/dist/core/flows/messages.js +293 -0
  84. package/dist/core/flows/notices.js +51 -0
  85. package/dist/core/flows/repair.js +106 -0
  86. package/dist/core/flows/resume.js +163 -0
  87. package/dist/core/flows/setup.js +130 -0
  88. package/dist/core/flows/status.js +98 -0
  89. package/dist/core/flows/talk.js +207 -0
  90. package/dist/core/flows/update.js +370 -0
  91. package/dist/core/flows/words.js +171 -0
  92. package/dist/core/folders.js +93 -0
  93. package/dist/core/footer.js +313 -0
  94. package/dist/core/guide.js +50 -0
  95. package/dist/core/hold.js +126 -0
  96. package/dist/core/instance.js +46 -0
  97. package/dist/core/lock.js +412 -0
  98. package/dist/core/models.js +10 -0
  99. package/dist/core/paths.js +75 -0
  100. package/dist/core/port/file-flows.js +1 -0
  101. package/dist/core/port/home.js +1 -0
  102. package/dist/core/port/requests.js +1 -0
  103. package/dist/core/port/threads.js +1 -0
  104. package/dist/core/port/updates.js +1 -0
  105. package/dist/core/port.js +17 -0
  106. package/dist/core/reply/fold.js +175 -0
  107. package/dist/core/reply/previews.js +258 -0
  108. package/dist/core/reply/renderer.js +846 -0
  109. package/dist/core/reply/tail.js +24 -0
  110. package/dist/core/reply/words.js +32 -0
  111. package/dist/core/requests.js +158 -0
  112. package/dist/core/sessions/constants.js +61 -0
  113. package/dist/core/sessions/deps.js +1 -0
  114. package/dist/core/sessions/directory.js +86 -0
  115. package/dist/core/sessions/errors.js +79 -0
  116. package/dist/core/sessions/manager.js +378 -0
  117. package/dist/core/sessions/session.js +3297 -0
  118. package/dist/core/sessions/tasks.js +266 -0
  119. package/dist/core/sessions/turn.js +45 -0
  120. package/dist/core/setup.js +28 -0
  121. package/dist/core/state.js +724 -0
  122. package/dist/core/texts.js +451 -0
  123. package/dist/core/updates.js +332 -0
  124. package/dist/daemon.js +566 -0
  125. package/dist/host/npm.js +134 -0
  126. package/dist/host/program.js +49 -0
  127. package/dist/host/run.js +52 -0
  128. package/dist/host/service/launchd.js +385 -0
  129. package/dist/host/service/listing.js +42 -0
  130. package/dist/host/service/seam.js +1 -0
  131. package/dist/log.js +37 -0
  132. package/dist/main.js +29 -0
  133. package/dist/node-version.js +40 -0
  134. package/dist/registry.js +12 -0
  135. package/dist/semver.js +53 -0
  136. package/dist/text.js +89 -0
  137. package/dist/updates-host.js +113 -0
  138. package/dist/values.js +30 -0
  139. package/dist/version.js +54 -0
  140. package/package.json +32 -4
  141. package/slack-app-manifest.json +50 -0
@@ -0,0 +1,332 @@
1
+ import { getLogger } from "../log.js";
2
+ import { isNewer } from "../semver.js";
3
+ import { errorName } from "../values.js";
4
+ import * as texts from "./texts.js";
5
+ import { fill } from "./texts.js";
6
+ export const logger = getLogger("allaboard.core.updates");
7
+ /** The registry is asked at start and every 6 hours. */
8
+ export const UPDATE_CHECK_SECONDS = 6 * 60 * 60;
9
+ /** A check that could not finish (the copy was being replaced) is asked again after this. */
10
+ export const UPDATE_RETRY_SECONDS = 30;
11
+ /**
12
+ * How many checks in a row may find the entry file missing before the short retries end: a swap
13
+ * by another process lasts seconds, and a file that is gone for good (`npm uninstall -g` under a
14
+ * live daemon) must not be asked about every 30 seconds for as long as the daemon runs.
15
+ */
16
+ export const UPDATE_RETRY_LIMIT = 10;
17
+ export class Updates {
18
+ host;
19
+ /**
20
+ * Called after a change a page may show: a check, a phase. The daemon sets it to the chat's
21
+ * index refresh. A throw is logged here.
22
+ */
23
+ onChange = null;
24
+ #now;
25
+ #latest = null;
26
+ #checkedAt = null;
27
+ #failure = null;
28
+ #updatable = null;
29
+ #phase = { kind: "idle" };
30
+ #checking = null;
31
+ // Checks in a row that found the copy being replaced (`UPDATE_RETRY_LIMIT`).
32
+ #swaps = 0;
33
+ // Ended at the start of the shutdown: a registry question has no use then.
34
+ #checkEnd = new AbortController();
35
+ // Ended when a drain begins: a click that has not reached `npm install` stops asking.
36
+ #drainStart = new AbortController();
37
+ // Ended when the stop will not wait for an install any more: npm gets SIGTERM and rolls back.
38
+ #installEnd = new AbortController();
39
+ // The clicks under way (`track`): the drain waits for them.
40
+ #clicks = new Set();
41
+ constructor(host, options) {
42
+ this.host = host;
43
+ this.#now = options.now;
44
+ }
45
+ /** The version this process runs. */
46
+ get installed() {
47
+ return this.host.installed;
48
+ }
49
+ /** The newest version the last successful check found; null before one. */
50
+ get latest() {
51
+ return this.#latest;
52
+ }
53
+ /** Wall-clock seconds of the last check, successful or not; null before one. */
54
+ get checkedAt() {
55
+ return this.#checkedAt;
56
+ }
57
+ /** Why the last check could not say; null when it could. */
58
+ get checkFailure() {
59
+ return this.#failure;
60
+ }
61
+ /** Why the copy is not one an update replaces; null when it is, when unknown, or when npm could not say. */
62
+ get refusal() {
63
+ const found = this.#updatable;
64
+ if (found === null || !("refusal" in found) || found.refusal.kind === "npm")
65
+ return null;
66
+ return found.refusal;
67
+ }
68
+ /** The package folder of the copy the last check found to be one an update replaces. */
69
+ get copy() {
70
+ return this.#updatable !== null && "copy" in this.#updatable ? this.#updatable.copy : null;
71
+ }
72
+ /** Whether the last check found the copy to be one an update replaces. */
73
+ get canUpdate() {
74
+ return this.copy !== null;
75
+ }
76
+ get phase() {
77
+ return this.#phase;
78
+ }
79
+ /** The latest version when it is newer than the one that runs. */
80
+ get newer() {
81
+ return this.#latest !== null && isNewer(this.#latest, this.installed) ? this.#latest : null;
82
+ }
83
+ /** The signal that cancels the install of the click (`cancelInstall`). */
84
+ get installing() {
85
+ return this.#installEnd.signal;
86
+ }
87
+ /** Whether a click is at work (an install, or what follows it). */
88
+ get working() {
89
+ return this.#clicks.size > 0;
90
+ }
91
+ /**
92
+ * Runs the work of a click so that a stop can wait for it (`settled`, `cancelInstall`). `work`
93
+ * starts at once, so its first lines run before this returns.
94
+ */
95
+ track(work) {
96
+ const running = work().finally(() => this.#clicks.delete(running));
97
+ this.#clicks.add(running);
98
+ return running;
99
+ }
100
+ /**
101
+ * The drain's wait for the update: resolves once no click is at work, or when `cutShort`
102
+ * aborts. A click inside `validate` (still asking npm whether the copy updates) is ended at the
103
+ * start of the wait, since a question that hangs would hold the drain for its whole limit; any
104
+ * other click, an install under way included, is waited for.
105
+ */
106
+ async settled(cutShort) {
107
+ this.#drainStart.abort();
108
+ const cut = new Promise((resolve) => {
109
+ if (cutShort.aborted)
110
+ resolve();
111
+ else
112
+ cutShort.addEventListener("abort", () => resolve(), { once: true });
113
+ });
114
+ while (this.working && !cutShort.aborted) {
115
+ await Promise.race([Promise.allSettled([...this.#clicks]), cut]);
116
+ }
117
+ }
118
+ /**
119
+ * The stop will not wait for the install: npm under way is sent SIGTERM (it finishes its step,
120
+ * puts the old folder back and ends), and no other install starts. Resolves once every click
121
+ * has ended; never rejects.
122
+ */
123
+ async cancelInstall() {
124
+ this.#installEnd.abort();
125
+ await Promise.allSettled([...this.#clicks]);
126
+ }
127
+ /** The shutdown began: a question to the registry under way is ended. */
128
+ endChecks() {
129
+ this.#checkEnd.abort();
130
+ }
131
+ /**
132
+ * Asks the host for the newest version and for the copy; one at a time, never rejects. Resolves
133
+ * false when the answer is not complete (the copy was being replaced by another process: the
134
+ * previous answer about the copy stands and the caller asks again soon, up to
135
+ * `UPDATE_RETRY_LIMIT` times in a row), true otherwise.
136
+ */
137
+ check() {
138
+ this.#checking ??= this.#check().finally(() => {
139
+ this.#checking = null;
140
+ });
141
+ return this.#checking;
142
+ }
143
+ async #check() {
144
+ let complete = true;
145
+ try {
146
+ const signal = this.#checkEnd.signal;
147
+ const [latest, updatable] = await Promise.all([
148
+ this.host.latest(signal),
149
+ this.host.updatable(signal),
150
+ ]);
151
+ if ("swapping" in updatable) {
152
+ this.#swaps += 1;
153
+ if (this.#swaps < UPDATE_RETRY_LIMIT)
154
+ complete = false;
155
+ // The file is not coming back soon: the answer about the copy is not kept, so that
156
+ // nothing offers an update of a copy that is gone, and the long wait begins.
157
+ else
158
+ this.#updatable = null;
159
+ }
160
+ else {
161
+ this.#swaps = 0;
162
+ this.#updatable = updatable;
163
+ }
164
+ if ("version" in latest) {
165
+ this.#latest = latest.version;
166
+ this.#failure = null;
167
+ // A version newer than the one that failed is a new offer, not the old failure.
168
+ if (this.#phase.kind === "failed" && isNewer(latest.version, this.#phase.version)) {
169
+ this.#phase = { kind: "idle" };
170
+ }
171
+ }
172
+ else {
173
+ this.#failure = latest.failure;
174
+ }
175
+ // npm that could not say what the copy is leaves the update unproven, as an unknown latest does.
176
+ if (this.#failure === null && "refusal" in updatable && updatable.refusal.kind === "npm") {
177
+ this.#failure = updatable.refusal.failure;
178
+ }
179
+ }
180
+ catch (error) {
181
+ logger.warning(`the update check failed: ${errorName(error)}`);
182
+ this.#failure = { kind: "other", status: null };
183
+ }
184
+ this.#checkedAt = this.#now();
185
+ this.#changed();
186
+ return complete;
187
+ }
188
+ /** The state of a check, made once if none has run yet. */
189
+ async current() {
190
+ if (this.#checkedAt === null || this.#checking !== null)
191
+ await this.check();
192
+ }
193
+ /**
194
+ * Asks the host again whether the copy updates, and keeps the answer; `swapping` (the copy is
195
+ * being replaced by another process) keeps the previous one and the count of swaps; any other
196
+ * answer restarts that count (`UPDATE_RETRY_LIMIT`). Null when a drain or the shutdown
197
+ * ended the question: what npm answered to its SIGTERM is no answer about the copy.
198
+ */
199
+ async validate() {
200
+ const signal = AbortSignal.any([this.#checkEnd.signal, this.#drainStart.signal]);
201
+ const found = await this.host.updatable(signal);
202
+ if (signal.aborted)
203
+ return null;
204
+ if (!("swapping" in found)) {
205
+ this.#updatable = found;
206
+ this.#swaps = 0;
207
+ }
208
+ this.#changed();
209
+ return found;
210
+ }
211
+ /**
212
+ * Claims the update of `version`: false while another is installing or waiting to restart, and
213
+ * once the stop cancelled the install.
214
+ */
215
+ begin(version) {
216
+ if (this.#installEnd.signal.aborted)
217
+ return false;
218
+ if (this.#phase.kind === "installing" || this.#phase.kind === "restarting")
219
+ return false;
220
+ this.#phase = { kind: "installing", version };
221
+ this.#changed();
222
+ return true;
223
+ }
224
+ /** The claim ended with nothing installed and nothing to say: back to what a check found. */
225
+ abandon() {
226
+ this.#phase = { kind: "idle" };
227
+ this.#changed();
228
+ }
229
+ /**
230
+ * `version` is installed: the daemon waits for running turns to finish, then restarts. `note`
231
+ * tells the instances that did not restart.
232
+ */
233
+ restarting(version, note = null) {
234
+ this.#phase = { kind: "restarting", version, note };
235
+ this.#changed();
236
+ }
237
+ /** The update of `version` did not happen; `text` says why, for a notice. */
238
+ fail(version, text) {
239
+ this.#phase = { kind: "failed", version, text };
240
+ this.#changed();
241
+ }
242
+ /**
243
+ * The version the owner has not been told of yet, once per version (`told` is the last one he
244
+ * was told); null when up to date, for a copy that does not update, while an update is under
245
+ * way, for a daemon no service runs, and before a check.
246
+ */
247
+ noticeDue(told) {
248
+ const latest = this.newer;
249
+ if (latest === null || !this.canUpdate || latest === told)
250
+ return null;
251
+ if (this.#phase.kind === "installing" || this.#phase.kind === "restarting")
252
+ return null;
253
+ // Asked last: it runs the service manager. A daemon no service runs cannot update, as a copy
254
+ // that updates outside the chat cannot (`!allaboard` says why).
255
+ return this.host.supervised() ? latest : null;
256
+ }
257
+ /**
258
+ * The line of the chat's own page; null when there is nothing to offer, the copy does not
259
+ * update, or no service runs this daemon.
260
+ */
261
+ line() {
262
+ if (!this.canUpdate)
263
+ return null;
264
+ const phase = this.#phase;
265
+ const latest = phase.kind === "idle" ? this.newer : phase.version;
266
+ if (latest === null || !this.host.supervised())
267
+ return null;
268
+ return {
269
+ installed: this.installed,
270
+ latest,
271
+ changelog: this.host.changelog(latest),
272
+ phase: phase.kind === "idle" ? "available" : phase.kind,
273
+ failure: phase.kind === "failed" ? phase.text : null,
274
+ note: phase.kind === "restarting" ? phase.note : null,
275
+ };
276
+ }
277
+ #changed() {
278
+ try {
279
+ this.onChange?.();
280
+ }
281
+ catch (error) {
282
+ logger.warning(`the page was not told of an update change: ${errorName(error)}`);
283
+ }
284
+ }
285
+ }
286
+ /**
287
+ * The scheduled check: one at start, then one every `seconds`, none once a stop has begun (the
288
+ * daemon is leaving, and the next start checks anyway). A check that could not finish (`check`
289
+ * resolved false) is asked again after `retrySeconds`. Ends when `signal` aborts; a check in
290
+ * flight ends with the shutdown (`Updates.endChecks`).
291
+ */
292
+ export async function checkEvery(options) {
293
+ for (;;) {
294
+ if (options.signal.aborted || options.sessions.draining)
295
+ return;
296
+ const complete = await options.check();
297
+ try {
298
+ await options.clock.sleep(complete ? options.seconds : (options.retrySeconds ?? options.seconds), options.signal);
299
+ }
300
+ catch {
301
+ return; // the stop: the only thing that ends a sleep
302
+ }
303
+ }
304
+ }
305
+ /** Why npm could not say what it was asked, as a clause after a colon. */
306
+ export function whyUnknown(failure) {
307
+ switch (failure.kind) {
308
+ case "network":
309
+ return texts.UPDATE_WHY_NETWORK;
310
+ case "permission":
311
+ return texts.UPDATE_WHY_PERMISSION;
312
+ case "other":
313
+ return fill(texts.UPDATE_WHY_STATUS, { status: failure.status ?? "unknown" });
314
+ }
315
+ }
316
+ /**
317
+ * The sentence that says which instances restart with this one, or null when none does: the
318
+ * other loaded instances on the copy (`UpdatesHost.sharing`), null being the default instance.
319
+ */
320
+ export function sharedCopyLine(sharing) {
321
+ const [only, second] = sharing;
322
+ if (only === undefined)
323
+ return null;
324
+ if (second === undefined) {
325
+ return only === null
326
+ ? texts.UPDATE_SHARED_DEFAULT
327
+ : fill(texts.UPDATE_SHARED_ONE, { name: only });
328
+ }
329
+ const names = sharing.map((name) => name === null ? texts.UPDATE_DEFAULT_INSTANCE : `\`${name}\``);
330
+ const joined = `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
331
+ return fill(texts.UPDATE_SHARED_MANY, { names: joined });
332
+ }