allaboard 0.0.0-stage → 0.4.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,266 @@
1
+ /**
2
+ * The primitives of the sessions: a task that can be cancelled, an event, a semaphore, a lock, a
3
+ * queue, and a wait that a signal ends. The rule they serve is the header of `session.ts`.
4
+ *
5
+ * A promise callback runs at the next `await` of the code that made it ready, whether or not
6
+ * that `await` waits for anything. So a new task starts, and the waiters of an event wake, on
7
+ * the next turn of the event loop (`setImmediate`), in the order they became ready: the code
8
+ * that made them ready reaches its own next real wait first.
9
+ */
10
+ import { getLogger } from "../../log.js";
11
+ import { errorName } from "../../values.js";
12
+ export const logger = getLogger("allaboard.core.sessions.tasks");
13
+ // A stack frame as V8 writes it: `at name (place:line:column)` or `at place:line:column`.
14
+ const FRAME = /^\s+at (?:.+ \()?[^\s()]+:\d+:\d+\)?$/;
15
+ /**
16
+ * Where an error was thrown, as a log line may say it: the first frame of its stack. The stack
17
+ * opens with the error's own words, which may run over several lines and may hold one that reads
18
+ * like a frame, so only what follows them is looked at, and only a line shaped as a frame.
19
+ */
20
+ function thrownAt(error) {
21
+ if (!(error instanceof Error) || typeof error.stack !== "string")
22
+ return "";
23
+ const words = error.stack.indexOf(error.message);
24
+ if (words === -1)
25
+ return "";
26
+ const frames = error.stack.slice(words + error.message.length).split("\n");
27
+ const frame = frames.find((line) => FRAME.test(line));
28
+ return frame === undefined ? "" : ` (${frame.trim()})`;
29
+ }
30
+ /** Resolves on the next turn of the event loop, after every promise callback that is ready. */
31
+ function nextTurn() {
32
+ return new Promise((resolve) => {
33
+ setImmediate(resolve);
34
+ });
35
+ }
36
+ /** What a wait rejects with when its signal aborts. */
37
+ export class Cancelled extends Error {
38
+ constructor() {
39
+ super("cancelled");
40
+ this.name = "Cancelled";
41
+ }
42
+ }
43
+ /**
44
+ * `promise`, for a caller that may stop waiting: rejects with the signal's reason when it aborts
45
+ * first. The promise itself goes on: what it began runs to its end.
46
+ */
47
+ export function cancellable(promise, signal) {
48
+ if (signal === undefined)
49
+ return promise;
50
+ return new Promise((resolve, reject) => {
51
+ const onAbort = () => reject(signal.reason);
52
+ if (signal.aborted) {
53
+ // Whoever began it still owns its outcome: no unhandled rejection from this side.
54
+ promise.catch(() => { });
55
+ onAbort();
56
+ return;
57
+ }
58
+ signal.addEventListener("abort", onAbort, { once: true });
59
+ promise.then((value) => {
60
+ signal.removeEventListener("abort", onAbort);
61
+ resolve(value);
62
+ }, (error) => {
63
+ signal.removeEventListener("abort", onAbort);
64
+ reject(error);
65
+ });
66
+ });
67
+ }
68
+ /**
69
+ * A task. The function starts on the next turn of the event loop, never inside the call that
70
+ * made the task, and the sessions set state right after creating one. A task cancelled before
71
+ * it started never runs. With `immediate`, the function has started running when the task is
72
+ * made, which is the rule of the chat provider's passes (the header of `sinks.ts`).
73
+ * `cancel` aborts the task's signal with `Cancelled`; the function decides which of its waits
74
+ * take it. A task that fails rejects `result()` and never surfaces as an unhandled rejection.
75
+ * When nobody has asked for its result, the failure is logged by its name and by where it was
76
+ * thrown, so that a task that broke leaves a trace.
77
+ */
78
+ export class Task {
79
+ controller = new AbortController();
80
+ promise;
81
+ finished = false;
82
+ retrieved = false;
83
+ constructor(run, options = {}) {
84
+ this.promise = this.start(run, options.immediate === true);
85
+ this.promise.catch((error) => {
86
+ if (this.retrieved || error instanceof Cancelled)
87
+ return;
88
+ logger.error(`a task failed: ${errorName(error)}${thrownAt(error)}`);
89
+ });
90
+ }
91
+ async start(run, immediate) {
92
+ try {
93
+ if (!immediate) {
94
+ await nextTurn();
95
+ this.controller.signal.throwIfAborted();
96
+ }
97
+ return await run(this.controller.signal);
98
+ }
99
+ finally {
100
+ this.finished = true;
101
+ }
102
+ }
103
+ get signal() {
104
+ return this.controller.signal;
105
+ }
106
+ /** Whether the function has returned or thrown. */
107
+ get done() {
108
+ return this.finished;
109
+ }
110
+ cancel() {
111
+ if (!this.finished && !this.controller.signal.aborted)
112
+ this.controller.abort(new Cancelled());
113
+ }
114
+ /** The function's own outcome: whoever asks for it answers for its failure. */
115
+ result() {
116
+ this.retrieved = true;
117
+ return this.promise;
118
+ }
119
+ /** Resolves when the task ends, however it ends: awaiting a task one has just cancelled. */
120
+ settled() {
121
+ return this.promise.then(() => { }, () => { });
122
+ }
123
+ }
124
+ /**
125
+ * An event. `set` wakes its waiters on the next turn of the event loop: whoever set it goes on
126
+ * to its own next real wait first, so a waiter never sees the state of the setter half-way
127
+ * through what it was doing. A waiter whose signal aborts leaves and rejects with the signal's
128
+ * reason.
129
+ */
130
+ export class Event {
131
+ value = false;
132
+ waiters = new Set();
133
+ get isSet() {
134
+ return this.value;
135
+ }
136
+ set() {
137
+ this.value = true;
138
+ if (this.waiters.size === 0)
139
+ return;
140
+ const waiting = [...this.waiters];
141
+ this.waiters.clear();
142
+ setImmediate(() => {
143
+ for (const wake of waiting)
144
+ wake();
145
+ });
146
+ }
147
+ clear() {
148
+ this.value = false;
149
+ }
150
+ wait(signal) {
151
+ if (signal?.aborted)
152
+ return Promise.reject(signal.reason);
153
+ if (this.value)
154
+ return Promise.resolve();
155
+ return new Promise((resolve, reject) => {
156
+ const wake = () => {
157
+ signal?.removeEventListener("abort", onAbort);
158
+ resolve();
159
+ };
160
+ const onAbort = () => {
161
+ this.waiters.delete(wake);
162
+ reject(signal?.reason);
163
+ };
164
+ signal?.addEventListener("abort", onAbort, { once: true });
165
+ this.waiters.add(wake);
166
+ });
167
+ }
168
+ }
169
+ /**
170
+ * A counting gate: at most `limit` holders at once, the others served in the order they
171
+ * came. A place given back goes straight to the first in line, so it is never free for a
172
+ * newcomer to take ahead of a waiter.
173
+ */
174
+ export class Semaphore {
175
+ free;
176
+ waiters = [];
177
+ constructor(limit) {
178
+ this.free = limit;
179
+ }
180
+ /**
181
+ * Resolves with the function that gives the place back, once it is this caller's turn; calling
182
+ * that function twice gives back one place. A waiter whose signal aborts leaves the line and
183
+ * rejects with the signal's reason.
184
+ */
185
+ acquire(signal) {
186
+ if (signal?.aborted)
187
+ return Promise.reject(signal.reason);
188
+ if (this.free > 0) {
189
+ this.free -= 1;
190
+ return Promise.resolve(this.releaser());
191
+ }
192
+ return new Promise((resolve, reject) => {
193
+ const turn = () => {
194
+ signal?.removeEventListener("abort", onAbort);
195
+ resolve(this.releaser());
196
+ };
197
+ const onAbort = () => {
198
+ const index = this.waiters.indexOf(turn);
199
+ if (index !== -1)
200
+ this.waiters.splice(index, 1);
201
+ reject(signal?.reason);
202
+ };
203
+ signal?.addEventListener("abort", onAbort, { once: true });
204
+ this.waiters.push(turn);
205
+ });
206
+ }
207
+ releaser() {
208
+ let released = false;
209
+ return () => {
210
+ if (released)
211
+ return;
212
+ released = true;
213
+ const next = this.waiters.shift();
214
+ if (next === undefined)
215
+ this.free += 1;
216
+ else
217
+ next();
218
+ };
219
+ }
220
+ /** `work` while holding a place, given back however `work` ends. */
221
+ async run(work, signal) {
222
+ const release = await this.acquire(signal);
223
+ try {
224
+ return await work();
225
+ }
226
+ finally {
227
+ release();
228
+ }
229
+ }
230
+ }
231
+ /** A lock: a semaphore of one place. */
232
+ export class Mutex extends Semaphore {
233
+ constructor() {
234
+ super(1);
235
+ }
236
+ }
237
+ /** A queue, unbounded, for one consumer. */
238
+ export class Queue {
239
+ items = [];
240
+ filled = new Event();
241
+ get size() {
242
+ return this.items.length;
243
+ }
244
+ get empty() {
245
+ return this.items.length === 0;
246
+ }
247
+ putNowait(item) {
248
+ this.items.push(item);
249
+ this.filled.set();
250
+ }
251
+ /** The next item now; throws on an empty queue, as `get_nowait` raised. */
252
+ getNowait() {
253
+ const item = this.items.shift();
254
+ if (item === undefined)
255
+ throw new RangeError("the queue is empty");
256
+ if (this.items.length === 0)
257
+ this.filled.clear();
258
+ return item;
259
+ }
260
+ /** The next item, waiting for one; the wait leaves through its signal. */
261
+ async get(signal) {
262
+ while (this.items.length === 0)
263
+ await this.filled.wait(signal);
264
+ return this.getNowait();
265
+ }
266
+ }
@@ -0,0 +1,45 @@
1
+ /** One prompt and its reply, the turn that is running, and the notes about prompts that never ran. */
2
+ import { randomUUID } from "node:crypto";
3
+ import { blank } from "../../text.js";
4
+ import { oneLine } from "../reply/words.js";
5
+ import * as texts from "../texts.js";
6
+ import { ASKED_LIMIT, NOT_SENT_START } from "./constants.js";
7
+ import { Event } from "./tasks.js";
8
+ export class Turn {
9
+ prompt;
10
+ sink;
11
+ done = new Event();
12
+ /** The daemon's own id for the prompt, which the agent names when it takes it. */
13
+ uuid = randomUUID();
14
+ constructor(prompt, sink) {
15
+ this.prompt = prompt;
16
+ this.sink = sink;
17
+ }
18
+ }
19
+ /** The owner's question on one line, as a note about a message that was not sent quotes it. */
20
+ export function asked(prompt) {
21
+ const text = typeof prompt === "string"
22
+ ? prompt
23
+ : prompt
24
+ .filter((part) => part.type === "text")
25
+ .map((part) => part.text)
26
+ .join(" ");
27
+ return blank(text) ? texts.PROMPT_IMAGE : oneLine(text, ASKED_LIMIT);
28
+ }
29
+ /**
30
+ * The note that names turns that will never be sent: how many, why, and the start of each, so
31
+ * the owner knows which to send again.
32
+ */
33
+ export function notSent(turns, because) {
34
+ const header = texts.fill(turns.length === 1 ? texts.NOT_SENT_ONE : texts.NOT_SENT_MANY, {
35
+ count: turns.length,
36
+ because,
37
+ });
38
+ const starts = turns.map((turn) => `- "${oneLine(asked(turn.prompt), NOT_SENT_START)}"`);
39
+ return [header, ...starts].join("\n");
40
+ }
41
+ /** The note for the reply a turn that took `count` prompts into itself ends with. */
42
+ export function takenNote(count, names) {
43
+ const note = count === 1 ? texts.TAKEN_INTO_REPLY_ONE : texts.TAKEN_INTO_REPLY_MANY;
44
+ return texts.fill(texts.named(note, names), { count });
45
+ }
@@ -0,0 +1,28 @@
1
+ export const DEFAULT = "default";
2
+ export const DEFAULT_CHOICE = { model: DEFAULT, effort: DEFAULT, bypass: false };
3
+ /** The levels `model` accepts, in the agent's order; empty for a model without effort. */
4
+ export function effortLevels(models, model) {
5
+ const entry = models.find((m) => m.value === model && m.supportsEffort);
6
+ return entry === undefined ? [] : entry.supportedEffortLevels;
7
+ }
8
+ /**
9
+ * The owner's choice from a selection. A model the agent did not list reads as its default; an
10
+ * effort the chosen model does not support reads as `Default`, which is what the message shows
11
+ * after a model change.
12
+ */
13
+ export function choiceOf(models, selection) {
14
+ const listed = selection.model === DEFAULT || models.some((m) => m.value === selection.model);
15
+ const model = listed ? selection.model : DEFAULT;
16
+ const effort = effortLevels(models, model).includes(selection.effort)
17
+ ? selection.effort
18
+ : DEFAULT;
19
+ return { model, effort, bypass: selection.bypass };
20
+ }
21
+ export function summaryOf(models, choice) {
22
+ const listed = models.find((m) => m.value === choice.model);
23
+ return {
24
+ model: listed === undefined ? choice.model : listed.displayName,
25
+ effort: choice.effort === DEFAULT ? null : choice.effort,
26
+ bypass: choice.bypass,
27
+ };
28
+ }