spur-board 0.3.0 → 0.4.1
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 +6 -12
- package/dist/{client-C81xon5D.mjs → client-CHohYNxa.mjs} +1 -1
- package/dist/client.mjs +1 -1
- package/dist/{config-D8AEf8vU.mjs → config-BuxxS_er.mjs} +26 -7
- package/dist/daemon.mjs +303 -24
- package/dist/mcp.mjs +3 -3
- package/dist/{src-BDNEze3S.mjs → src-BEo3mvRC.mjs} +15 -2
- package/package.json +1 -1
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 { 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";
|
|
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";
|
|
@@ -140,12 +140,6 @@ function warning(id, fix) {
|
|
|
140
140
|
fix
|
|
141
141
|
};
|
|
142
142
|
}
|
|
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
143
|
function unitFile(env, home) {
|
|
150
144
|
const xdg = env["XDG_CONFIG_HOME"];
|
|
151
145
|
return join(xdg && isAbsolute(xdg) ? xdg : join(home, ".config"), "systemd", "user", "spur-board.service");
|
|
@@ -334,8 +328,8 @@ async function runDoctor(options = {}) {
|
|
|
334
328
|
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
329
|
let remote = null;
|
|
336
330
|
try {
|
|
337
|
-
const
|
|
338
|
-
const response = await ports.fetcher(new URL("/v1/doctor",
|
|
331
|
+
const address = resolveDaemonAddress(env, home, config.http.port);
|
|
332
|
+
const response = await ports.fetcher(new URL("/v1/doctor", address), { signal: AbortSignal.timeout(8e3) });
|
|
339
333
|
if (response.ok) remote = remoteChecks(await response.json());
|
|
340
334
|
} catch {}
|
|
341
335
|
checks.push(result("daemon.available", remote !== null, "Start or restart the user service."));
|
|
@@ -1032,7 +1026,7 @@ function createProgram(options = {}) {
|
|
|
1032
1026
|
const key = op.kind === "mutation" ? keyOption || randomUUID() : void 0;
|
|
1033
1027
|
if (key) io.stderr(`key: ${escapeTerminalControls(key)}\n`);
|
|
1034
1028
|
try {
|
|
1035
|
-
const result = await requestOperation(op, input, key, config);
|
|
1029
|
+
const result = await (options.sendOperation ?? requestOperation)(op, input, key, config);
|
|
1036
1030
|
io.stdout(`${json || !io.isTTY ? JSON.stringify(result) : formatHuman(result)}\n`);
|
|
1037
1031
|
} catch (error) {
|
|
1038
1032
|
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-BEo3mvRC.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-CHohYNxa.mjs";
|
|
2
2
|
export { requestOperation, requestTokenEnrollment };
|
|
@@ -1,13 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { I as configSchema } from "./src-BEo3mvRC.mjs";
|
|
2
|
+
import { r as CliError } from "./client-CHohYNxa.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, 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";
|
|
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
|
-
|
|
7547
|
-
|
|
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
|
-
})
|
|
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
|
|
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
|
|
11479
|
-
|
|
11480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
15084
|
-
if (addressed === null ||
|
|
15356
|
+
const rendered = renderResponse(text, options.cliHint);
|
|
15357
|
+
if (addressed === null || rendered.text.length === 0) return "dropped";
|
|
15085
15358
|
try {
|
|
15086
|
-
|
|
15359
|
+
const payload = {
|
|
15087
15360
|
channel: addressed.channel,
|
|
15088
15361
|
user: addressed.user,
|
|
15089
|
-
text:
|
|
15090
|
-
|
|
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-
|
|
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-BEo3mvRC.mjs";
|
|
2
|
+
import { r as CliError, t as requestOperation } from "./client-CHohYNxa.mjs";
|
|
3
|
+
import { n as loadClientConfig } from "./config-BuxxS_er.mjs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -110,6 +110,7 @@ const SLACK_BOT_SCOPES = [
|
|
|
110
110
|
];
|
|
111
111
|
//#endregion
|
|
112
112
|
//#region ../contracts/src/config.ts
|
|
113
|
+
const DEFAULT_HTTP_PORT = 4311;
|
|
113
114
|
const WEEKDAYS = [
|
|
114
115
|
"monday",
|
|
115
116
|
"tuesday",
|
|
@@ -276,7 +277,7 @@ const configSchema = z.strictObject({
|
|
|
276
277
|
message: "Final unanswered pause must exceed the second retry pause"
|
|
277
278
|
});
|
|
278
279
|
}).prefault({}),
|
|
279
|
-
http: z.strictObject({ port: z.int().min(0).max(65535).default(
|
|
280
|
+
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
281
|
adapters: z.strictObject({
|
|
281
282
|
spur: spurAdapterSection,
|
|
282
283
|
github: adapterSection(6e4),
|
|
@@ -1117,6 +1118,18 @@ const commandEventSchema = z.strictObject({
|
|
|
1117
1118
|
interaction: interactionRefSchema,
|
|
1118
1119
|
author: userIdSchema,
|
|
1119
1120
|
text: text$1,
|
|
1121
|
+
command: z.discriminatedUnion("kind", [z.strictObject({
|
|
1122
|
+
kind: z.literal("query"),
|
|
1123
|
+
id: z.enum([
|
|
1124
|
+
"status.get",
|
|
1125
|
+
"task.get",
|
|
1126
|
+
"task.history"
|
|
1127
|
+
]),
|
|
1128
|
+
input: z.unknown()
|
|
1129
|
+
}), z.strictObject({
|
|
1130
|
+
kind: z.literal("usage"),
|
|
1131
|
+
text: z.string().min(1)
|
|
1132
|
+
})]).optional(),
|
|
1120
1133
|
sourceUrl,
|
|
1121
1134
|
sourceSpace: sourceSpace.optional()
|
|
1122
1135
|
});
|
|
@@ -2618,4 +2631,4 @@ var AdapterError = class extends Error {
|
|
|
2618
2631
|
}
|
|
2619
2632
|
};
|
|
2620
2633
|
//#endregion
|
|
2621
|
-
export { taskKeySchema as A, sessionFactsSchema as C, errorCodeOf as D, encodeActorId as E,
|
|
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 };
|