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
package/dist/log.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The daemon's log: lines on standard error, which the service writes to the log file. A line
3
+ * carries ids, counts and error names, never the text of a prompt or of a reply.
4
+ */
5
+ const ORDER = ["DEBUG", "INFO", "WARNING", "ERROR"];
6
+ let threshold = "INFO";
7
+ let write = (line) => {
8
+ process.stderr.write(`${line}\n`);
9
+ };
10
+ /** The lowest level that is written; `INFO` until set. */
11
+ export function setLevel(level) {
12
+ threshold = level;
13
+ }
14
+ /** Where the lines go, for a test that reads them; returns the writer it replaced. */
15
+ export function setWriter(writer) {
16
+ const previous = write;
17
+ write = writer;
18
+ return previous;
19
+ }
20
+ function two(value) {
21
+ return String(value).padStart(2, "0");
22
+ }
23
+ /** `2026-10-10 09:05:03,042`, local time. */
24
+ export function timestamp(at) {
25
+ const day = `${at.getFullYear()}-${two(at.getMonth() + 1)}-${two(at.getDate())}`;
26
+ const time = `${two(at.getHours())}:${two(at.getMinutes())}:${two(at.getSeconds())}`;
27
+ return `${day} ${time},${String(at.getMilliseconds()).padStart(3, "0")}`;
28
+ }
29
+ /** A logger whose lines read `<time> <LEVEL> <name>: <message>`. */
30
+ export function getLogger(name) {
31
+ const at = (level) => (message) => {
32
+ if (ORDER.indexOf(level) < ORDER.indexOf(threshold))
33
+ return;
34
+ write(`${timestamp(new Date())} ${level} ${name}: ${message}`);
35
+ };
36
+ return { debug: at("DEBUG"), info: at("INFO"), warning: at("WARNING"), error: at("ERROR") };
37
+ }
package/dist/main.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `allaboard`: the file `bin` in `package.json` names, and the one the LaunchAgent runs. It checks
4
+ * the Node, then loads the daemon (`daemon.ts`).
5
+ *
6
+ * It has no static import, and the modules it loads before it knows the Node is new enough
7
+ * (`node-version.ts`, `version.ts`, `log.ts`) name `node:fs` and each other only. An ES module
8
+ * graph is linked whole before any module runs: a named import that an old Node lacks, such as
9
+ * `parseEnv` of `node:util` on Node 18, would end the process with a `SyntaxError` at link time,
10
+ * before a guard anywhere in the graph could run (measured on 2026-10-17, Node 18.19.0). The
11
+ * daemon's own graph is loaded only after the check. `test/node-version.test.ts` holds both
12
+ * rules.
13
+ *
14
+ * The file is the process's entry whenever it loads, a symlink in `node_modules/.bin` included, so
15
+ * nothing here depends on `import.meta.main`, which an older Node lacks (it is `@since v22.18.0`).
16
+ */
17
+ const { nodeRefusal } = await import("./node-version.js");
18
+ const refusal = nodeRefusal(process.versions.node);
19
+ if (refusal === null) {
20
+ const { start } = await import("./daemon.js");
21
+ await start();
22
+ }
23
+ else {
24
+ const { getLogger } = await import("./log.js");
25
+ getLogger("allaboard.main").error(refusal);
26
+ // A status, not `process.exit`: a write to a pipe can still be pending on macOS.
27
+ process.exitCode = 1;
28
+ }
29
+ export {};
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The oldest Node that runs allaboard, and the sentence that refuses an older one. The entry reads
3
+ * it before it loads the daemon (`main.ts`). `test/node-version.test.ts` holds `MINIMUM_NODE` to
4
+ * `engines.node` in `package.json`. This file and what it imports are loaded on every Node, so
5
+ * they name `node:fs` and each other only.
6
+ */
7
+ import { SETUP_SECTIONS, setupGuide } from "./version.js";
8
+ // The floor `undici` 8 declares (`engines.node`). No Node API the source uses is newer than Node
9
+ // 20.12 (`util.parseEnv`); the dependencies set it. `@slack/socket-mode` takes `undici` 7 or 8 as a
10
+ // peer, and the lockfile, which the tests run, holds 8. On a lower Node 22 whether npm installs 7
11
+ // or 8 depends on the user's npm (measured on 2026-10-17: npm 10.5.1 installs 8, which fails to
12
+ // load on 22.0.0), so the floor follows the version the tests run, not one npm happens to pick.
13
+ export const MINIMUM_NODE = "22.19.0";
14
+ /** `major.minor.patch` as three numbers; null for a text that does not start with them. */
15
+ function parts(version) {
16
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
17
+ if (match === null)
18
+ return null;
19
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
20
+ }
21
+ /**
22
+ * The refusal for a Node whose `process.versions.node` is `found`, or null when it is at least
23
+ * `MINIMUM_NODE`. Compared number by number (22.9.0 is below 22.19.0); a pre-release suffix is
24
+ * ignored, and a text that holds no version is not refused, since nothing says it is too old.
25
+ */
26
+ export function nodeRefusal(found) {
27
+ const have = parts(found);
28
+ const need = parts(MINIMUM_NODE);
29
+ if (have === null)
30
+ return null;
31
+ for (const [index, wanted] of need.entries()) {
32
+ const actual = have[index];
33
+ if (actual > wanted)
34
+ return null;
35
+ if (actual < wanted) {
36
+ return `allaboard needs Node ${MINIMUM_NODE} or later and this is Node ${found}; to run it on a newer Node without changing this one, see ${setupGuide(SETUP_SECTIONS.nodeOfItsOwn)}`;
37
+ }
38
+ }
39
+ return null;
40
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The agents and the chats the daemon can run, by the name `ALLABOARD_AGENT` and `ALLABOARD_CHAT`
3
+ * select them with. The one file outside the two modules that names them: `main` takes each
4
+ * module from here, and a test passes its own registry through `run`'s options.
5
+ */
6
+ import { claudeAgent } from "./agent/claude/module.js";
7
+ import { slackChat } from "./chat/slack/module.js";
8
+ export const REGISTRY = {
9
+ agents: { claude: claudeAgent() },
10
+ chats: { slack: slackChat() },
11
+ defaults: { agent: "claude", chat: "slack" },
12
+ };
package/dist/semver.js ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Semantic Versioning 2.0.0 as npm publishes it: whether a text is a version, and which of two
3
+ * versions is newer. Any layer may import this file: the host asks npm for versions and the core
4
+ * decides whether one is newer than the one that runs.
5
+ */
6
+ // Semantic Versioning 2.0.0 without build metadata (`+...`), which npm drops anyway. A match
7
+ // starts with a digit, so npm can never read it as an option, and holds no space or quote.
8
+ const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?$/;
9
+ /** Whether `text` is a version number as npm publishes one: `0.2.0`, `1.0.0-rc.1`. */
10
+ export function isVersion(text) {
11
+ return VERSION.test(text);
12
+ }
13
+ /** Semantic Versioning 2.0.0, rule 11: the order of two pre-release identifiers. */
14
+ function identifierOrder(a, b) {
15
+ const numbers = [/^\d+$/.test(a), /^\d+$/.test(b)];
16
+ if (numbers[0] && numbers[1])
17
+ return Math.sign(Number(a) - Number(b));
18
+ // A numeric identifier is lower than an alphanumeric one.
19
+ if (numbers[0] !== numbers[1])
20
+ return numbers[0] ? -1 : 1;
21
+ return a < b ? -1 : a > b ? 1 : 0;
22
+ }
23
+ /**
24
+ * Whether `candidate` takes precedence over `current` by Semantic Versioning 2.0.0, rule 11. A
25
+ * string that is no version is never newer, and nothing is newer than it.
26
+ */
27
+ export function isNewer(candidate, current) {
28
+ if (!isVersion(candidate) || !isVersion(current))
29
+ return false;
30
+ const [cores, pres] = [candidate, current].map((text) => {
31
+ // The first hyphen ends the numbers; a pre-release may hold more of them.
32
+ const dash = text.indexOf("-");
33
+ const core = dash === -1 ? text : text.slice(0, dash);
34
+ return {
35
+ numbers: core.split(".").map(Number),
36
+ pre: dash === -1 ? null : text.slice(dash + 1).split("."),
37
+ };
38
+ });
39
+ for (let index = 0; index < 3; index += 1) {
40
+ const difference = cores.numbers[index] - pres.numbers[index];
41
+ if (difference !== 0)
42
+ return difference > 0;
43
+ }
44
+ // A release is newer than any pre-release of the same numbers.
45
+ if (cores.pre === null || pres.pre === null)
46
+ return cores.pre === null && pres.pre !== null;
47
+ for (let index = 0; index < Math.min(cores.pre.length, pres.pre.length); index += 1) {
48
+ const order = identifierOrder(cores.pre[index], pres.pre[index]);
49
+ if (order !== 0)
50
+ return order > 0;
51
+ }
52
+ return cores.pre.length > pres.pre.length;
53
+ }
package/dist/text.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Strings the way Python's `str` treats them, where JavaScript's `String` differs: what counts
3
+ * as whitespace, and the order of two strings, and the quoting of a word for a shell. The texts the daemon cuts and sorts come out as
4
+ * Python cut and sorted them, and the goldens hold those results. Any layer imports this.
5
+ */
6
+ // What Python's `str.split()`, `str.strip()` and `\s` take for whitespace (`str.isspace`), as
7
+ // ranges of code points: JavaScript's `\s` lacks U+001C to U+001F and U+0085, and adds U+FEFF.
8
+ const SPACE_RANGES = [
9
+ [0x09, 0x0d],
10
+ [0x1c, 0x20],
11
+ [0x85, 0x85],
12
+ [0xa0, 0xa0],
13
+ [0x1680, 0x1680],
14
+ [0x2000, 0x200a],
15
+ [0x2028, 0x2029],
16
+ [0x202f, 0x202f],
17
+ [0x205f, 0x205f],
18
+ [0x3000, 0x3000],
19
+ ];
20
+ /** The UTF-16 unit `code` as it is written in a pattern built from codes. */
21
+ export function escaped(code) {
22
+ return `\\u${code.toString(16).padStart(4, "0")}`;
23
+ }
24
+ /** The body of a regular expression class that matches Python's whitespace: `[${PY_SPACE}]`. */
25
+ export const PY_SPACE = SPACE_RANGES.map(([first, last]) => first === last ? escaped(first) : `${escaped(first)}-${escaped(last)}`).join("");
26
+ /** Whether the UTF-16 unit `code` is whitespace to Python: every such character is one unit. */
27
+ export function isSpace(code) {
28
+ return SPACE_RANGES.some(([first, last]) => code >= first && code <= last);
29
+ }
30
+ /**
31
+ * Python's `text.strip()`. Read by code from each end: a pattern that ends on `$` would start
32
+ * again from every run of spaces, which is quadratic on a long text.
33
+ */
34
+ export function strip(text) {
35
+ let start = 0;
36
+ let end = text.length;
37
+ while (start < end && isSpace(text.charCodeAt(start)))
38
+ start += 1;
39
+ while (end > start && isSpace(text.charCodeAt(end - 1)))
40
+ end -= 1;
41
+ return text.slice(start, end);
42
+ }
43
+ /** Whether `text` holds nothing but whitespace: Python's `not text.strip()`. */
44
+ export function blank(text) {
45
+ for (let i = 0; i < text.length; i += 1) {
46
+ if (!isSpace(text.charCodeAt(i)))
47
+ return false;
48
+ }
49
+ return true;
50
+ }
51
+ /** Python's `sorted` on strings: by code point, where `Array.prototype.sort` goes by UTF-16 unit. */
52
+ export function compareCodePoints(a, b) {
53
+ const left = Array.from(a);
54
+ const right = Array.from(b);
55
+ for (let i = 0; i < Math.min(left.length, right.length); i += 1) {
56
+ const x = left[i];
57
+ const y = right[i];
58
+ if (x !== y)
59
+ return x.codePointAt(0) - y.codePointAt(0);
60
+ }
61
+ return left.length - right.length;
62
+ }
63
+ /** Whether the UTF-16 unit `code` opens a surrogate pair. */
64
+ export function isHigh(code) {
65
+ return code >= 0xd800 && code <= 0xdbff;
66
+ }
67
+ /** Whether the UTF-16 unit `code` closes a surrogate pair. */
68
+ export function isLow(code) {
69
+ return code >= 0xdc00 && code <= 0xdfff;
70
+ }
71
+ /** Python's `len(text)`: the code points of a text. */
72
+ export function len(text) {
73
+ let count = text.length;
74
+ for (let i = 0; i < text.length - 1; i += 1) {
75
+ if (isHigh(text.charCodeAt(i)) && isLow(text.charCodeAt(i + 1))) {
76
+ count -= 1;
77
+ i += 1;
78
+ }
79
+ }
80
+ return count;
81
+ }
82
+ /** `shlex.quote`: the word as one shell argument, quoted only when it needs to be. */
83
+ export function shellQuote(word) {
84
+ if (word === "")
85
+ return "''";
86
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(word))
87
+ return word;
88
+ return `'${word.replaceAll("'", "'\"'\"'")}'`;
89
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The machine as the core's update sees it (`UpdatesHost`, issue #361): the daemon's adapter from
3
+ * the host layer (npm, the service manager, the update lock) to the questions the core asks. One
4
+ * of the entry files above the layers, since it imports both sides.
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { machineHost } from "./cli.js";
9
+ import { UpdateUnderWay, updateHeld, updateLock } from "./core/lock.js";
10
+ import { installVersion, latestVersion, updatableCopy } from "./host/npm.js";
11
+ import { programOf } from "./host/program.js";
12
+ import { runAsync } from "./host/run.js";
13
+ import { logPath } from "./host/service/launchd.js";
14
+ import { loadedOn } from "./host/service/listing.js";
15
+ import { getLogger } from "./log.js";
16
+ import { errorCode, errorName } from "./values.js";
17
+ import { changelogOf, packageVersion } from "./version.js";
18
+ export const logger = getLogger("allaboard.updates-host");
19
+ /** The machine allaboard runs on. The entry is read when asked: a test never reaches it. */
20
+ export function realMachine() {
21
+ return {
22
+ run: runAsync,
23
+ entry: () => programOf(process.argv[1] ?? "", process.env.PATH ?? "", process.execPath).main,
24
+ service: (name) => machineHost.service({ name }),
25
+ };
26
+ }
27
+ /** `UpdatesHost` over `options.machine`. */
28
+ export function updatesHost(options) {
29
+ const { machine, instance, home, installed } = options;
30
+ const manager = machine.service(instance.name);
31
+ let supervisor = null;
32
+ /** The other loaded instances on `copy`. */
33
+ const othersOn = (copy) => {
34
+ const listing = manager?.instances();
35
+ if (listing === undefined || "failure" in listing)
36
+ return [];
37
+ return loadedOn(listing.rows, copy).filter((row) => row.instance !== instance.name);
38
+ };
39
+ return {
40
+ installed,
41
+ logFile: logPath(home, instance.name),
42
+ latest: (signal) => latestVersion(machine.run, signal),
43
+ async updatable(signal) {
44
+ // While another process's `npm install -g` replaces the folder, the entry file is not
45
+ // there (npm moves the old folder away and fills a new one): that is no answer about the
46
+ // copy, and the host does not guess one.
47
+ let main;
48
+ try {
49
+ main = machine.entry();
50
+ }
51
+ catch (error) {
52
+ if (errorCode(error) === "ENOENT")
53
+ return { swapping: true };
54
+ throw error;
55
+ }
56
+ if (!existsSync(main))
57
+ return { swapping: true };
58
+ return updatableCopy(machine.run, main, installed, signal);
59
+ },
60
+ copyVersion: (copy) => packageVersion(join(copy, "package.json")),
61
+ // A system with no service manager has nothing that starts the daemon again.
62
+ supervised() {
63
+ if (manager === null)
64
+ return false;
65
+ // Which process started this one cannot change while it lives, so a pid the manager reports
66
+ // settles it for good; the pages ask on every redraw, and the question is a blocking
67
+ // `launchctl list`. No pid is no answer: "not running" and "could not ask" look alike.
68
+ if (supervisor === null) {
69
+ const pid = manager.runningPid();
70
+ if (pid === null)
71
+ return false;
72
+ supervisor = pid === process.pid;
73
+ }
74
+ return supervisor;
75
+ },
76
+ sharing: (copy) => othersOn(copy).map((row) => row.instance),
77
+ async lock() {
78
+ try {
79
+ return await updateLock(home);
80
+ }
81
+ catch (error) {
82
+ if (error instanceof UpdateUnderWay)
83
+ return null;
84
+ throw error;
85
+ }
86
+ },
87
+ async updateHeld() {
88
+ try {
89
+ return await updateHeld(home);
90
+ }
91
+ catch (error) {
92
+ // Not an update under way: a daemon the system cannot answer for would otherwise start
93
+ // nothing at all.
94
+ logger.warning(`could not tell whether an update is under way: ${errorName(error)}`);
95
+ return false;
96
+ }
97
+ },
98
+ install: (copy, version, signal) => installVersion(machine.run, copy, version, signal),
99
+ async restartOthers(copy) {
100
+ const failed = [];
101
+ for (const row of othersOn(copy).filter((other) => other.pid !== null)) {
102
+ const report = machine.service(row.instance)?.restart();
103
+ if (report === undefined || report.code !== 0) {
104
+ logger.warning(`could not restart instance ${row.instance ?? "(default)"}: ${report?.err.join(" ") ?? "no service manager"}`);
105
+ failed.push({ instance: row.instance, status: report?.code ?? null });
106
+ }
107
+ }
108
+ return failed;
109
+ },
110
+ restart: options.restart,
111
+ changelog: changelogOf,
112
+ };
113
+ }
package/dist/values.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Values nobody typed: what a parser returned and what a `catch` caught. Read by what they are
3
+ * at run time, never by a cast. Any layer imports this.
4
+ */
5
+ /** Whether `value` is a JSON object: not null, not an array. */
6
+ export function isRecord(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+ /** The JSON objects of a list; none for anything that is no list. */
10
+ export function records(value) {
11
+ return Array.isArray(value) ? value.filter(isRecord) : [];
12
+ }
13
+ /** The errno code of a system error (`ENOENT`, `EACCES`); undefined for anything else. */
14
+ export function errorCode(error) {
15
+ return error instanceof Error &&
16
+ "code" in error &&
17
+ typeof error.code === "string" &&
18
+ error.code !== ""
19
+ ? error.code
20
+ : undefined;
21
+ }
22
+ /**
23
+ * The name of a failure, all a log line or a reply may say of it, never its words: the code of
24
+ * a system error (`EACCES`), else the name the error gives itself (a `ChatError` is named by
25
+ * the chat's code). Never its class: a bundled library's is a minified name that changes with
26
+ * each release, so the daemon's own error classes set `name`.
27
+ */
28
+ export function errorName(error) {
29
+ return error instanceof Error ? (errorCode(error) ?? error.name) : typeof error;
30
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The version of allaboard that runs, and the address of its setup guide. The version is the
3
+ * `version` of the package's own `package.json`, which sits one level above this file both in the
4
+ * sources (`src/`) and in the installed package (`dist/`), so a run from a checkout reads the
5
+ * checkout's and an installed copy its own. Any layer may import this file: a text that sends the
6
+ * owner to the guide names the page of the version that runs, since the package no longer carries
7
+ * `docs/`.
8
+ */
9
+ import { readFileSync } from "node:fs";
10
+ const REPOSITORY = "https://github.com/duqaXxX/allaboard";
11
+ /**
12
+ * The sections of `docs/setup.md` a text of the core sends the owner to, by their anchor on
13
+ * GitHub; a module keeps the anchors of its own sections. `test/version.test.ts` holds each to a
14
+ * heading of the page.
15
+ */
16
+ export const SETUP_SECTIONS = {
17
+ nodeOfItsOwn: "a-node-of-its-own",
18
+ configuration: "part-2-configuration",
19
+ protectedFolders: "folders-macos-protects",
20
+ };
21
+ let version = null;
22
+ /**
23
+ * The `version` of the package whose `package.json` is `manifest`, or null when it cannot be read
24
+ * or has none. It names `node:fs` alone: this file is loaded before the Node is known to be new
25
+ * enough (`main.ts`).
26
+ */
27
+ export function packageVersion(manifest) {
28
+ try {
29
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
30
+ const found = typeof parsed === "object" && parsed !== null && "version" in parsed ? parsed.version : null;
31
+ return typeof found === "string" && found !== "" ? found : null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /** The `version` field of the package's `package.json`; throws when the file has none. */
38
+ export function installedVersion() {
39
+ if (version !== null)
40
+ return version;
41
+ const found = packageVersion(new URL("../package.json", import.meta.url));
42
+ if (found === null)
43
+ throw new Error("package.json has no version");
44
+ version = found;
45
+ return version;
46
+ }
47
+ /** The address of the changelog as published with `version`: what that version changed. */
48
+ export function changelogOf(version) {
49
+ return `${REPOSITORY}/blob/v${version}/docs/CHANGELOG.md`;
50
+ }
51
+ /** The address of `section` of the setup guide, as published with the version that runs. */
52
+ export function setupGuide(section) {
53
+ return `${REPOSITORY}/blob/v${installedVersion()}/docs/setup.md#${section}`;
54
+ }
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "allaboard",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.5.0",
4
+ "description": "Your Claude Code workspace in Slack: one channel per project, one thread per session, running on your own Mac.",
5
+ "keywords": [
6
+ "slack",
7
+ "coding-agent",
8
+ "claude-code",
9
+ "agent-sdk",
10
+ "remote",
11
+ "daemon"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/duqaXxX/allaboard.git"
17
+ },
18
+ "type": "module",
19
+ "bin": {
20
+ "allaboard": "dist/main.js"
21
+ },
22
+ "engines": {
23
+ "node": ">=22.19.0"
24
+ },
25
+ "dependencies": {
26
+ "@anthropic-ai/claude-agent-sdk": "0.3.302",
27
+ "@slack/bolt": "5.1.0",
28
+ "@slack/web-api": "8.2.0"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "slack-app-manifest.json"
33
+ ]
34
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "display_information": {
3
+ "name": "allaboard",
4
+ "description": "Drive the coding-agent sessions on your own computer from private Slack channels.",
5
+ "background_color": "#1f2328"
6
+ },
7
+ "features": {
8
+ "app_home": {
9
+ "home_tab_enabled": true,
10
+ "messages_tab_enabled": true,
11
+ "messages_tab_read_only_enabled": true
12
+ },
13
+ "bot_user": {
14
+ "display_name": "allaboard",
15
+ "always_online": true
16
+ }
17
+ },
18
+ "oauth_config": {
19
+ "scopes": {
20
+ "bot": [
21
+ "groups:history",
22
+ "groups:read",
23
+ "chat:write",
24
+ "files:read",
25
+ "files:write",
26
+ "reactions:write"
27
+ ],
28
+ "user": [
29
+ "chat:write"
30
+ ]
31
+ },
32
+ "pkce_enabled": false
33
+ },
34
+ "settings": {
35
+ "event_subscriptions": {
36
+ "bot_events": [
37
+ "message.groups",
38
+ "file_change"
39
+ ]
40
+ },
41
+ "interactivity": {
42
+ "is_enabled": true
43
+ },
44
+ "org_deploy_enabled": false,
45
+ "socket_mode_enabled": true,
46
+ "token_rotation_enabled": false,
47
+ "app_level_token_rotation_enabled": false,
48
+ "is_mcp_enabled": false
49
+ }
50
+ }