spur-board 0.4.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
- import { I as configSchema, L as SLACK_BOT_SCOPES, s as OPERATIONS } from "./src-BEo3mvRC.mjs";
2
- import { a as renderCliError, i as errorExitCode, n as requestTokenEnrollment, o as escapeTerminalControls, r as CliError, t as requestOperation } from "./client-CHohYNxa.mjs";
3
- import { i as resolveDaemonAddress, n as loadClientConfig, r as loadEnrollmentConfig, t as configFile } from "./config-BuxxS_er.mjs";
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";
@@ -988,7 +1005,9 @@ function registerSetupCommands(program, options, io, env) {
988
1005
  const appName = manifest.opts()["appName"];
989
1006
  const command = manifest.opts()["command"];
990
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()) : {};
991
1009
  io.stdout(renderSlackManifest({
1010
+ ...defaults,
992
1011
  ...appName === void 0 ? {} : { appName },
993
1012
  ...command === void 0 ? {} : { command }
994
1013
  }));
@@ -1,4 +1,4 @@
1
- import { M as errorContextSchema, N as errorEnvelopeSchema, P as successEnvelopeSchema, j as ERRORS, l as renderJsonError } from "./src-BEo3mvRC.mjs";
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-CHohYNxa.mjs";
1
+ import { n as requestTokenEnrollment, t as requestOperation } from "./client-BnkZmz1i.mjs";
2
2
  export { requestOperation, requestTokenEnrollment };
@@ -1,5 +1,5 @@
1
- import { I as configSchema } from "./src-BEo3mvRC.mjs";
2
- import { r as CliError } from "./client-CHohYNxa.mjs";
1
+ import { R as configSchema } from "./src-VWyX7Nwm.mjs";
2
+ import { r as CliError } from "./client-BnkZmz1i.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, I as configSchema, L as SLACK_BOT_SCOPES, O as parseActorId, P as successEnvelopeSchema, 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 } from "./src-BEo3mvRC.mjs";
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(/^\/board\b/, commandName) : ""
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 = "/board") {
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
- if (body.command !== commandName) return [];
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-BEo3mvRC.mjs";
2
- import { r as CliError, t as requestOperation } from "./client-CHohYNxa.mjs";
3
- import { n as loadClientConfig } from "./config-BuxxS_er.mjs";
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";
@@ -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
- command: z.string().regex(/^\/[a-z0-9_-]+$/, "Expected a slash command such as /board").default("/board"),
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(),
@@ -1114,6 +1117,7 @@ const actionEventSchema = z.strictObject({
1114
1117
  /** `text` is the command's argument text. */
1115
1118
  const commandEventSchema = z.strictObject({
1116
1119
  kind: z.literal("command"),
1120
+ name: z.string().min(1).optional(),
1117
1121
  dedupeKey,
1118
1122
  interaction: interactionRefSchema,
1119
1123
  author: userIdSchema,
@@ -2631,4 +2635,4 @@ var AdapterError = class extends Error {
2631
2635
  }
2632
2636
  };
2633
2637
  //#endregion
2634
- export { taskKeySchema as A, sessionFactsSchema as C, errorCodeOf as D, encodeActorId as E, DEFAULT_HTTP_PORT as F, configSchema as I, SLACK_BOT_SCOPES as L, errorContextSchema as M, errorEnvelopeSchema as N, parseActorId as O, successEnvelopeSchema as P, 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 };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spur-board",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "license": "MIT",
5
5
  "engines": {
6
6
  "node": "^24.12.0 || ^26.0.0"
@@ -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`. Guide the operator through creating that token in Slack, then use the private entry procedure in [secret files](secrets.md). Never ask for a token value in chat.
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.