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,24 @@
1
+ /**
2
+ * Where a thread's list of what still runs is drawn. A pure function of the order of the
3
+ * messages, so a provider can ask it again after every change. The footer is not placed here: the
4
+ * session decides it, once, and a provider draws it under the app's last message.
5
+ */
6
+ /**
7
+ * For each entry, what it shows: the list goes to the lowest reply of the thread once it rests,
8
+ * and to no reply while that one is still written. Left on a reply above it, the list sat
9
+ * between that reply and the one being written, then jumped below it (seen on Slack desktop,
10
+ * 2026-10-15).
11
+ */
12
+ export function tails(entries) {
13
+ const lowest = entries.reduce((found, entry) => (found === undefined || entry.at > found.at ? entry : found), undefined);
14
+ const holder = lowest?.resting ? lowest : undefined;
15
+ return new Map(entries.map((entry) => [entry.key, { running: entry === holder }]));
16
+ }
17
+ /** Nothing of the owner's and no other reply sits below `at`. */
18
+ export function isLast(at, others, ownerMarks) {
19
+ return others.every((other) => other < at) && ownerMarks.every((mark) => mark < at);
20
+ }
21
+ /** Another reply has a message below `at`. */
22
+ export function isOvertaken(at, others) {
23
+ return others.some((other) => other > at);
24
+ }
@@ -0,0 +1,32 @@
1
+ /** The words a reply's lines share, whoever writes them: the renderer, the fold, a chat provider. */
2
+ import { escaped, PY_SPACE } from "../../text.js";
3
+ export const BACKGROUND = "Running in background";
4
+ export const STOPPED = "Stopped";
5
+ // The most of a title one line shows, a call's in a reply or a session's in a list.
6
+ export const TITLE_LIMIT = 80;
7
+ const SPACES = new RegExp(`[${PY_SPACE}]+`);
8
+ /** `value` with its runs of whitespace as one space and none at its ends: `" ".join(value.split())`. */
9
+ export function spaced(value) {
10
+ return value
11
+ .split(SPACES)
12
+ .filter((word) => word !== "")
13
+ .join(" ");
14
+ }
15
+ // The line boundaries of Python's `str.splitlines`: more than `\n` and `\r`, and not the unit
16
+ // separator U+001F, which is whitespace and no boundary.
17
+ const LINE_END_CODES = [0x0a, 0x0b, 0x0c, 0x0d, 0x1c, 0x1d, 0x1e, 0x85, 0x2028, 0x2029];
18
+ const LINE_ENDS = new RegExp(`\\r\\n|[${LINE_END_CODES.map(escaped).join("")}]`);
19
+ /** The lines of `text` as Python's `str.splitlines()` cuts them: a boundary at the end adds none. */
20
+ export function splitLines(text) {
21
+ const lines = text.split(LINE_ENDS);
22
+ if (lines.at(-1) === "")
23
+ lines.pop();
24
+ return lines;
25
+ }
26
+ /** `value` on one line, its runs of whitespace as one space, cut with `…` past `limit` characters. */
27
+ export function oneLine(value, limit) {
28
+ const text = spaced(value);
29
+ // Counted in code points: an emoji is one character.
30
+ const chars = Array.from(text);
31
+ return chars.length <= limit ? text : `${chars.slice(0, limit - 1).join("")}…`;
32
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * The agent's permission requests and questions waiting for the owner. This is the bookkeeping
3
+ * that belongs to no chat provider: which request is pending, who may resolve it, and what the
4
+ * answer is. A provider draws the request and reports what the owner chose.
5
+ *
6
+ * Kept in memory only: a request never outlives the process. The wait is a promise the agent
7
+ * side holds (`Pending.decision`), settled by `resolve` or `denyAll` and by nothing else.
8
+ */
9
+ import { randomBytes } from "node:crypto";
10
+ import { DENY_MESSAGE, named, SKIP_MESSAGE } from "./texts.js";
11
+ export const APPROVE = { kind: "approve" };
12
+ export const DENY = { kind: "deny" };
13
+ /**
14
+ * The answer the agent takes for what the owner chose. A refusal is a dismissal when the request
15
+ * was a question (`questions` is not null), so Claude is told the form was skipped, not denied.
16
+ */
17
+ export function toAnswer(outcome, questions, names) {
18
+ switch (outcome.kind) {
19
+ case "answer":
20
+ return { answered: true, answers: outcome.answers, notes: outcome.notes };
21
+ case "approve":
22
+ return { allow: true };
23
+ case "deny":
24
+ return questions !== null && questions.length > 0
25
+ ? { answered: false, message: SKIP_MESSAGE }
26
+ : { allow: false, message: named(DENY_MESSAGE, names) };
27
+ }
28
+ }
29
+ /**
30
+ * A question's answer as Claude Code takes it: the labels of the options picked and the text
31
+ * typed under Other, which is the answer itself (tools reference); null when there is neither.
32
+ * `picked` are indexes into `question.options`; one that names no option throws, since the
33
+ * form only offers the options the question has.
34
+ */
35
+ export function answerOf(question, picked, typed) {
36
+ let labels = picked.map((index) => {
37
+ const option = question.options[index];
38
+ if (option === undefined)
39
+ throw new RangeError(`no option ${index} in the question`);
40
+ return option.label;
41
+ });
42
+ if (typed)
43
+ labels = question.multiSelect ? [...labels, typed] : [typed];
44
+ if (labels.length === 0)
45
+ return null;
46
+ return question.multiSelect ? labels : labels[0];
47
+ }
48
+ /** Requests waiting for the owner, by an id that only the posted buttons carry. */
49
+ export class Approvals {
50
+ entries = new Map();
51
+ names;
52
+ /** `names` fill the message a denial carries to the agent. */
53
+ constructor(names) {
54
+ this.names = names;
55
+ }
56
+ /** Starts waiting for `title`'s request; `questions` makes it a question. */
57
+ open(channelId, threadId, title, questions = null) {
58
+ const approvalId = randomBytes(16).toString("base64url");
59
+ let settle = () => { };
60
+ const decision = new Promise((resolve) => {
61
+ settle = resolve;
62
+ });
63
+ const pending = {
64
+ channelId,
65
+ threadId,
66
+ title,
67
+ questions,
68
+ decision,
69
+ decided: false,
70
+ messageTs: null,
71
+ };
72
+ this.entries.set(approvalId, { pending, settle });
73
+ return [approvalId, pending];
74
+ }
75
+ get(approvalId) {
76
+ return this.entries.get(approvalId)?.pending ?? null;
77
+ }
78
+ /**
79
+ * The questions of the request `approvalId`, read from the thread it was posted in; null when
80
+ * the request is gone, belongs to another thread or has no questions.
81
+ */
82
+ questions(approvalId, channelId, threadId) {
83
+ const pending = this.get(approvalId);
84
+ if (pending === null ||
85
+ pending.channelId !== channelId ||
86
+ pending.threadId !== threadId ||
87
+ pending.questions === null ||
88
+ pending.questions.length === 0) {
89
+ return null;
90
+ }
91
+ return pending.questions;
92
+ }
93
+ /** Resolve once, and only from the channel and thread the request was posted in. */
94
+ resolve(approvalId, channelId, threadId, outcome) {
95
+ const entry = this.entries.get(approvalId);
96
+ if (entry === undefined ||
97
+ entry.pending.channelId !== channelId ||
98
+ entry.pending.threadId !== threadId ||
99
+ entry.pending.decided) {
100
+ return null;
101
+ }
102
+ this.decide(approvalId, entry, outcome);
103
+ return entry.pending;
104
+ }
105
+ /**
106
+ * Refuses the one request `approvalId` still open, for an agent that no longer waits for its
107
+ * answer; null when it was answered or removed already.
108
+ */
109
+ giveUp(approvalId) {
110
+ const entry = this.entries.get(approvalId);
111
+ if (entry === undefined || entry.pending.decided)
112
+ return null;
113
+ this.decide(approvalId, entry, DENY);
114
+ return entry.pending;
115
+ }
116
+ /** Denies every request still open in this thread, and returns them. */
117
+ denyAll(channelId, threadId) {
118
+ const denied = [];
119
+ for (const [approvalId, entry] of [...this.entries]) {
120
+ const { pending } = entry;
121
+ if (pending.channelId === channelId && pending.threadId === threadId && !pending.decided) {
122
+ this.decide(approvalId, entry, DENY);
123
+ denied.push(pending);
124
+ }
125
+ }
126
+ return denied;
127
+ }
128
+ /**
129
+ * A read-only snapshot of this thread's open requests, for a caller that needs to know which
130
+ * messages are still live without resolving them: `ThreadSession.close` captures these before
131
+ * cancelling its reader, since `denyAll`'s own list is empty by the time close reaches it
132
+ * (issue #19 fix round item 8).
133
+ */
134
+ pendingIn(channelId, threadId) {
135
+ return [...this.entries.values()]
136
+ .map((entry) => entry.pending)
137
+ .filter((pending) => pending.channelId === channelId && pending.threadId === threadId);
138
+ }
139
+ /**
140
+ * Record the message that shows a request, so it can be removed once decided; false when the
141
+ * request was decided before its message was known (`!stop` while posting).
142
+ */
143
+ posted(approvalId, messageTs) {
144
+ const entry = this.entries.get(approvalId);
145
+ if (entry === undefined)
146
+ return false;
147
+ entry.pending.messageTs = messageTs;
148
+ return true;
149
+ }
150
+ discard(approvalId) {
151
+ this.entries.delete(approvalId);
152
+ }
153
+ decide(approvalId, entry, outcome) {
154
+ entry.pending.decided = true;
155
+ entry.settle(toAnswer(outcome, entry.pending.questions, this.names));
156
+ this.entries.delete(approvalId);
157
+ }
158
+ }
@@ -0,0 +1,61 @@
1
+ import { Named } from "../texts.js";
2
+ // The events that belong to a turn: the first one that arrives with no turn running starts one
3
+ // (a stream event, an assistant or a user message, a result).
4
+ export const TURN_EVENTS = new Set([
5
+ "message_started",
6
+ "message_ended",
7
+ "text_started",
8
+ "text_delta",
9
+ "text",
10
+ "call_started",
11
+ "call_ended",
12
+ "agent_error",
13
+ "turn_ended",
14
+ ]);
15
+ export const TASK_EVENTS = new Set([
16
+ "task_started",
17
+ "task_progress",
18
+ "task_ended",
19
+ "task_updated",
20
+ ]);
21
+ // After a background task's notification, the CLI starts a turn of its own to report it
22
+ // (measured on Claude Code 2.1.280). If that turn never comes, the owner's queue moves on.
23
+ export const INJECTED_TURN_WAIT = 30.0;
24
+ // How long the answer to `!stop` waits for the reply it cut short to end. An interrupted turn
25
+ // ends within a second or two; a final write that is retried takes ten.
26
+ export const STOP_TAIL_WAIT = 15.0;
27
+ // Each kind of task as the list of what still runs counts it and as its end line names it, the way the terminal
28
+ // prints `Agent "..." finished`. The agent decides which kind a task is (`task_started.kind`): a
29
+ // kind it does not name arrives as `other`, which counts and reads as a task, so a new one shows
30
+ // with no change.
31
+ export const TASK_WORDS = {
32
+ command: ["shell", "Background command"],
33
+ subagent: ["agent", "Agent"],
34
+ other: ["task", "Task"],
35
+ };
36
+ // Tasks whose kind and description the session keeps. A task can end with a terminal
37
+ // `task_updated` and no notification (SDK docstring), so past this many the oldest are dropped.
38
+ export const TASKS_KEPT = 200;
39
+ // The kinds whose notification summary is already the terminal's end line (measured: a
40
+ // command's reads `Background command "..." completed (exit code 0)`; an agent's is its result).
41
+ export const SUMMARY_IS_END_LINE = new Set(["command"]);
42
+ // How much of the reason an agent failed its end line keeps: the terminal prints it whole, and
43
+ // the longest one captured (a refused request, with its gateway and model) ran to 330 characters.
44
+ export const FAILURE_LIMIT = 400;
45
+ // Tasks whose reply the session remembers. An ended task stays a while, since an agent can
46
+ // report again after its notification; past this many, the oldest ended ones are forgotten.
47
+ export const TASK_REPLIES_KEPT = 100;
48
+ // How often a stopping daemon checks whether every channel's turns have ended.
49
+ export const DRAIN_POLL_SECONDS = 0.5;
50
+ // A session's agent process closes after this long with nothing to do (idle, and no approval,
51
+ // question or background report pending); the next message rebuilds it and resumes.
52
+ export const IDLE_CLOSE_SECONDS = 3600.0;
53
+ // The one hold a thread can have open at once, in the session's waiting set alongside real
54
+ // approval ids (`waitingForOwner` and the idle-close timer treat every entry the same).
55
+ export const HOLD_MARKER = "d8-hold";
56
+ // How much of the owner's question a note about a message that was not sent quotes.
57
+ export const ASKED_LIMIT = 100;
58
+ // How much of each message a note about messages that were not sent quotes.
59
+ export const NOT_SENT_START = 60;
60
+ // What the reader says ended the stream when the back end gave no reason of its own.
61
+ export const PROCESS_EXITED = new Named("the {agent} process exited");
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ /** Whether a folder can hold a session, the folder a typed path names, and `!status`'s terminal line. */
2
+ import { realpathSync, statSync } from "node:fs";
3
+ import { opendir, stat } from "node:fs/promises";
4
+ import { isAbsolute, sep } from "node:path";
5
+ import { shellQuote } from "../../text.js";
6
+ import { errorCode } from "../../values.js";
7
+ import { ABSENT, expandUser, resolvePath } from "../paths.js";
8
+ import * as texts from "../texts.js";
9
+ import { DirectoryMissing, DirectoryUnreadable, DirectoryUntrusted } from "./errors.js";
10
+ /**
11
+ * `agent`'s record of trust as `checkDirectory` and the folder list read it. An agent that
12
+ * keeps none (`folderTrust`) has no gate to start a session behind: every folder counts.
13
+ */
14
+ export function trustedBy(agent) {
15
+ return async (folder) => !agent.capabilities.folderTrust || agent.folderTrusted(folder);
16
+ }
17
+ /**
18
+ * `agent`'s answer for the repository holding a folder when the daemon's own git may run in it:
19
+ * the one lookup behind the footer and `!open`. Null for an agent with no record of trust
20
+ * (`folderTrust`): the daemon's git runs in no folder then.
21
+ */
22
+ export function repositoryBy(agent) {
23
+ return async (folder) => agent.capabilities.folderTrust ? agent.trustedRepository(folder) : null;
24
+ }
25
+ /**
26
+ * Rejects with the `DirectoryUnavailable` that keeps a session from starting in `directory`,
27
+ * the first found in the order the owner can act on: missing, unreadable, untrusted.
28
+ */
29
+ export async function checkDirectory(directory, trusted, names) {
30
+ // One stat and one directory open on a local folder, as a session start always made them.
31
+ // A folder the daemon is not let into is unreadable at either: at the stat when a folder
32
+ // above it is closed, at the open when it is closed itself. Both say `DirectoryUnreadable`.
33
+ try {
34
+ if (!(await stat(directory)).isDirectory())
35
+ throw new DirectoryMissing(directory);
36
+ await (await opendir(directory)).close();
37
+ }
38
+ catch (error) {
39
+ const code = errorCode(error);
40
+ if (code === "EACCES" || code === "EPERM")
41
+ throw new DirectoryUnreadable(directory);
42
+ // A path with a null in it is one Node refuses to look at: it names no folder.
43
+ if (code !== undefined && (ABSENT.has(code) || code === "ERR_INVALID_ARG_VALUE")) {
44
+ throw new DirectoryMissing(directory);
45
+ }
46
+ throw error;
47
+ }
48
+ if (!(await trusted(directory)))
49
+ throw new DirectoryUntrusted(directory, names);
50
+ }
51
+ /** Whether `path` is `root` or lies under it. */
52
+ function under(path, root) {
53
+ return path === root || path.startsWith(root.endsWith(sep) ? root : root + sep);
54
+ }
55
+ /** The real directory `raw` names, if it exists under `root`; guards against a typo only. */
56
+ export function resolveDirectory(raw, root) {
57
+ // A relative path is read under the root: the daemon's own working directory means nothing
58
+ // to the owner (launchd starts it in `/`).
59
+ const written = expandUser(raw);
60
+ try {
61
+ // Joined as text: the disk follows a link before a `..`.
62
+ const path = resolvePath(isAbsolute(written) ? written : `${root}/${written}`);
63
+ if (!statSync(path).isDirectory() || !under(path, root))
64
+ return null;
65
+ // The kernel is asked too, since a session runs in the folder it reaches: whatever the
66
+ // reading above made of the spelling, that folder lies under the root or is refused.
67
+ return under(realpathSync.native(path), realpathSync.native(root)) ? path : null;
68
+ }
69
+ catch {
70
+ return null; // it names nothing, or goes through a loop of links
71
+ }
72
+ }
73
+ /**
74
+ * The `!status` line with the command that forks a session in the terminal (the agent's
75
+ * `words.terminal`, `template`), run from the thread's folder, where `!resume` in its channel
76
+ * then finds the fork. `escapeMarkup` is the chat's, for a reference (`ChatProvider.escape`).
77
+ */
78
+ export function terminalLine(template, directory, sessionId, escapeMarkup) {
79
+ const command = texts.fill(template, {
80
+ directory: shellQuote(directory),
81
+ session: sessionId,
82
+ });
83
+ // A code span cannot hold a backtick: such a command is plain text, escaped.
84
+ const shown = command.includes("`") ? escapeMarkup(command) : `\`${command}\``;
85
+ return texts.fill(texts.STATUS_TERMINAL, { command: shown });
86
+ }
@@ -0,0 +1,79 @@
1
+ /** Why a session cannot start, or stopped under whoever was using it. */
2
+ import * as texts from "../texts.js";
3
+ /** The channel's directory cannot be used; `message` tells the owner what to do. */
4
+ export class DirectoryUnavailable extends Error {
5
+ directory;
6
+ constructor(directory, message) {
7
+ super(message);
8
+ this.name = "DirectoryUnavailable";
9
+ this.directory = directory;
10
+ }
11
+ }
12
+ export class DirectoryMissing extends DirectoryUnavailable {
13
+ constructor(directory) {
14
+ super(directory, texts.fill(texts.DIRECTORY_MISSING, { directory }));
15
+ this.name = "DirectoryMissing";
16
+ }
17
+ }
18
+ /**
19
+ * The owner has not trusted the folder in the agent: a session would run its hooks and apply
20
+ * its settings with no trust dialog.
21
+ */
22
+ export class DirectoryUntrusted extends DirectoryUnavailable {
23
+ constructor(directory, names) {
24
+ super(directory, texts.fill(texts.named(texts.DIRECTORY_UNTRUSTED, names), { directory }));
25
+ this.name = "DirectoryUntrusted";
26
+ }
27
+ }
28
+ /**
29
+ * macOS privacy protection (TCC) denies the daemon a folder such as ~/Documents: a process
30
+ * started by launchd does not inherit the Terminal's permission, and the CLI fails to start.
31
+ */
32
+ export class DirectoryUnreadable extends DirectoryUnavailable {
33
+ constructor(directory) {
34
+ super(directory, texts.fill(texts.DIRECTORY_UNREADABLE, { directory }));
35
+ this.name = "DirectoryUnreadable";
36
+ }
37
+ }
38
+ /**
39
+ * The session closed (the daemon stopping, or the idle close, most likely) while something was
40
+ * using it: a daemon word (`!status`, `!bypass`), or `submit` queuing an owner's prompt (the
41
+ * caller retries once, against a freshly looked-up session).
42
+ */
43
+ export class SessionClosed extends Error {
44
+ constructor() {
45
+ super(texts.SESSION_CLOSED);
46
+ this.name = "SessionClosed";
47
+ }
48
+ }
49
+ /**
50
+ * A thread's stored session id could not be resumed: its entry was removed, and the thread
51
+ * needs a new top-level message to start again.
52
+ */
53
+ export class SessionGone extends Error {
54
+ constructor(names, options) {
55
+ super(texts.named(texts.SESSION_GONE, names), options);
56
+ this.name = "SessionGone";
57
+ }
58
+ }
59
+ /**
60
+ * `!bypass` was asked of an agent with no mode that skips permission requests: nothing was
61
+ * switched or stored.
62
+ */
63
+ export class BypassUnavailable extends Error {
64
+ constructor() {
65
+ super("the agent has no bypass mode");
66
+ this.name = "BypassUnavailable";
67
+ }
68
+ }
69
+ /**
70
+ * An install is replacing allaboard's own copy (issue #361): no agent process starts until it ends,
71
+ * since a new one would run the new copy's agent under the old code, or a half-written copy. A
72
+ * session already connected is not touched.
73
+ */
74
+ export class StartsBlocked extends Error {
75
+ constructor() {
76
+ super(texts.RESTARTING);
77
+ this.name = "StartsBlocked";
78
+ }
79
+ }