spur-board 0.4.0 → 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.
- package/dist/cli.mjs +25 -12
- package/dist/{client-sGJ4fzQ4.mjs → client-BnkZmz1i.mjs} +1 -1
- package/dist/client.mjs +1 -1
- package/dist/{config-DPXFxZQy.mjs → config-UyXnaekw.mjs} +26 -7
- package/dist/daemon.mjs +16 -6
- package/dist/mcp.mjs +3 -3
- package/dist/{src-Cf78bzK5.mjs → src-VWyX7Nwm.mjs} +8 -3
- package/package.json +1 -1
- package/skills/setup/references/slack.md +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as renderCliError, i as errorExitCode, n as requestTokenEnrollment, o as escapeTerminalControls, r as CliError, t as requestOperation } from "./client-
|
|
3
|
-
import { n as loadEnrollmentConfig, t as
|
|
1
|
+
import { R as configSchema, s as OPERATIONS, z as SLACK_BOT_SCOPES } from "./src-VWyX7Nwm.mjs";
|
|
2
|
+
import { a as renderCliError, i as errorExitCode, n as requestTokenEnrollment, o as escapeTerminalControls, r as CliError, t as requestOperation } from "./client-BnkZmz1i.mjs";
|
|
3
|
+
import { i as resolveDaemonAddress, n as loadClientConfig, r as loadEnrollmentConfig, t as configFile } from "./config-UyXnaekw.mjs";
|
|
4
4
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import { chmodSync, closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { homedir, hostname, userInfo } from "node:os";
|
|
@@ -13,6 +13,23 @@ import { parseEnv } from "node:util";
|
|
|
13
13
|
import { access, cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
//#region src/manifest.ts
|
|
16
|
+
function loadSlackManifestDefaults(env, home) {
|
|
17
|
+
let contents;
|
|
18
|
+
try {
|
|
19
|
+
contents = readFileSync(configFile(env, home), "utf8");
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT") return {};
|
|
22
|
+
throw new CliError("config", void 0, void 0, "Read config.yaml before generating a manifest.");
|
|
23
|
+
}
|
|
24
|
+
const document = YAML.parseDocument(contents);
|
|
25
|
+
if (document.errors.length > 0) throw new CliError("config", void 0, void 0, "Fix the YAML in config.yaml.");
|
|
26
|
+
const parsed = configSchema.safeParse(document.toJS());
|
|
27
|
+
if (!parsed.success) throw new CliError("config", void 0, void 0, `Fix ${parsed.error.issues[0]?.path.map(String).join(".") || "config.yaml"} in config.yaml.`);
|
|
28
|
+
return {
|
|
29
|
+
appName: parsed.data.slack.appName,
|
|
30
|
+
command: parsed.data.slack.command
|
|
31
|
+
};
|
|
32
|
+
}
|
|
16
33
|
function renderSlackManifest(options = {}) {
|
|
17
34
|
const appName = (options.appName ?? "spur-board").trim();
|
|
18
35
|
const command = options.command ?? "/board";
|
|
@@ -140,12 +157,6 @@ function warning(id, fix) {
|
|
|
140
157
|
fix
|
|
141
158
|
};
|
|
142
159
|
}
|
|
143
|
-
function configFile(env, home) {
|
|
144
|
-
const boardHome = env["BOARD_HOME"];
|
|
145
|
-
if (boardHome) return join(resolve(boardHome), "config.yaml");
|
|
146
|
-
const xdg = env["XDG_CONFIG_HOME"];
|
|
147
|
-
return join(xdg && isAbsolute(xdg) ? xdg : join(home, ".config"), "spur-board", "config.yaml");
|
|
148
|
-
}
|
|
149
160
|
function unitFile(env, home) {
|
|
150
161
|
const xdg = env["XDG_CONFIG_HOME"];
|
|
151
162
|
return join(xdg && isAbsolute(xdg) ? xdg : join(home, ".config"), "systemd", "user", "spur-board.service");
|
|
@@ -334,8 +345,8 @@ async function runDoctor(options = {}) {
|
|
|
334
345
|
checks.push(stale.length > 0 || unknownStarts ? warning("sessions.stale", `Restart sessions started before the last skill install${staleIds.length ? `: ${staleIds.join(", ")}` : ""}; verify sessions whose start time is unavailable.`) : result("sessions.stale", true, "Restart older sessions."));
|
|
335
346
|
let remote = null;
|
|
336
347
|
try {
|
|
337
|
-
const
|
|
338
|
-
const response = await ports.fetcher(new URL("/v1/doctor",
|
|
348
|
+
const address = resolveDaemonAddress(env, home, config.http.port);
|
|
349
|
+
const response = await ports.fetcher(new URL("/v1/doctor", address), { signal: AbortSignal.timeout(8e3) });
|
|
339
350
|
if (response.ok) remote = remoteChecks(await response.json());
|
|
340
351
|
} catch {}
|
|
341
352
|
checks.push(result("daemon.available", remote !== null, "Start or restart the user service."));
|
|
@@ -994,7 +1005,9 @@ function registerSetupCommands(program, options, io, env) {
|
|
|
994
1005
|
const appName = manifest.opts()["appName"];
|
|
995
1006
|
const command = manifest.opts()["command"];
|
|
996
1007
|
if (appName !== void 0 && typeof appName !== "string" || command !== void 0 && typeof command !== "string") throw new CliError("usage");
|
|
1008
|
+
const defaults = appName === void 0 || command === void 0 ? loadSlackManifestDefaults(env, options.home ?? homedir()) : {};
|
|
997
1009
|
io.stdout(renderSlackManifest({
|
|
1010
|
+
...defaults,
|
|
998
1011
|
...appName === void 0 ? {} : { appName },
|
|
999
1012
|
...command === void 0 ? {} : { command }
|
|
1000
1013
|
}));
|
|
@@ -1032,7 +1045,7 @@ function createProgram(options = {}) {
|
|
|
1032
1045
|
const key = op.kind === "mutation" ? keyOption || randomUUID() : void 0;
|
|
1033
1046
|
if (key) io.stderr(`key: ${escapeTerminalControls(key)}\n`);
|
|
1034
1047
|
try {
|
|
1035
|
-
const result = await requestOperation(op, input, key, config);
|
|
1048
|
+
const result = await (options.sendOperation ?? requestOperation)(op, input, key, config);
|
|
1036
1049
|
io.stdout(`${json || !io.isTTY ? JSON.stringify(result) : formatHuman(result)}\n`);
|
|
1037
1050
|
} catch (error) {
|
|
1038
1051
|
if (error instanceof CliError) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { M as errorContextSchema, N as errorEnvelopeSchema, P as successEnvelopeSchema, j as ERRORS, l as renderJsonError } from "./src-
|
|
1
|
+
import { M as errorContextSchema, N as errorEnvelopeSchema, P as successEnvelopeSchema, j as ERRORS, l as renderJsonError } from "./src-VWyX7Nwm.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { request } from "node:http";
|
package/dist/client.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as requestTokenEnrollment, t as requestOperation } from "./client-
|
|
1
|
+
import { n as requestTokenEnrollment, t as requestOperation } from "./client-BnkZmz1i.mjs";
|
|
2
2
|
export { requestOperation, requestTokenEnrollment };
|
|
@@ -1,13 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { R as configSchema } from "./src-VWyX7Nwm.mjs";
|
|
2
|
+
import { r as CliError } from "./client-BnkZmz1i.mjs";
|
|
2
3
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import YAML from "yaml";
|
|
5
7
|
//#region src/config.ts
|
|
6
8
|
const CONNECT_TIMEOUT_MS = 2e3;
|
|
7
9
|
const RESPONSE_TIMEOUT_MS = 3e4;
|
|
8
|
-
function
|
|
10
|
+
function configFile(env, home) {
|
|
11
|
+
const boardHome = env["BOARD_HOME"];
|
|
12
|
+
if (boardHome) return join(resolve(boardHome), "config.yaml");
|
|
13
|
+
const xdg = env["XDG_CONFIG_HOME"];
|
|
14
|
+
return join(xdg && isAbsolute(xdg) ? xdg : join(home, ".config"), "spur-board", "config.yaml");
|
|
15
|
+
}
|
|
16
|
+
function resolveDaemonAddress(env = process.env, home = homedir(), configuredPort) {
|
|
9
17
|
const rawAddress = env["BOARD_DAEMON_URL"];
|
|
10
|
-
if (
|
|
18
|
+
if (rawAddress === void 0) {
|
|
19
|
+
let port = configuredPort ?? 4311;
|
|
20
|
+
if (configuredPort === void 0) try {
|
|
21
|
+
const parsed = YAML.parseDocument(readFileSync(configFile(env, home), "utf8"));
|
|
22
|
+
if (parsed.errors.length === 0) {
|
|
23
|
+
const config = configSchema.safeParse(parsed.toJS());
|
|
24
|
+
if (config.success) port = config.data.http.port;
|
|
25
|
+
}
|
|
26
|
+
} catch {}
|
|
27
|
+
if (port === 0) throw new CliError("config", void 0, void 0, "http.port: 0 assigns a runtime port that clients cannot discover. Set BOARD_DAEMON_URL to the daemon listening address.");
|
|
28
|
+
return new URL(`http://127.0.0.1:${String(port)}`);
|
|
29
|
+
}
|
|
11
30
|
let address;
|
|
12
31
|
try {
|
|
13
32
|
address = new URL(rawAddress);
|
|
@@ -66,7 +85,7 @@ function loadClientConfig(env = process.env, paths = {
|
|
|
66
85
|
cwd: process.cwd(),
|
|
67
86
|
home: homedir()
|
|
68
87
|
}) {
|
|
69
|
-
const address =
|
|
88
|
+
const address = resolveDaemonAddress(env, paths.home);
|
|
70
89
|
const session = env["SPUR_SESSION"] || void 0;
|
|
71
90
|
const token = session ? void 0 : env["BOARD_TOKEN"] === void 0 ? findTokenFile(paths.cwd, paths.home) : env["BOARD_TOKEN"];
|
|
72
91
|
if (!session && !token) throw new CliError("config", { environmentVariable: "BOARD_TOKEN" });
|
|
@@ -81,7 +100,7 @@ function loadClientConfig(env = process.env, paths = {
|
|
|
81
100
|
/** Enrollment sends only a hash and works before any operator credential exists. */
|
|
82
101
|
function loadEnrollmentConfig(env = process.env) {
|
|
83
102
|
return {
|
|
84
|
-
address:
|
|
103
|
+
address: resolveDaemonAddress(env),
|
|
85
104
|
session: void 0,
|
|
86
105
|
token: void 0,
|
|
87
106
|
connectTimeoutMs: CONNECT_TIMEOUT_MS,
|
|
@@ -89,4 +108,4 @@ function loadEnrollmentConfig(env = process.env) {
|
|
|
89
108
|
};
|
|
90
109
|
}
|
|
91
110
|
//#endregion
|
|
92
|
-
export {
|
|
111
|
+
export { resolveDaemonAddress as i, loadClientConfig as n, loadEnrollmentConfig as r, configFile as t };
|
package/dist/daemon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as taskKeySchema, C as sessionFactsSchema, D as errorCodeOf, E as encodeActorId,
|
|
1
|
+
import { A as taskKeySchema, C as sessionFactsSchema, D as errorCodeOf, E as encodeActorId, L as DEFAULT_SLACK_COMMAND, O as parseActorId, P as successEnvelopeSchema, R as configSchema, S as prRefSchema, T as actorIdSchema, _ as EVENT_KINDS, a as buildCatalogSnapshot, b as SESSION_STATUSES, c as renderHttpProblem, d as SPACE_IDS, f as conversationRefSchema, g as userIdSchema, h as messageRefSchema, i as sourceCursorSchema, j as ERRORS, k as parseRef$1, m as interactionRefSchema, n as batchSchema, o as roleCanUseOperation, p as inboundEventSchema, r as entityRefSchema, s as OPERATIONS, t as AdapterError, v as eventPayloadSchemas, w as ticketFactsSchema, x as prFactsSchema, y as SESSION_HEALTH_STATES, z as SLACK_BOT_SCOPES } from "./src-VWyX7Nwm.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { closeSync, existsSync, fstatSync, ftruncateSync, mkdirSync, openSync, readFileSync, readdirSync, writeSync } from "node:fs";
|
|
@@ -11546,6 +11546,13 @@ function createInbound(input) {
|
|
|
11546
11546
|
await clearItem(trx, item.id, new Date(at));
|
|
11547
11547
|
}
|
|
11548
11548
|
async function runUser(trx, event, at) {
|
|
11549
|
+
if (event.kind === "command" && event.name !== void 0 && event.name !== config.slack.command) return deny(trx, {
|
|
11550
|
+
event,
|
|
11551
|
+
reason: "Command name does not match the configured command.",
|
|
11552
|
+
actor: "system",
|
|
11553
|
+
gateway: gateway.kind,
|
|
11554
|
+
at
|
|
11555
|
+
});
|
|
11549
11556
|
if (event.kind !== "message_deleted" && event.author !== config.operator.slack) return deny(trx, {
|
|
11550
11557
|
event,
|
|
11551
11558
|
reason: "Only the configured operator may act.",
|
|
@@ -11731,7 +11738,7 @@ function createInbound(input) {
|
|
|
11731
11738
|
} catch {}
|
|
11732
11739
|
}
|
|
11733
11740
|
async function applyUser(event) {
|
|
11734
|
-
if ((event.kind === "command" || event.kind === "action") && event.author === config.operator.slack && (event.kind === "command" || event.sourceSpace !== null)) {
|
|
11741
|
+
if ((event.kind === "command" || event.kind === "action") && event.author === config.operator.slack && (event.kind !== "command" || event.name === void 0 || event.name === config.slack.command) && (event.kind === "command" || event.sourceSpace !== null)) {
|
|
11735
11742
|
await dispatchInteraction(event);
|
|
11736
11743
|
return;
|
|
11737
11744
|
}
|
|
@@ -11811,6 +11818,7 @@ function createInbound(input) {
|
|
|
11811
11818
|
const item = await store.db.selectFrom("operator_item").select(["cleared_at", "inbound_event_json"]).where("id", "=", itemId).executeTakeFirst();
|
|
11812
11819
|
if (item === void 0 || item.cleared_at !== null || item.inbound_event_json === null) throw new CommandError("retry_not_applicable", "This item has no stored event to retry.");
|
|
11813
11820
|
let event = inboundEventSchema.parse(JSON.parse(item.inbound_event_json));
|
|
11821
|
+
if (event.kind === "command" && event.name !== void 0 && event.name !== config.slack.command) throw new CommandError("retry_not_applicable", "This interaction cannot be retried.");
|
|
11814
11822
|
if (event.kind === "command" && event.command === void 0) {
|
|
11815
11823
|
const command = legacyCommand(event.text);
|
|
11816
11824
|
if (command === null) throw new CommandError("retry_not_applicable", "This interaction cannot be retried.");
|
|
@@ -14896,7 +14904,7 @@ function createDeliveryGateway(client, channels) {
|
|
|
14896
14904
|
function descriptors(commandName) {
|
|
14897
14905
|
return OPERATIONS.filter((operation) => operation.kind === "query" && operation.slack?.kind === "slash-command").map((operation) => ({
|
|
14898
14906
|
operation,
|
|
14899
|
-
usage: operation.slack?.kind === "slash-command" ? operation.slack.usage.replace(
|
|
14907
|
+
usage: operation.slack?.kind === "slash-command" ? operation.slack.usage.replace(/^\/\S+/, commandName) : ""
|
|
14900
14908
|
}));
|
|
14901
14909
|
}
|
|
14902
14910
|
function commandUsage(commandName) {
|
|
@@ -15071,7 +15079,7 @@ function normalizeMessage(channel, payload, context) {
|
|
|
15071
15079
|
return events;
|
|
15072
15080
|
}
|
|
15073
15081
|
/** Actions and commands share the same stable trigger identity as their Slack retry. */
|
|
15074
|
-
function normalizeInteraction(type, payload, channels, root, commandName =
|
|
15082
|
+
function normalizeInteraction(type, payload, channels, root, commandName = DEFAULT_SLACK_COMMAND) {
|
|
15075
15083
|
const body = record$1(payload);
|
|
15076
15084
|
if (body === null) return [];
|
|
15077
15085
|
const triggerId = string$1(body.trigger_id);
|
|
@@ -15080,16 +15088,18 @@ function normalizeInteraction(type, payload, channels, root, commandName = "/boa
|
|
|
15080
15088
|
if (user === void 0) return [];
|
|
15081
15089
|
const author = userIdSchema.parse(user);
|
|
15082
15090
|
if (type === "slash_commands") {
|
|
15083
|
-
|
|
15091
|
+
const name = string$1(body.command);
|
|
15092
|
+
if (name === void 0) return [];
|
|
15084
15093
|
const channel = string$1(body.channel_id);
|
|
15085
15094
|
if (channel === void 0) return [];
|
|
15086
15095
|
return [{
|
|
15087
15096
|
kind: "command",
|
|
15097
|
+
name,
|
|
15088
15098
|
dedupeKey: `slack:${triggerId}`,
|
|
15089
15099
|
interaction: interactionRef(channel, user, triggerId),
|
|
15090
15100
|
author,
|
|
15091
15101
|
text: text(body.text) ?? "",
|
|
15092
|
-
command: parseCommand(text(body.text) ?? "", commandName),
|
|
15102
|
+
...name === commandName ? { command: parseCommand(text(body.text) ?? "", commandName) } : {},
|
|
15093
15103
|
sourceUrl: sourceUrl(channel),
|
|
15094
15104
|
sourceSpace: sourceSpace(channel, channels)
|
|
15095
15105
|
}];
|
package/dist/mcp.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { j as ERRORS, s as OPERATIONS, u as renderMcpError } from "./src-
|
|
2
|
-
import { r as CliError, t as requestOperation } from "./client-
|
|
3
|
-
import {
|
|
1
|
+
import { j as ERRORS, s as OPERATIONS, u as renderMcpError } from "./src-VWyX7Nwm.mjs";
|
|
2
|
+
import { r as CliError, t as requestOperation } from "./client-BnkZmz1i.mjs";
|
|
3
|
+
import { n as loadClientConfig } from "./config-UyXnaekw.mjs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -110,6 +110,9 @@ const SLACK_BOT_SCOPES = [
|
|
|
110
110
|
];
|
|
111
111
|
//#endregion
|
|
112
112
|
//#region ../contracts/src/config.ts
|
|
113
|
+
const DEFAULT_HTTP_PORT = 4311;
|
|
114
|
+
const DEFAULT_SLACK_APP_NAME = "spur-board";
|
|
115
|
+
const DEFAULT_SLACK_COMMAND = "/board";
|
|
113
116
|
const WEEKDAYS = [
|
|
114
117
|
"monday",
|
|
115
118
|
"tuesday",
|
|
@@ -246,7 +249,8 @@ const configSchema = z.strictObject({
|
|
|
246
249
|
}),
|
|
247
250
|
operator: z.strictObject({ slack: nonEmpty }),
|
|
248
251
|
slack: z.strictObject({
|
|
249
|
-
|
|
252
|
+
appName: z.string().trim().min(1).default(DEFAULT_SLACK_APP_NAME),
|
|
253
|
+
command: z.string().regex(/^\/[a-z0-9_-]+$/, "Expected a slash command such as /board").default(DEFAULT_SLACK_COMMAND),
|
|
250
254
|
appId: z.string().regex(/^A[A-Z0-9]+$/, "Expected a Slack app ID").optional(),
|
|
251
255
|
channels: z.strictObject({
|
|
252
256
|
orchestrators: nonEmpty.optional(),
|
|
@@ -276,7 +280,7 @@ const configSchema = z.strictObject({
|
|
|
276
280
|
message: "Final unanswered pause must exceed the second retry pause"
|
|
277
281
|
});
|
|
278
282
|
}).prefault({}),
|
|
279
|
-
http: z.strictObject({ port: z.int().min(0).max(65535).default(
|
|
283
|
+
http: z.strictObject({ port: z.int().min(0).max(65535).default(DEFAULT_HTTP_PORT).describe("Local HTTP API port (0 assigns a free port).") }).prefault({}),
|
|
280
284
|
adapters: z.strictObject({
|
|
281
285
|
spur: spurAdapterSection,
|
|
282
286
|
github: adapterSection(6e4),
|
|
@@ -1113,6 +1117,7 @@ const actionEventSchema = z.strictObject({
|
|
|
1113
1117
|
/** `text` is the command's argument text. */
|
|
1114
1118
|
const commandEventSchema = z.strictObject({
|
|
1115
1119
|
kind: z.literal("command"),
|
|
1120
|
+
name: z.string().min(1).optional(),
|
|
1116
1121
|
dedupeKey,
|
|
1117
1122
|
interaction: interactionRefSchema,
|
|
1118
1123
|
author: userIdSchema,
|
|
@@ -2630,4 +2635,4 @@ var AdapterError = class extends Error {
|
|
|
2630
2635
|
}
|
|
2631
2636
|
};
|
|
2632
2637
|
//#endregion
|
|
2633
|
-
export { taskKeySchema as A, sessionFactsSchema as C, errorCodeOf as D, encodeActorId as E,
|
|
2638
|
+
export { taskKeySchema as A, sessionFactsSchema as C, errorCodeOf as D, encodeActorId as E, DEFAULT_HTTP_PORT as F, DEFAULT_SLACK_APP_NAME as I, DEFAULT_SLACK_COMMAND as L, errorContextSchema as M, errorEnvelopeSchema as N, parseActorId as O, successEnvelopeSchema as P, configSchema as R, prRefSchema as S, actorIdSchema as T, EVENT_KINDS as _, buildCatalogSnapshot as a, SESSION_STATUSES as b, renderHttpProblem as c, SPACE_IDS as d, conversationRefSchema as f, userIdSchema as g, messageRefSchema as h, sourceCursorSchema as i, ERRORS as j, parseRef as k, renderJsonError as l, interactionRefSchema as m, batchSchema as n, roleCanUseOperation as o, inboundEventSchema as p, entityRefSchema as r, OPERATIONS as s, AdapterError as t, renderMcpError as u, eventPayloadSchemas as v, ticketFactsSchema as w, prFactsSchema as x, SESSION_HEALTH_STATES as y, SLACK_BOT_SCOPES as z };
|
package/package.json
CHANGED
|
@@ -4,6 +4,6 @@ Use this when the orchestrators or feed channel is not configured, or the app-le
|
|
|
4
4
|
|
|
5
5
|
1. Check the bot's current channel membership. List the private channels it can see and ask the operator, one choice at a time, which should serve orchestrators and feed. An empty list does not prove no channels exist: the bot sees only private channels to which it was invited. Store channel IDs in config, never guessed names.
|
|
6
6
|
2. If no channel fits, ask for an operator-chosen prefix and suggest available names using it. The operator creates or invites the app to channels; ask before any app or channel change.
|
|
7
|
-
3. Check whether an app and slash command already exist. If either is missing, use `board slack manifest --app-name <name> --command <slash-command>` to prepare a manifest with operator-chosen names. The operator creates the Slack app; do not claim the manifest creates it.
|
|
8
|
-
4. The manifest cannot create the app-level token with `connections:write`.
|
|
7
|
+
3. Check whether an app and slash command already exist. If either is missing, use `board slack manifest --app-name <name> --command <slash-command>` to prepare a manifest with operator-chosen names. The operator creates the Slack app; do not claim the manifest creates it. Set `slack.appName` and `slack.command` in config to the selected names.
|
|
8
|
+
4. The manifest cannot create the app-level token with `connections:write`. In the Slack app settings, open **Basic Information → App-Level Tokens** and create a token with that scope. Then use the private entry procedure in [secret files](secrets.md). Never ask for a token value in chat.
|
|
9
9
|
5. Check channel membership and command configuration with online doctor. If a check fails, apply its reported fix after inspecting state.
|