spur-board 0.4.1 → 0.6.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 +23 -9
- package/dist/{client-CHohYNxa.mjs → client-Dw9ZovyI.mjs} +1 -1
- package/dist/client.mjs +1 -1
- package/dist/{config-BuxxS_er.mjs → config-CeDZQlvy.mjs} +2 -2
- package/dist/daemon.mjs +94 -45
- package/dist/mcp.mjs +3 -3
- package/dist/{src-BEo3mvRC.mjs → src-Cy74ZQfR.mjs} +49 -2
- 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 { i as resolveDaemonAddress, n as loadClientConfig, r as loadEnrollmentConfig, t as configFile } from "./config-
|
|
1
|
+
import { B as SLACK_BOT_SCOPES, R as configSchema, s as OPERATIONS, z as requiredSecretKeys } from "./src-Cy74ZQfR.mjs";
|
|
2
|
+
import { a as renderCliError, i as errorExitCode, n as requestTokenEnrollment, o as escapeTerminalControls, r as CliError, t as requestOperation } from "./client-Dw9ZovyI.mjs";
|
|
3
|
+
import { i as resolveDaemonAddress, n as loadClientConfig, r as loadEnrollmentConfig, t as configFile } from "./config-CeDZQlvy.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";
|
|
@@ -173,12 +190,7 @@ function localSecrets(config, path, ports) {
|
|
|
173
190
|
} catch {
|
|
174
191
|
names = {};
|
|
175
192
|
}
|
|
176
|
-
const
|
|
177
|
-
const required = [
|
|
178
|
-
"GITHUB_TOKEN",
|
|
179
|
-
...config.projects.some((project) => project.tracker.type === "jira") ? ["JIRA_EMAIL", "JIRA_API_TOKEN"] : [],
|
|
180
|
-
...slackConfigured ? ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"] : []
|
|
181
|
-
];
|
|
193
|
+
const required = requiredSecretKeys(config);
|
|
182
194
|
const missing = required.filter((name) => !names[name]);
|
|
183
195
|
return {
|
|
184
196
|
...result("secrets", missing.length === 0, `Add ${missing.join(", ")} to the private secrets.env file.`),
|
|
@@ -988,7 +1000,9 @@ function registerSetupCommands(program, options, io, env) {
|
|
|
988
1000
|
const appName = manifest.opts()["appName"];
|
|
989
1001
|
const command = manifest.opts()["command"];
|
|
990
1002
|
if (appName !== void 0 && typeof appName !== "string" || command !== void 0 && typeof command !== "string") throw new CliError("usage");
|
|
1003
|
+
const defaults = appName === void 0 || command === void 0 ? loadSlackManifestDefaults(env, options.home ?? homedir()) : {};
|
|
991
1004
|
io.stdout(renderSlackManifest({
|
|
1005
|
+
...defaults,
|
|
992
1006
|
...appName === void 0 ? {} : { appName },
|
|
993
1007
|
...command === void 0 ? {} : { command }
|
|
994
1008
|
}));
|
|
@@ -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-Cy74ZQfR.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-Dw9ZovyI.mjs";
|
|
2
2
|
export { requestOperation, requestTokenEnrollment };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as CliError } from "./client-
|
|
1
|
+
import { R as configSchema } from "./src-Cy74ZQfR.mjs";
|
|
2
|
+
import { r as CliError } from "./client-Dw9ZovyI.mjs";
|
|
3
3
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
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, B as SLACK_BOT_SCOPES, 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 requiredSecretKeys } from "./src-Cy74ZQfR.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.");
|
|
@@ -12728,6 +12736,11 @@ function retryAt(headers, now) {
|
|
|
12728
12736
|
}
|
|
12729
12737
|
//#endregion
|
|
12730
12738
|
//#region ../adapters/src/github/client.ts
|
|
12739
|
+
function teamNameScopeDenied(error, query) {
|
|
12740
|
+
if (error.type !== "INSUFFICIENT_SCOPES" || !query.includes("... on Team { name }")) return false;
|
|
12741
|
+
if (error.path !== void 0) return error.path.includes("reviewRequests") && error.path.includes("requestedReviewer") && error.path.at(-1) === "name";
|
|
12742
|
+
return /\bname\b/i.test(error.message) && /\bread:(?:org|discussion)\b/i.test(error.message);
|
|
12743
|
+
}
|
|
12731
12744
|
function record$2(value) {
|
|
12732
12745
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new AdapterError("transient", "Malformed GitHub response");
|
|
12733
12746
|
return value;
|
|
@@ -12816,6 +12829,8 @@ var GithubClient = class {
|
|
|
12816
12829
|
status: 429,
|
|
12817
12830
|
headers: response.headers
|
|
12818
12831
|
}, Date.now());
|
|
12832
|
+
if (errors.some((error) => teamNameScopeDenied(error, query))) throw new AdapterError("auth", "GitHub token needs read:org for team review requests");
|
|
12833
|
+
if (errors.some((error) => error.type === "INSUFFICIENT_SCOPES")) throw new AdapterError("auth", "GitHub token lacks a required GraphQL read scope");
|
|
12819
12834
|
const data = record$2(wire.data);
|
|
12820
12835
|
const rate = record$2(data.rateLimit);
|
|
12821
12836
|
const remaining = rate.remaining;
|
|
@@ -13048,6 +13063,12 @@ function githubSources(config, options) {
|
|
|
13048
13063
|
function scoped(errors, root) {
|
|
13049
13064
|
return errors.filter((error) => error.path?.[0] === root);
|
|
13050
13065
|
}
|
|
13066
|
+
function checkAccessDenied(errors) {
|
|
13067
|
+
return errors.some((error) => error.type === "FORBIDDEN" && error.path?.includes("statusCheckRollup") === true && error.path.includes("contexts"));
|
|
13068
|
+
}
|
|
13069
|
+
function requireCheckAccess(errors) {
|
|
13070
|
+
if (checkAccessDenied(errors)) throw new AdapterError("auth", "GitHub token lacks Checks read permission");
|
|
13071
|
+
}
|
|
13051
13072
|
function rejectUnscoped(errors, roots) {
|
|
13052
13073
|
if (errors.some((error) => error.path === void 0 || !roots.includes(String(error.path[0])))) throw new AdapterError("transient", "Unscoped GitHub GraphQL error");
|
|
13053
13074
|
}
|
|
@@ -13089,6 +13110,7 @@ async function fullPr(client, pr, owner, name, number, signal, budget) {
|
|
|
13089
13110
|
}, signal);
|
|
13090
13111
|
budget(response.budget);
|
|
13091
13112
|
rejectUnscoped(response.errors, ["repository"]);
|
|
13113
|
+
requireCheckAccess(response.errors);
|
|
13092
13114
|
if (response.errors.length > 0) return void 0;
|
|
13093
13115
|
const pagePr = record$2(nested(response.data, "repository", "pullRequest"));
|
|
13094
13116
|
const page = field === "contexts" ? contexts(pagePr) : record$2(pagePr[field]);
|
|
@@ -13146,6 +13168,7 @@ async function pollRepository({ id, repo, login, prefixes, client, ctx }) {
|
|
|
13146
13168
|
const found = discover(nodes, ctx.cursor, login);
|
|
13147
13169
|
const bootstrapCursor = bootstrap && discoveryOk ? latestUpdatedAt(connectionNodes(nested(response.data, "repository", "mergedBaseline")), found.cursor) ?? sourceCursorSchema.parse("1970-01-01T00:00:00.000Z") : void 0;
|
|
13148
13170
|
const process = async (number, wire, errors, ref) => {
|
|
13171
|
+
requireCheckAccess(errors);
|
|
13149
13172
|
const error = errors[0];
|
|
13150
13173
|
if (error !== void 0) {
|
|
13151
13174
|
const direct = error.path === void 0 || error.path.length <= 2;
|
|
@@ -13449,6 +13472,7 @@ function createGithubIssuesAdapter(options) {
|
|
|
13449
13472
|
//#endregion
|
|
13450
13473
|
//#region ../adapters/src/jira/jira.ts
|
|
13451
13474
|
const SEARCH_PAGE_LIMIT = 20;
|
|
13475
|
+
const MAX_GET_SEARCH_URL_LENGTH = 2e3;
|
|
13452
13476
|
const OUTSIDE_BATCH_SIZE = 50;
|
|
13453
13477
|
const FALLBACK_REQUEST_LIMIT = 100;
|
|
13454
13478
|
const SPRINT_SCHEMA = "com.pyxis.greenhopper.jira:gh-sprint";
|
|
@@ -13563,6 +13587,7 @@ function createJiraAdapter(options) {
|
|
|
13563
13587
|
if (signal.aborted) throw new AdapterError("transient", "Jira poll aborted");
|
|
13564
13588
|
throw new AdapterError("transient", "Jira request failed");
|
|
13565
13589
|
}
|
|
13590
|
+
if (response.status === 414 && options.searchMethod === "GET") throw new AdapterError("config", "Jira GET search URL is too long");
|
|
13566
13591
|
if (!response.ok) throw new JiraHttpError(response.status, classifyHttpFailure({
|
|
13567
13592
|
status: response.status,
|
|
13568
13593
|
headers: response.headers
|
|
@@ -13604,19 +13629,28 @@ function createJiraAdapter(options) {
|
|
|
13604
13629
|
const seen = /* @__PURE__ */ new Set();
|
|
13605
13630
|
for (let page = 0; page < SEARCH_PAGE_LIMIT; page += 1) {
|
|
13606
13631
|
beforePage?.();
|
|
13607
|
-
const
|
|
13632
|
+
const fields = [
|
|
13633
|
+
"summary",
|
|
13634
|
+
"status",
|
|
13635
|
+
"assignee",
|
|
13636
|
+
"labels",
|
|
13637
|
+
"updated",
|
|
13638
|
+
...sprintField === null ? [] : [sprintField]
|
|
13639
|
+
];
|
|
13640
|
+
const body = {
|
|
13608
13641
|
jql,
|
|
13609
13642
|
maxResults: 100,
|
|
13610
|
-
fields
|
|
13611
|
-
"summary",
|
|
13612
|
-
"status",
|
|
13613
|
-
"assignee",
|
|
13614
|
-
"labels",
|
|
13615
|
-
"updated",
|
|
13616
|
-
...sprintField === null ? [] : [sprintField]
|
|
13617
|
-
],
|
|
13643
|
+
fields,
|
|
13618
13644
|
...token === void 0 ? {} : { nextPageToken: token }
|
|
13619
|
-
}
|
|
13645
|
+
};
|
|
13646
|
+
const path = options.searchMethod === "GET" ? `/rest/api/3/search/jql?${new URLSearchParams({
|
|
13647
|
+
jql,
|
|
13648
|
+
maxResults: "100",
|
|
13649
|
+
fields: fields.join(","),
|
|
13650
|
+
...token === void 0 ? {} : { nextPageToken: token }
|
|
13651
|
+
}).toString()}` : "/rest/api/3/search/jql";
|
|
13652
|
+
if (options.searchMethod === "GET" && new URL(path, tracker.baseUrl).href.length > MAX_GET_SEARCH_URL_LENGTH) throw new AdapterError("config", `Jira GET search URL is too long (over ${String(MAX_GET_SEARCH_URL_LENGTH)} characters)`);
|
|
13653
|
+
const result = object$1(await request(tracker.baseUrl, path, signal, options.searchMethod === "GET" ? void 0 : body));
|
|
13620
13654
|
issues.push(...array(result["issues"]));
|
|
13621
13655
|
const next = result["nextPageToken"];
|
|
13622
13656
|
if (next == null || next === "") {
|
|
@@ -13870,9 +13904,15 @@ function applyEntityOutcomes(input) {
|
|
|
13870
13904
|
function startRunner(options) {
|
|
13871
13905
|
const startedAt = (options.now ?? Date.now)();
|
|
13872
13906
|
const sources = options.entries.map((entry) => runSource(entry, options, startedAt));
|
|
13873
|
-
return {
|
|
13874
|
-
|
|
13875
|
-
|
|
13907
|
+
return {
|
|
13908
|
+
firstAttempts: Promise.all(sources.map(async (source) => ({
|
|
13909
|
+
source: source.id,
|
|
13910
|
+
outcome: await source.firstAttempt
|
|
13911
|
+
}))),
|
|
13912
|
+
async stop() {
|
|
13913
|
+
await Promise.all(sources.map((source) => source.stop()));
|
|
13914
|
+
}
|
|
13915
|
+
};
|
|
13876
13916
|
}
|
|
13877
13917
|
function runSource(entry, options, runnerStartMs) {
|
|
13878
13918
|
const { adapter, reserveRemaining } = entry;
|
|
@@ -13946,7 +13986,7 @@ function runSource(entry, options, runnerStartMs) {
|
|
|
13946
13986
|
if (stopping) return;
|
|
13947
13987
|
pollTimer = setTimeout(() => {
|
|
13948
13988
|
pollTimer = void 0;
|
|
13949
|
-
current = poll();
|
|
13989
|
+
current = poll().then(() => void 0);
|
|
13950
13990
|
}, delayMs);
|
|
13951
13991
|
}
|
|
13952
13992
|
async function poll() {
|
|
@@ -14042,22 +14082,28 @@ function runSource(entry, options, runnerStartMs) {
|
|
|
14042
14082
|
now: now()
|
|
14043
14083
|
});
|
|
14044
14084
|
failures = delay.failures;
|
|
14045
|
-
armDeadline();
|
|
14085
|
+
if (!options.oneShot) armDeadline();
|
|
14046
14086
|
await report();
|
|
14047
|
-
schedule(delay.delayMs);
|
|
14087
|
+
if (!options.oneShot) schedule(delay.delayMs);
|
|
14088
|
+
return outcome;
|
|
14048
14089
|
}
|
|
14049
|
-
armDeadline();
|
|
14050
|
-
|
|
14051
|
-
|
|
14052
|
-
|
|
14053
|
-
|
|
14054
|
-
|
|
14055
|
-
|
|
14056
|
-
|
|
14057
|
-
|
|
14058
|
-
|
|
14059
|
-
|
|
14060
|
-
|
|
14090
|
+
if (!options.oneShot) armDeadline();
|
|
14091
|
+
const firstAttempt = poll();
|
|
14092
|
+
current = firstAttempt.then(() => void 0);
|
|
14093
|
+
return {
|
|
14094
|
+
id: source,
|
|
14095
|
+
firstAttempt,
|
|
14096
|
+
async stop() {
|
|
14097
|
+
stopping = true;
|
|
14098
|
+
clearTimeout(pollTimer);
|
|
14099
|
+
clearTimeout(deadlineTimer);
|
|
14100
|
+
clearTimeout(hardBoundTimer);
|
|
14101
|
+
pollTimer = deadlineTimer = hardBoundTimer = void 0;
|
|
14102
|
+
controller?.abort();
|
|
14103
|
+
await current;
|
|
14104
|
+
await reports;
|
|
14105
|
+
}
|
|
14106
|
+
};
|
|
14061
14107
|
}
|
|
14062
14108
|
/**
|
|
14063
14109
|
* Settles with `work`, or with undefined as soon as `signal` aborts, so `stop()` never waits
|
|
@@ -14317,7 +14363,8 @@ function startAdapters(config, core, options = {}) {
|
|
|
14317
14363
|
},
|
|
14318
14364
|
intervalMs: config.adapters.jira.intervalMs,
|
|
14319
14365
|
...options.fetch === void 0 ? {} : { fetch: options.fetch },
|
|
14320
|
-
warn: options.warn ?? (() => void 0)
|
|
14366
|
+
warn: options.warn ?? (() => void 0),
|
|
14367
|
+
...options.jiraSearchMethod === void 0 ? {} : { searchMethod: options.jiraSearchMethod }
|
|
14321
14368
|
}) : blindSource(id, config.adapters.jira.intervalMs, "Jira credential is missing") });
|
|
14322
14369
|
} else {
|
|
14323
14370
|
const id = `github-issues:${project.id}:${project.tracker.repo.toLowerCase()}`;
|
|
@@ -14354,12 +14401,16 @@ function startAdapters(config, core, options = {}) {
|
|
|
14354
14401
|
}) }],
|
|
14355
14402
|
core,
|
|
14356
14403
|
config,
|
|
14357
|
-
...options.now === void 0 ? {} : { now: options.now }
|
|
14404
|
+
...options.now === void 0 ? {} : { now: options.now },
|
|
14405
|
+
...options.oneShot === void 0 ? {} : { oneShot: options.oneShot }
|
|
14358
14406
|
});
|
|
14359
|
-
return {
|
|
14360
|
-
|
|
14361
|
-
|
|
14362
|
-
|
|
14407
|
+
return {
|
|
14408
|
+
firstAttempts: runner.firstAttempts,
|
|
14409
|
+
async stop() {
|
|
14410
|
+
await runner.stop();
|
|
14411
|
+
await Promise.allSettled([...notices]);
|
|
14412
|
+
}
|
|
14413
|
+
};
|
|
14363
14414
|
}
|
|
14364
14415
|
//#endregion
|
|
14365
14416
|
//#region ../gateway-slack/src/api.ts
|
|
@@ -14896,7 +14947,7 @@ function createDeliveryGateway(client, channels) {
|
|
|
14896
14947
|
function descriptors(commandName) {
|
|
14897
14948
|
return OPERATIONS.filter((operation) => operation.kind === "query" && operation.slack?.kind === "slash-command").map((operation) => ({
|
|
14898
14949
|
operation,
|
|
14899
|
-
usage: operation.slack?.kind === "slash-command" ? operation.slack.usage.replace(
|
|
14950
|
+
usage: operation.slack?.kind === "slash-command" ? operation.slack.usage.replace(/^\/\S+/, commandName) : ""
|
|
14900
14951
|
}));
|
|
14901
14952
|
}
|
|
14902
14953
|
function commandUsage(commandName) {
|
|
@@ -15071,7 +15122,7 @@ function normalizeMessage(channel, payload, context) {
|
|
|
15071
15122
|
return events;
|
|
15072
15123
|
}
|
|
15073
15124
|
/** Actions and commands share the same stable trigger identity as their Slack retry. */
|
|
15074
|
-
function normalizeInteraction(type, payload, channels, root, commandName =
|
|
15125
|
+
function normalizeInteraction(type, payload, channels, root, commandName = DEFAULT_SLACK_COMMAND) {
|
|
15075
15126
|
const body = record$1(payload);
|
|
15076
15127
|
if (body === null) return [];
|
|
15077
15128
|
const triggerId = string$1(body.trigger_id);
|
|
@@ -15080,16 +15131,18 @@ function normalizeInteraction(type, payload, channels, root, commandName = "/boa
|
|
|
15080
15131
|
if (user === void 0) return [];
|
|
15081
15132
|
const author = userIdSchema.parse(user);
|
|
15082
15133
|
if (type === "slash_commands") {
|
|
15083
|
-
|
|
15134
|
+
const name = string$1(body.command);
|
|
15135
|
+
if (name === void 0) return [];
|
|
15084
15136
|
const channel = string$1(body.channel_id);
|
|
15085
15137
|
if (channel === void 0) return [];
|
|
15086
15138
|
return [{
|
|
15087
15139
|
kind: "command",
|
|
15140
|
+
name,
|
|
15088
15141
|
dedupeKey: `slack:${triggerId}`,
|
|
15089
15142
|
interaction: interactionRef(channel, user, triggerId),
|
|
15090
15143
|
author,
|
|
15091
15144
|
text: text(body.text) ?? "",
|
|
15092
|
-
command: parseCommand(text(body.text) ?? "", commandName),
|
|
15145
|
+
...name === commandName ? { command: parseCommand(text(body.text) ?? "", commandName) } : {},
|
|
15093
15146
|
sourceUrl: sourceUrl(channel),
|
|
15094
15147
|
sourceSpace: sourceSpace(channel, channels)
|
|
15095
15148
|
}];
|
|
@@ -15508,11 +15561,7 @@ async function runDaemonDoctor(ctx, productVersion, fetcher = fetch) {
|
|
|
15508
15561
|
const checks = [];
|
|
15509
15562
|
const jiraTrackers = config.projects.flatMap((project) => project.tracker.type === "jira" ? [project.tracker] : []);
|
|
15510
15563
|
const slackConfigured = Boolean(config.slack.channels.feed || config.slack.channels.orchestrators);
|
|
15511
|
-
const missing = [
|
|
15512
|
-
"GITHUB_TOKEN",
|
|
15513
|
-
...jiraTrackers.length > 0 ? ["JIRA_EMAIL", "JIRA_API_TOKEN"] : [],
|
|
15514
|
-
...slackConfigured ? ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"] : []
|
|
15515
|
-
].filter((name) => !secrets[name]);
|
|
15564
|
+
const missing = requiredSecretKeys(config).filter((name) => !secrets[name]);
|
|
15516
15565
|
checks.push(check("secrets", missing.length === 0, `Add ${missing.join(", ")} to the private secrets.env file.`));
|
|
15517
15566
|
const jiraEmail = secrets["JIRA_EMAIL"];
|
|
15518
15567
|
const jiraToken = secrets["JIRA_API_TOKEN"];
|
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 { n as loadClientConfig } from "./config-
|
|
1
|
+
import { j as ERRORS, s as OPERATIONS, u as renderMcpError } from "./src-Cy74ZQfR.mjs";
|
|
2
|
+
import { r as CliError, t as requestOperation } from "./client-Dw9ZovyI.mjs";
|
|
3
|
+
import { n as loadClientConfig } from "./config-CeDZQlvy.mjs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -111,6 +111,8 @@ const SLACK_BOT_SCOPES = [
|
|
|
111
111
|
//#endregion
|
|
112
112
|
//#region ../contracts/src/config.ts
|
|
113
113
|
const DEFAULT_HTTP_PORT = 4311;
|
|
114
|
+
const DEFAULT_SLACK_APP_NAME = "spur-board";
|
|
115
|
+
const DEFAULT_SLACK_COMMAND = "/board";
|
|
114
116
|
const WEEKDAYS = [
|
|
115
117
|
"monday",
|
|
116
118
|
"tuesday",
|
|
@@ -247,7 +249,8 @@ const configSchema = z.strictObject({
|
|
|
247
249
|
}),
|
|
248
250
|
operator: z.strictObject({ slack: nonEmpty }),
|
|
249
251
|
slack: z.strictObject({
|
|
250
|
-
|
|
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),
|
|
251
254
|
appId: z.string().regex(/^A[A-Z0-9]+$/, "Expected a Slack app ID").optional(),
|
|
252
255
|
channels: z.strictObject({
|
|
253
256
|
orchestrators: nonEmpty.optional(),
|
|
@@ -300,6 +303,49 @@ const configSchema = z.strictObject({
|
|
|
300
303
|
}).prefault({})
|
|
301
304
|
}).prefault({})
|
|
302
305
|
});
|
|
306
|
+
/** Recognized names in the private secrets.env file; values never belong in config.yaml. */
|
|
307
|
+
const SECRET_ENV_KEYS = [
|
|
308
|
+
{
|
|
309
|
+
name: "GITHUB_TOKEN",
|
|
310
|
+
requiredWhen: "always",
|
|
311
|
+
purpose: "Read GitHub repositories and check runs."
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: "JIRA_EMAIL",
|
|
315
|
+
requiredWhen: "jira",
|
|
316
|
+
purpose: "Jira account email."
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: "JIRA_API_TOKEN",
|
|
320
|
+
requiredWhen: "jira",
|
|
321
|
+
purpose: "Jira API token."
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "SLACK_BOT_TOKEN",
|
|
325
|
+
requiredWhen: "slack",
|
|
326
|
+
purpose: "Slack bot Web API token."
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "SLACK_APP_TOKEN",
|
|
330
|
+
requiredWhen: "slack",
|
|
331
|
+
purpose: "Slack app-level Socket Mode token."
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "SLACK_APP_CONFIG_TOKEN",
|
|
335
|
+
requiredWhen: "optional",
|
|
336
|
+
purpose: "Verify the installed slash command through the Slack app manifest API."
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "SLACK_API_URL",
|
|
340
|
+
requiredWhen: "optional",
|
|
341
|
+
purpose: "Override the Slack API URL for local testing."
|
|
342
|
+
}
|
|
343
|
+
];
|
|
344
|
+
function requiredSecretKeys(config) {
|
|
345
|
+
const jira = config.projects.some((project) => project.tracker.type === "jira");
|
|
346
|
+
const slack = Boolean(config.slack.channels.feed || config.slack.channels.orchestrators);
|
|
347
|
+
return SECRET_ENV_KEYS.filter(({ requiredWhen }) => requiredWhen === "always" || requiredWhen === "jira" && jira || requiredWhen === "slack" && slack).map(({ name }) => name);
|
|
348
|
+
}
|
|
303
349
|
//#endregion
|
|
304
350
|
//#region ../contracts/src/errors.ts
|
|
305
351
|
/**
|
|
@@ -1114,6 +1160,7 @@ const actionEventSchema = z.strictObject({
|
|
|
1114
1160
|
/** `text` is the command's argument text. */
|
|
1115
1161
|
const commandEventSchema = z.strictObject({
|
|
1116
1162
|
kind: z.literal("command"),
|
|
1163
|
+
name: z.string().min(1).optional(),
|
|
1117
1164
|
dedupeKey,
|
|
1118
1165
|
interaction: interactionRefSchema,
|
|
1119
1166
|
author: userIdSchema,
|
|
@@ -2631,4 +2678,4 @@ var AdapterError = class extends Error {
|
|
|
2631
2678
|
}
|
|
2632
2679
|
};
|
|
2633
2680
|
//#endregion
|
|
2634
|
-
export { taskKeySchema as A, sessionFactsSchema as C, errorCodeOf as D, encodeActorId as E, DEFAULT_HTTP_PORT as F,
|
|
2681
|
+
export { taskKeySchema as A, SLACK_BOT_SCOPES as B, 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, requiredSecretKeys 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.
|