spur-board 0.3.0 → 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.
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { F as configSchema, I as SLACK_BOT_SCOPES, s as OPERATIONS } from "./src-BDNEze3S.mjs";
2
- import { a as renderCliError, i as errorExitCode, n as requestTokenEnrollment, o as escapeTerminalControls, r as CliError, t as requestOperation } from "./client-C81xon5D.mjs";
3
- import { n as loadEnrollmentConfig, t as loadClientConfig } from "./config-D8AEf8vU.mjs";
1
+ import { F as configSchema, I as SLACK_BOT_SCOPES, s as OPERATIONS } from "./src-Cf78bzK5.mjs";
2
+ import { a as renderCliError, i as errorExitCode, n as requestTokenEnrollment, o as escapeTerminalControls, r as CliError, t as requestOperation } from "./client-sGJ4fzQ4.mjs";
3
+ import { n as loadEnrollmentConfig, t as loadClientConfig } from "./config-DPXFxZQy.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";
@@ -1,4 +1,4 @@
1
- import { M as errorContextSchema, N as errorEnvelopeSchema, P as successEnvelopeSchema, j as ERRORS, l as renderJsonError } from "./src-BDNEze3S.mjs";
1
+ import { M as errorContextSchema, N as errorEnvelopeSchema, P as successEnvelopeSchema, j as ERRORS, l as renderJsonError } from "./src-Cf78bzK5.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-C81xon5D.mjs";
1
+ import { n as requestTokenEnrollment, t as requestOperation } from "./client-sGJ4fzQ4.mjs";
2
2
  export { requestOperation, requestTokenEnrollment };
@@ -1,4 +1,4 @@
1
- import { r as CliError } from "./client-C81xon5D.mjs";
1
+ import { r as CliError } from "./client-sGJ4fzQ4.mjs";
2
2
  import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, realpathSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, isAbsolute, join, relative, 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, F as configSchema, I 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, 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-BDNEze3S.mjs";
1
+ import { A as taskKeySchema, C as sessionFactsSchema, D as errorCodeOf, E as encodeActorId, F as configSchema, I 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-Cf78bzK5.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";
@@ -7543,9 +7543,8 @@ var ScanNeeded = class extends Error {
7543
7543
  this.strings = strings;
7544
7544
  }
7545
7545
  };
7546
- /** Return a rule ID, never a finding message or a copy of the matched text. */
7547
- async function scanSecret(text) {
7548
- return (await lintSource({
7546
+ function lintGatewayText(text) {
7547
+ return lintSource({
7549
7548
  source: {
7550
7549
  content: text,
7551
7550
  filePath: "board.txt",
@@ -7559,7 +7558,59 @@ async function scanSecret(text) {
7559
7558
  rule: scannerPreset
7560
7559
  }] }
7561
7560
  }
7562
- })).messages[0]?.ruleId ?? null;
7561
+ });
7562
+ }
7563
+ /** Return a rule ID, never a finding message or a copy of the matched text. */
7564
+ async function scanSecret(text) {
7565
+ return (await lintGatewayText(text)).messages[0]?.ruleId ?? null;
7566
+ }
7567
+ /** Replace each detected span while preserving the rest of a private reply. */
7568
+ async function redactSecretMatches(text) {
7569
+ const matches = (await lintGatewayText(text)).messages.map(({ range, ruleId }) => ({
7570
+ start: range[0],
7571
+ end: range[1],
7572
+ ruleId
7573
+ })).sort((a, b) => a.start - b.start || b.end - a.end);
7574
+ const regions = [];
7575
+ for (const match of matches) {
7576
+ if (!Number.isInteger(match.start) || !Number.isInteger(match.end) || match.start < 0 || match.end <= match.start || match.end > text.length) throw new Error("Invalid gateway secret scan range");
7577
+ const previous = regions.at(-1);
7578
+ if (previous !== void 0 && match.start < previous.end) previous.end = Math.max(previous.end, match.end);
7579
+ else regions.push({ ...match });
7580
+ }
7581
+ let safe = "";
7582
+ let offset = 0;
7583
+ for (const { start, end, ruleId } of regions) {
7584
+ safe += `${text.slice(offset, start)}[redacted: ${ruleId}]`;
7585
+ offset = end;
7586
+ }
7587
+ return safe + text.slice(offset);
7588
+ }
7589
+ function visibleInline(value) {
7590
+ switch (value.kind) {
7591
+ case "text":
7592
+ case "emphasis":
7593
+ case "code": return value.text;
7594
+ case "link": return `${value.text} (${value.url})`;
7595
+ case "mention": return `<@${value.user}>`;
7596
+ }
7597
+ }
7598
+ /** Scan the exact text the Slack private-reply renderer will display. */
7599
+ async function redactPrivateReply(body) {
7600
+ return Promise.all(body.map(async (block) => {
7601
+ if (block.kind === "code_block") return {
7602
+ kind: "code_block",
7603
+ text: await redactSecretMatches(block.text)
7604
+ };
7605
+ const text = block.children.map(visibleInline).join("");
7606
+ return {
7607
+ kind: block.kind,
7608
+ children: [{
7609
+ kind: "text",
7610
+ text: await redactSecretMatches(text)
7611
+ }]
7612
+ };
7613
+ }));
7563
7614
  }
7564
7615
  /** Used by the common outbox insertion point before it issues SQL. */
7565
7616
  function checkGatewayStrings(strings) {
@@ -11192,6 +11243,69 @@ async function conversationsForRecovery(trx, gateway, now) {
11192
11243
  };
11193
11244
  }
11194
11245
  //#endregion
11246
+ //#region ../core/src/app/command-replies.ts
11247
+ function line(text) {
11248
+ return {
11249
+ kind: "paragraph",
11250
+ children: [{
11251
+ kind: "text",
11252
+ text
11253
+ }]
11254
+ };
11255
+ }
11256
+ function quote(text) {
11257
+ return {
11258
+ kind: "quote",
11259
+ children: [{
11260
+ kind: "text",
11261
+ text
11262
+ }]
11263
+ };
11264
+ }
11265
+ /** A compact Slack view of the same validated catalog output used by CLI and MCP. */
11266
+ function commandReply(id, value, input) {
11267
+ if (id === "status.get") {
11268
+ const operation = OPERATIONS.find((entry) => entry.id === "status.get");
11269
+ if (operation === void 0) throw new Error("status.get is missing from the catalog");
11270
+ const status = operation.output.parse(value);
11271
+ return {
11272
+ cliHint: `board ${operation.cli.command}`,
11273
+ body: [
11274
+ line(`Board status: ${String(status.tasks.length)} tasks, ${String(status.items.length)} items needing attention.`),
11275
+ ...status.tasks.map((task) => line(`${task.key}: ${task.column} (${task.membership})`)),
11276
+ ...status.items.map((item) => line(`${item.key}: ${item.summary}`)),
11277
+ line(`Unlinked: ${String(status.unlinkedPrs.length)} pull requests, ${String(status.unlinkedSessions.length)} sessions.`),
11278
+ ...status.sources.map((source) => line(`${source.source}: ${source.blindReason !== null ? `blind (${source.blindReason})` : source.warmingUp ? "warming up" : source.stale ? "stale" : "current"}`)),
11279
+ ...status.warmingUp ? [line("Sources are warming up.")] : [],
11280
+ ...status.spurUnavailable === null ? [] : [line(status.spurUnavailable)],
11281
+ ...status.hostLoad === null ? [] : [line(status.hostLoad)]
11282
+ ]
11283
+ };
11284
+ }
11285
+ if (id === "task.get") {
11286
+ const operation = OPERATIONS.find((entry) => entry.id === "task.get");
11287
+ if (operation === void 0) throw new Error("task.get is missing from the catalog");
11288
+ const result = operation.output.parse(value);
11289
+ const request = operation.input.parse(input);
11290
+ return {
11291
+ cliHint: `board ${operation.cli.command} ${request.task}`,
11292
+ body: [
11293
+ line(`Task ${result.task.key}: ${result.task.column} (${result.task.membership})`),
11294
+ ...result.blocks.flatMap((block) => [line(`Block ${block.id} (${block.state}, ${block.type}):`), quote(block.reason)]),
11295
+ ...result.comments.flatMap((comment) => [line(`Comment ${comment.id} by ${comment.author}:`), quote(comment.text)])
11296
+ ]
11297
+ };
11298
+ }
11299
+ const operation = OPERATIONS.find((entry) => entry.id === "task.history");
11300
+ if (operation === void 0) throw new Error("task.history is missing from the catalog");
11301
+ const history = operation.output.parse(value);
11302
+ const request = operation.input.parse(input);
11303
+ return {
11304
+ cliHint: `board ${operation.cli.command} ${request.task}`,
11305
+ body: [line(`Task ${history.task.key} history:`), ...history.events.map((event) => line(`${event.at}: ${event.summary} (${event.kind})`))]
11306
+ };
11307
+ }
11308
+ //#endregion
11195
11309
  //#region ../core/src/app/inbound.ts
11196
11310
  const resultSchema = z.strictObject({
11197
11311
  status: z.enum([
@@ -11415,9 +11529,6 @@ function createInbound(input) {
11415
11529
  });
11416
11530
  return { status: "applied" };
11417
11531
  }
11418
- function queryId(parts) {
11419
- return parts.length === 1 && parts[0] === "status" ? "status.get" : parts.length === 2 && parts[0] === "task" ? "task.get" : parts.length === 3 && parts[0] === "task" && parts[2] === "history" ? "task.history" : void 0;
11420
- }
11421
11532
  async function retryStoredInbound(trx, itemId, at) {
11422
11533
  const item = await trx.selectFrom("operator_item").selectAll().where("id", "=", itemId).executeTakeFirst();
11423
11534
  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.");
@@ -11468,16 +11579,46 @@ function createInbound(input) {
11468
11579
  const id = event.actionId.split(":", 1)[0];
11469
11580
  return OPERATIONS.some((entry) => entry.id === id && entry.slack?.kind === "button") ? id ?? "inbound.action" : "inbound.action";
11470
11581
  }
11471
- if (event.kind === "command") return queryId(event.text.trim().split(/\s+/)) ?? "inbound.command";
11582
+ if (event.kind === "command") return interactionOperation(event).id ?? "inbound.command";
11472
11583
  return `inbound.${event.kind}`;
11473
11584
  }
11585
+ /** Decode command text only for events stored before parsed selections were added. */
11586
+ function legacyCommand(text) {
11587
+ const parts = text.trim().split(/\s+/).filter(Boolean);
11588
+ if (parts.length === 0 || parts.length === 1 && parts[0] === "status") return {
11589
+ kind: "query",
11590
+ id: "status.get",
11591
+ input: {}
11592
+ };
11593
+ if (parts[0] === "task" && parts[1] !== void 0) {
11594
+ if (parts.length === 2) return {
11595
+ kind: "query",
11596
+ id: "task.get",
11597
+ input: { task: parts[1] }
11598
+ };
11599
+ if (parts.length === 3 && parts[2] === "history") return {
11600
+ kind: "query",
11601
+ id: "task.history",
11602
+ input: { task: parts[1] }
11603
+ };
11604
+ }
11605
+ return null;
11606
+ }
11474
11607
  function interactionOperation(event) {
11475
11608
  let id;
11476
11609
  let operationInput;
11477
11610
  if (event.kind === "command") {
11478
- const parts = event.text.trim().split(/\s+/);
11479
- id = queryId(parts);
11480
- operationInput = id === "status.get" ? {} : { task: parts[1] };
11611
+ const command = event.command;
11612
+ if (command?.kind === "query") {
11613
+ const operation = OPERATIONS.find((entry) => entry.id === command.id);
11614
+ if (operation?.kind === "query" && operation.slack?.kind === "slash-command") {
11615
+ const parsed = operation.input.safeParse(command.input);
11616
+ if (parsed.success) {
11617
+ id = operation.id;
11618
+ operationInput = parsed.data;
11619
+ }
11620
+ }
11621
+ }
11481
11622
  } else {
11482
11623
  const parts = event.actionId.split(":");
11483
11624
  if (parts.length === 2 && /^I[1-9]\d*$/.test(parts[1] ?? "")) {
@@ -11561,7 +11702,9 @@ function createInbound(input) {
11561
11702
  }
11562
11703
  const { id, operationInput } = interactionOperation(event);
11563
11704
  let response;
11564
- if (id === void 0) response = "not available yet";
11705
+ let cliHint;
11706
+ if (event.kind === "command" && event.command?.kind === "usage") response = plain(event.command.text);
11707
+ else if (id === void 0) response = plain("not allowed");
11565
11708
  else try {
11566
11709
  const value = await dispatchOperation({
11567
11710
  store,
@@ -11569,13 +11712,22 @@ function createInbound(input) {
11569
11712
  config,
11570
11713
  registry
11571
11714
  }, id, operationInput, "operator", event.dedupeKey);
11572
- response = JSON.stringify(value);
11715
+ if (event.kind === "command") {
11716
+ const reply = commandReply(id, value, operationInput);
11717
+ response = reply.body;
11718
+ cliHint = reply.cliHint;
11719
+ } else response = plain(JSON.stringify(value));
11573
11720
  } catch (error) {
11574
11721
  if (!(error instanceof CommandError)) await recordFailure(event, error, new Date(clock.now()).toISOString());
11575
- response = error instanceof CommandError && error.code !== "not_implemented" ? error.hint : "not available yet";
11722
+ response = plain(error instanceof CommandError && error.code !== "not_implemented" ? event.kind === "command" ? `${ERRORS[error.code].message} ${ERRORS[error.code].hint}` : error.hint : "not available yet");
11576
11723
  }
11577
11724
  try {
11578
- await gateway.respond(event.interaction, plain(response), { private: true });
11725
+ const safeResponse = event.kind === "command" ? await redactPrivateReply(response) : response;
11726
+ const safeHint = event.kind === "command" && cliHint !== void 0 ? await redactSecretMatches(cliHint) : cliHint;
11727
+ await gateway.respond(event.interaction, safeResponse, {
11728
+ private: true,
11729
+ ...safeHint === void 0 ? {} : { cliHint: safeHint }
11730
+ });
11579
11731
  } catch {}
11580
11732
  }
11581
11733
  async function applyUser(event) {
@@ -11658,7 +11810,15 @@ function createInbound(input) {
11658
11810
  return serial(async () => {
11659
11811
  const item = await store.db.selectFrom("operator_item").select(["cleared_at", "inbound_event_json"]).where("id", "=", itemId).executeTakeFirst();
11660
11812
  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.");
11661
- const event = inboundEventSchema.parse(JSON.parse(item.inbound_event_json));
11813
+ let event = inboundEventSchema.parse(JSON.parse(item.inbound_event_json));
11814
+ if (event.kind === "command" && event.command === void 0) {
11815
+ const command = legacyCommand(event.text);
11816
+ if (command === null) throw new CommandError("retry_not_applicable", "This interaction cannot be retried.");
11817
+ event = {
11818
+ ...event,
11819
+ command
11820
+ };
11821
+ }
11662
11822
  if (event.kind === "action" || event.kind === "command") {
11663
11823
  const { id, operationInput } = interactionOperation(event);
11664
11824
  if (id === void 0) throw new CommandError("retry_not_applicable", "This interaction cannot be retried.");
@@ -14433,6 +14593,71 @@ function renderText(body) {
14433
14593
  return block.kind === "quote" ? value.split("\n").map((line) => `> ${line}`).join("\n") : value;
14434
14594
  }).filter((value) => value.length > 0).join("\n\n");
14435
14595
  }
14596
+ function literal(value) {
14597
+ switch (value.kind) {
14598
+ case "text":
14599
+ case "emphasis":
14600
+ case "code": return value.text;
14601
+ case "link": return `${value.text} (${value.url})`;
14602
+ case "mention": return `<@${value.user}>`;
14603
+ }
14604
+ }
14605
+ /** Private replies use structured text so quoted data is never parsed as mrkdwn. */
14606
+ function renderResponse(body, cliHint) {
14607
+ const pointer = cliHint === void 0 ? "" : `More: ${cliHint} (CLI)`;
14608
+ const suffix = pointer === "" ? "" : `\n\n${pointer}`;
14609
+ let remaining = MAX_PART_TEXT - suffix.length;
14610
+ const maxElements = 40;
14611
+ const shown = [];
14612
+ let truncated = false;
14613
+ for (const block of body) {
14614
+ const value = block.kind === "code_block" ? block.text : block.children.map(literal).join("");
14615
+ if (value.length === 0) continue;
14616
+ if (shown.length >= maxElements || remaining <= 0) {
14617
+ truncated = true;
14618
+ break;
14619
+ }
14620
+ const part = value.slice(0, remaining);
14621
+ shown.push({
14622
+ kind: block.kind === "quote" ? "quote" : "paragraph",
14623
+ children: [{
14624
+ kind: "text",
14625
+ text: part
14626
+ }]
14627
+ });
14628
+ remaining -= part.length + 2;
14629
+ if (part.length < value.length) {
14630
+ truncated = true;
14631
+ break;
14632
+ }
14633
+ }
14634
+ const elements = shown.map((block) => ({
14635
+ type: block.kind === "quote" ? "rich_text_quote" : "rich_text_section",
14636
+ elements: [{
14637
+ type: "text",
14638
+ text: block.children.map(literal).join("")
14639
+ }]
14640
+ }));
14641
+ if (truncated && pointer !== "") elements.push({
14642
+ type: "rich_text_section",
14643
+ elements: [{
14644
+ type: "text",
14645
+ text: pointer
14646
+ }]
14647
+ });
14648
+ const fallback = shown.map((block) => {
14649
+ const value = block.children.map(literal).join("");
14650
+ return block.kind === "quote" ? `Quote: ${value}` : value;
14651
+ }).join("\n\n");
14652
+ const limit = MAX_PART_TEXT - (truncated ? suffix.length : 0);
14653
+ return {
14654
+ text: `${fallback.length <= limit ? fallback : splitText(fallback, limit)?.[0] ?? ""}${truncated ? suffix : ""}`,
14655
+ blocks: [{
14656
+ type: "rich_text",
14657
+ elements
14658
+ }]
14659
+ };
14660
+ }
14436
14661
  /** Prefer a paragraph or word boundary; never cut a Slack entity or angle-bracket token. */
14437
14662
  function splitText(text, max = MAX_PART_TEXT) {
14438
14663
  if (text.length === 0) return [];
@@ -14666,6 +14891,52 @@ function createDeliveryGateway(client, channels) {
14666
14891
  };
14667
14892
  }
14668
14893
  //#endregion
14894
+ //#region ../gateway-slack/src/commands.ts
14895
+ /** The catalog's slash bindings are the parser and help text's single source. */
14896
+ function descriptors(commandName) {
14897
+ return OPERATIONS.filter((operation) => operation.kind === "query" && operation.slack?.kind === "slash-command").map((operation) => ({
14898
+ operation,
14899
+ usage: operation.slack?.kind === "slash-command" ? operation.slack.usage.replace(/^\/board\b/, commandName) : ""
14900
+ }));
14901
+ }
14902
+ function commandUsage(commandName) {
14903
+ return `Usage:\n${descriptors(commandName).map(({ usage }) => usage).join("\n")}`;
14904
+ }
14905
+ function parseCommand(text, commandName) {
14906
+ const usage = () => ({
14907
+ kind: "usage",
14908
+ text: commandUsage(commandName)
14909
+ });
14910
+ const parts = text.trim().split(/\s+/).filter(Boolean);
14911
+ if (parts.length === 0) return {
14912
+ kind: "query",
14913
+ id: "status.get",
14914
+ input: {}
14915
+ };
14916
+ for (const { operation, usage: form } of descriptors(commandName)) {
14917
+ const pattern = form.slice(commandName.length).trim().split(/\s+/);
14918
+ if (pattern.length !== parts.length) continue;
14919
+ const input = {};
14920
+ if (!pattern.every((piece, index) => {
14921
+ const value = parts[index];
14922
+ if (value === void 0) return false;
14923
+ if (piece === "<KEY>") {
14924
+ const field = operation.cli.args[0];
14925
+ if (field === void 0) return false;
14926
+ input[field] = value;
14927
+ return true;
14928
+ }
14929
+ return piece === value;
14930
+ }) || !operation.input.safeParse(input).success) continue;
14931
+ return {
14932
+ kind: "query",
14933
+ id: operation.id,
14934
+ input
14935
+ };
14936
+ }
14937
+ return usage();
14938
+ }
14939
+ //#endregion
14669
14940
  //#region ../gateway-slack/src/normalize.ts
14670
14941
  function record$1(value) {
14671
14942
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
@@ -14800,7 +15071,7 @@ function normalizeMessage(channel, payload, context) {
14800
15071
  return events;
14801
15072
  }
14802
15073
  /** Actions and commands share the same stable trigger identity as their Slack retry. */
14803
- function normalizeInteraction(type, payload, channels, root) {
15074
+ function normalizeInteraction(type, payload, channels, root, commandName = "/board") {
14804
15075
  const body = record$1(payload);
14805
15076
  if (body === null) return [];
14806
15077
  const triggerId = string$1(body.trigger_id);
@@ -14809,6 +15080,7 @@ function normalizeInteraction(type, payload, channels, root) {
14809
15080
  if (user === void 0) return [];
14810
15081
  const author = userIdSchema.parse(user);
14811
15082
  if (type === "slash_commands") {
15083
+ if (body.command !== commandName) return [];
14812
15084
  const channel = string$1(body.channel_id);
14813
15085
  if (channel === void 0) return [];
14814
15086
  return [{
@@ -14817,6 +15089,7 @@ function normalizeInteraction(type, payload, channels, root) {
14817
15089
  interaction: interactionRef(channel, user, triggerId),
14818
15090
  author,
14819
15091
  text: text(body.text) ?? "",
15092
+ command: parseCommand(text(body.text) ?? "", commandName),
14820
15093
  sourceUrl: sourceUrl(channel),
14821
15094
  sourceSpace: sourceSpace(channel, channels)
14822
15095
  }];
@@ -14992,7 +15265,7 @@ function createSocket(api, config, botIdentity) {
14992
15265
  const threadTs = value(actionMessage?.thread_ts) ?? ts;
14993
15266
  const root = channel !== void 0 && threadTs !== void 0 && threadTs !== ts ? await rootFor(api, channel, threadTs) : actionMessage;
14994
15267
  events = normalizeInteraction(type, body, config.channels, root);
14995
- } else if (type === "slash_commands") events = normalizeInteraction(type, body, config.channels);
15268
+ } else if (type === "slash_commands") events = normalizeInteraction(type, body, config.channels, void 0, config.command);
14996
15269
  for (const event of events) await emit(event);
14997
15270
  await envelope.ack?.();
14998
15271
  }
@@ -15080,14 +15353,19 @@ function createSlackGateway(config) {
15080
15353
  recover: (request) => recoverThreads(api, request, botIdentity),
15081
15354
  async respond(interaction, text, options) {
15082
15355
  const addressed = parseInteractionRef(interaction);
15083
- const body = renderText(text);
15084
- if (addressed === null || body.length === 0) return "dropped";
15356
+ const rendered = renderResponse(text, options.cliHint);
15357
+ if (addressed === null || rendered.text.length === 0) return "dropped";
15085
15358
  try {
15086
- return (await api.chat.postEphemeral({
15359
+ const payload = {
15087
15360
  channel: addressed.channel,
15088
15361
  user: addressed.user,
15089
- text: body
15090
- })).ok ? "sent" : "dropped";
15362
+ text: rendered.text,
15363
+ blocks: rendered.blocks,
15364
+ mrkdwn: false,
15365
+ parse: "none",
15366
+ link_names: false
15367
+ };
15368
+ return (await api.chat.postEphemeral(payload)).ok ? "sent" : "dropped";
15091
15369
  } catch (error) {
15092
15370
  if (options.private) {
15093
15371
  const result = apiFailure(error);
@@ -15499,6 +15777,7 @@ function slackSettings(config, secrets) {
15499
15777
  }
15500
15778
  return {
15501
15779
  token,
15780
+ command: config.slack.command,
15502
15781
  ...appToken === void 0 ? {} : { appToken },
15503
15782
  channels: {
15504
15783
  orchestrators,
package/dist/mcp.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { j as ERRORS, s as OPERATIONS, u as renderMcpError } from "./src-BDNEze3S.mjs";
2
- import { r as CliError, t as requestOperation } from "./client-C81xon5D.mjs";
3
- import { t as loadClientConfig } from "./config-D8AEf8vU.mjs";
1
+ import { j as ERRORS, s as OPERATIONS, u as renderMcpError } from "./src-Cf78bzK5.mjs";
2
+ import { r as CliError, t as requestOperation } from "./client-sGJ4fzQ4.mjs";
3
+ import { t as loadClientConfig } from "./config-DPXFxZQy.mjs";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { z } from "zod";
@@ -1117,6 +1117,18 @@ const commandEventSchema = z.strictObject({
1117
1117
  interaction: interactionRefSchema,
1118
1118
  author: userIdSchema,
1119
1119
  text: text$1,
1120
+ command: z.discriminatedUnion("kind", [z.strictObject({
1121
+ kind: z.literal("query"),
1122
+ id: z.enum([
1123
+ "status.get",
1124
+ "task.get",
1125
+ "task.history"
1126
+ ]),
1127
+ input: z.unknown()
1128
+ }), z.strictObject({
1129
+ kind: z.literal("usage"),
1130
+ text: z.string().min(1)
1131
+ })]).optional(),
1120
1132
  sourceUrl,
1121
1133
  sourceSpace: sourceSpace.optional()
1122
1134
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spur-board",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "engines": {
6
6
  "node": "^24.12.0 || ^26.0.0"