@pinet/slack-bridge 0.2.2 → 0.2.6
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/README.md +316 -451
- package/dist/activity-log.d.ts +10 -3
- package/dist/agent-event-runtime.d.ts +0 -2
- package/dist/agent-event-runtime.js +0 -1
- package/dist/agent-prompt-guidance.d.ts +1 -6
- package/dist/agent-prompt-guidance.js +1 -6
- package/dist/broker/adapters/slack.d.ts +4 -1
- package/dist/broker/adapters/slack.js +16 -9
- package/dist/broker/client.d.ts +21 -4
- package/dist/broker/client.js +65 -25
- package/dist/broker/hibernation-activation-authority.d.ts +22 -0
- package/dist/broker/hibernation-activation-authority.js +51 -0
- package/dist/broker/hibernation-activation.d.ts +75 -0
- package/dist/broker/hibernation-activation.js +95 -0
- package/dist/broker/hibernation-command-router.d.ts +54 -0
- package/dist/broker/hibernation-command-router.js +124 -0
- package/dist/broker/hibernation-runtime-adapters.d.ts +79 -0
- package/dist/broker/hibernation-runtime-adapters.js +473 -0
- package/dist/broker/hibernation-runtime-helpers.d.ts +146 -0
- package/dist/broker/hibernation-runtime-helpers.js +243 -0
- package/dist/broker/index.d.ts +16 -1
- package/dist/broker/index.js +68 -10
- package/dist/broker/lock-conflict.d.ts +109 -0
- package/dist/broker/lock-conflict.js +409 -0
- package/dist/broker/socket-server.d.ts +26 -0
- package/dist/broker/socket-server.js +287 -4
- package/dist/broker-delivery.d.ts +1 -0
- package/dist/broker-delivery.js +10 -1
- package/dist/broker-runtime.d.ts +14 -2
- package/dist/broker-runtime.js +114 -3
- package/dist/canvases.d.ts +12 -2
- package/dist/canvases.js +1 -3
- package/dist/compaction-gate.d.ts +7 -0
- package/dist/compaction-gate.js +35 -0
- package/dist/deploy-manifest.d.ts +8 -2
- package/dist/deploy-manifest.js +9 -7
- package/dist/follower-runtime.d.ts +5 -2
- package/dist/follower-runtime.js +39 -5
- package/dist/git-metadata.d.ts +5 -9
- package/dist/git-metadata.js +23 -42
- package/dist/guardrails.js +5 -0
- package/dist/helpers.d.ts +77 -27
- package/dist/helpers.js +150 -39
- package/dist/hibernation-config.d.ts +13 -0
- package/dist/hibernation-config.js +14 -0
- package/dist/home-tab.d.ts +13 -2
- package/dist/imessage-tools.d.ts +4 -1
- package/dist/inbox-drain-runtime.d.ts +1 -0
- package/dist/inbox-drain-runtime.js +5 -1
- package/dist/index.js +341 -108
- package/dist/pinet-commands.d.ts +14 -1
- package/dist/pinet-commands.js +93 -22
- package/dist/pinet-mesh-ops.d.ts +6 -4
- package/dist/pinet-mesh-ops.js +9 -1
- package/dist/pinet-runtime-composition.d.ts +2 -1
- package/dist/pinet-runtime-composition.js +14 -3
- package/dist/pinet-session-formatting.d.ts +26 -2
- package/dist/pinet-session-formatting.js +2 -0
- package/dist/pinet-tools.d.ts +14 -0
- package/dist/pinet-tools.js +355 -50
- package/dist/prompts/broker/default.md +3 -3
- package/dist/prompts/broker/tmux.md +3 -3
- package/dist/runtime-agent-context.d.ts +3 -3
- package/dist/runtime-agent-context.js +12 -6
- package/dist/runtime-mode.js +4 -0
- package/dist/session-ui-runtime.js +3 -1
- package/dist/single-player-runtime.d.ts +1 -1
- package/dist/single-player-runtime.js +21 -5
- package/dist/slack-access.d.ts +10 -2
- package/dist/slack-access.js +64 -22
- package/dist/slack-block-kit.d.ts +44 -2
- package/dist/slack-block-kit.js +5 -5
- package/dist/slack-file-access.d.ts +12 -1
- package/dist/slack-file-access.js +4 -4
- package/dist/slack-message-context.d.ts +9 -2
- package/dist/slack-modals.d.ts +4 -1
- package/dist/slack-modals.js +5 -5
- package/dist/slack-pinet-runtime-adapter.d.ts +1 -0
- package/dist/slack-pinet-runtime-adapter.js +10 -0
- package/dist/slack-presence.d.ts +5 -4
- package/dist/slack-request-runtime.d.ts +3 -2
- package/dist/slack-scope-diagnostics.d.ts +27 -2
- package/dist/slack-socket-dedup.d.ts +8 -5
- package/dist/slack-tools.d.ts +10 -0
- package/dist/slack-tools.js +51 -64
- package/dist/slack-upload.d.ts +17 -1
- package/dist/subtree-broker-runtime.d.ts +83 -2
- package/dist/subtree-broker-runtime.js +774 -109
- package/dist/tool-output-limits.d.ts +11 -0
- package/dist/tool-output-limits.js +20 -0
- package/dist/tool-registration-runtime.d.ts +3 -0
- package/dist/tool-registration-runtime.js +27 -0
- package/package.json +8 -8
|
@@ -10,9 +10,9 @@ IF ASKED TO CODE: Refuse politely and immediately delegate. Say: "I'm the broker
|
|
|
10
10
|
|
|
11
11
|
ALLOWED — These are your responsibilities: (1) Route messages between humans and agents. (2) Check `pinet action=agents` for idle workers and delegate tasks via `pinet action=send`. (3) Coordinate GitHub issues/PRs and request reviews, but do NOT launch local review subagents from the broker. (4) Monitor agent health via the RALPH loop. (5) Relay status updates, answer questions about system state, and coordinate workflows. (6) Use bash for read-only inspection and lightweight GitHub coordination: git log, git status, gh pr list, gh pr view, ls, cat — never for code changes or implementation work. (7) Use tmux only to launch repo-scoped Pinet follower workers when capacity is missing.
|
|
12
12
|
|
|
13
|
-
PRIORITIZED ISSUE GATE:
|
|
13
|
+
GUGU91/PINET PRIORITIZED ISSUE GATE: For work in `gugu91/pinet`, do not start, assign, or broadcast changes unless the request names a GitHub issue/PR and carries clear maintainer priority/approval. Do not self-start from that repository's open issue lists. If unclear, stop and ask. This gate is specific to `gugu91/pinet`; unrelated repositories, including `tmustier/pi-extensions`, follow their own instructions and normal requester authority.
|
|
14
14
|
|
|
15
|
-
DELEGATE, THEN TRACK: Do not perform task triage yourself beyond checking explicit routing requirements already present in the request: repo, issue/PR number
|
|
15
|
+
DELEGATE, THEN TRACK: Do not perform task triage yourself beyond checking explicit routing requirements already present in the request: repo, and, where that repo's instructions require them, issue/PR number and maintainer approval, plus branch/worktree and worker availability. If required routing facts are missing, ask; otherwise assign promptly.
|
|
16
16
|
|
|
17
17
|
PM MODE AWARENESS: For complex or coordination-heavy maintainer-delegated tasks, offer PM mode instead of assuming it. PM mode is consent-gated: ask/confirm before enabling unless the maintainer explicitly requested PM-style coordination. When enabled, assign an accountable follower as PM/coordinator; that follower nominates an implementation lead, delegates implementation, coordinates blockers/status, performs second-pass review, and coordinates merge readiness. The broker remains coordination-only and must not implement or launch local subagents. Use durable lane metadata (`pinet action=lanes`) so PM-mode role/lane state is inspectable by RALPH/status flows. A `detached` lane means human/manual supervision; keep it visible, but do not auto-reassign it as normal broker-managed work without explicit human/broker action.
|
|
18
18
|
|
|
@@ -26,7 +26,7 @@ When a human asks for work to be done, ALWAYS check `pinet action=agents` for id
|
|
|
26
26
|
|
|
27
27
|
If a repo instruction says to use the `code-reviewer` subagent, treat that as work for the owning connected worker in the same repo to run locally and summarize back — never the broker itself.
|
|
28
28
|
|
|
29
|
-
When delegating, include: task,
|
|
29
|
+
When delegating, include: task, repo/branch/worktree setup, where to report back (Slack thread_ts), and any issue/PR numbers or maintainer priority/approval required by the target repository's instructions.
|
|
30
30
|
|
|
31
31
|
If no repo-matched workers are available and new capacity is needed, you may spin up a worker as broker infrastructure: create a tmux session in the target repo, launch `pi` with `PINET_BROKER_MANAGED=1 PINET_BROKER_AGENT_ID=<current-broker-agent-id> PINET_LAUNCH_SOURCE=broker-tmux PINET_TMUX_SESSION=<session>`, run `/pinet follow`, wait for the worker in `pinet action=agents`, then delegate via `pinet action=send`. These env vars let the broker safely distinguish broker-managed follower PIDs from unrelated local processes before ghost reaping. NEVER do the work yourself or cross-route to another repo as a fallback.
|
|
32
32
|
|
|
@@ -10,9 +10,9 @@ IF ASKED TO CODE: Refuse politely and immediately delegate. Say: "I'm the broker
|
|
|
10
10
|
|
|
11
11
|
ALLOWED — These are your responsibilities: (1) Route messages between humans and agents. (2) Check `pinet action=agents` for idle workers and delegate tasks via `pinet action=send`. (3) Coordinate GitHub issues/PRs and request reviews, but do NOT launch local review subagents from the broker. (4) Monitor agent health via the RALPH loop. (5) Relay status updates, answer questions about system state, and coordinate workflows. (6) Use bash for read-only inspection and lightweight GitHub coordination: git log, git status, gh pr list, gh pr view, ls, cat — never for code changes or implementation work. (7) Use tmux only for broker-managed follower lifecycle operations: launch repo-scoped workers, inspect recorded broker-managed sessions, and close safely mapped sessions after their workers have exited; never manipulate unrelated or unmapped tmux sessions.
|
|
12
12
|
|
|
13
|
-
PRIORITIZED ISSUE GATE:
|
|
13
|
+
GUGU91/PINET PRIORITIZED ISSUE GATE: For work in `gugu91/pinet`, do not start, assign, or broadcast changes unless the request names a GitHub issue/PR and carries clear maintainer priority/approval. Do not self-start from that repository's open issue lists. If unclear, stop and ask. This gate is specific to `gugu91/pinet`; unrelated repositories, including `tmustier/pi-extensions`, follow their own instructions and normal requester authority.
|
|
14
14
|
|
|
15
|
-
DELEGATE, THEN TRACK: Do not perform task triage yourself beyond checking explicit routing requirements already present in the request: repo, issue/PR number
|
|
15
|
+
DELEGATE, THEN TRACK: Do not perform task triage yourself beyond checking explicit routing requirements already present in the request: repo, and, where that repo's instructions require them, issue/PR number and maintainer approval, plus branch/worktree and worker availability. If required routing facts are missing, ask; otherwise assign promptly.
|
|
16
16
|
|
|
17
17
|
PM MODE AWARENESS: For complex or coordination-heavy maintainer-delegated tasks, offer PM mode instead of assuming it. PM mode is consent-gated: ask/confirm before enabling unless the maintainer explicitly requested PM-style coordination. When enabled, assign an accountable follower as PM/coordinator; that follower nominates an implementation lead, delegates implementation, coordinates blockers/status, performs second-pass review, and coordinates merge readiness. The broker remains coordination-only and must not implement or launch local subagents. Use durable lane metadata (`pinet action=lanes`) so PM-mode role/lane state is inspectable by RALPH/status flows. A `detached` lane means human/manual supervision; keep it visible, but do not auto-reassign it as normal broker-managed work without explicit human/broker action.
|
|
18
18
|
|
|
@@ -34,7 +34,7 @@ When a human asks for work to be done, ALWAYS check `pinet action=agents` for id
|
|
|
34
34
|
|
|
35
35
|
If a repo instruction says to use the `code-reviewer` subagent, treat that as work for the owning connected worker in the same repo to run locally and summarize back — never the broker itself.
|
|
36
36
|
|
|
37
|
-
When delegating, include: task,
|
|
37
|
+
When delegating, include: task, repo/branch/worktree setup, acceptance criteria, relevant files or docs, where to report back (Slack thread_ts or Pinet thread), and any issue/PR numbers or maintainer priority/approval required by the target repository's instructions.
|
|
38
38
|
|
|
39
39
|
If no repo-matched workers are available and new capacity is needed, you may spin up a worker as broker infrastructure: create a tmux session in the target repo, launch `pi` with `PINET_BROKER_MANAGED=1 PINET_BROKER_AGENT_ID=<current-broker-agent-id> PINET_LAUNCH_SOURCE=broker-tmux PINET_TMUX_SESSION=<session>`, run `/pinet follow`, wait for the worker in `pinet action=agents`, then delegate via `pinet action=send`. NEVER do the work yourself or cross-route to another repo as a fallback.
|
|
40
40
|
|
|
@@ -56,9 +56,9 @@ export interface RuntimeAgentContextDeps {
|
|
|
56
56
|
getExtensionContext: () => ExtensionContext | null;
|
|
57
57
|
persistState: () => void;
|
|
58
58
|
updateBadge: () => void;
|
|
59
|
-
getGitContext: () => Promise<GitContext>;
|
|
59
|
+
getGitContext: (signal?: AbortSignal) => Promise<GitContext>;
|
|
60
60
|
/** Optional test hook for live branch/dirty probing. */
|
|
61
|
-
getDynamicGitState?: (cwd: string) => Promise<GitDynamicState>;
|
|
61
|
+
getDynamicGitState?: (cwd: string, signal?: AbortSignal) => Promise<GitDynamicState>;
|
|
62
62
|
}
|
|
63
63
|
export interface RuntimeAgentContext {
|
|
64
64
|
isUserAllowed: (userId: string) => boolean;
|
|
@@ -79,7 +79,7 @@ export interface RuntimeAgentContext {
|
|
|
79
79
|
snapshotReloadableRuntime: () => ReloadableRuntimeSnapshot;
|
|
80
80
|
restoreReloadableRuntime: (snapshot: ReloadableRuntimeSnapshot) => void;
|
|
81
81
|
detectProjectTools: (repoRoot: string, cwd: string) => string[];
|
|
82
|
-
getAgentMetadata: (role?: RuntimeAgentRole) => Promise<Record<string, unknown>>;
|
|
82
|
+
getAgentMetadata: (role?: RuntimeAgentRole, signal?: AbortSignal) => Promise<Record<string, unknown>>;
|
|
83
83
|
asStringValue: (value: unknown) => string | undefined;
|
|
84
84
|
getMeshRoleFromMetadata: (metadata: Record<string, unknown> | undefined, fallback?: RuntimeAgentRole) => RuntimeAgentRole;
|
|
85
85
|
buildSkinMetadata: (metadata: Record<string, unknown> | undefined, personality: string, statusVocabulary?: PinetSkinStatusVocabulary) => Record<string, unknown>;
|
|
@@ -185,18 +185,20 @@ export function createRuntimeAgentContext(deps) {
|
|
|
185
185
|
tools.add("git");
|
|
186
186
|
return [...tools].sort();
|
|
187
187
|
}
|
|
188
|
-
const dynamicGitProbe = deps.getDynamicGitState ??
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
const dynamicGitProbe = deps.getDynamicGitState ??
|
|
189
|
+
((cwd, signal) => probeGitDynamic(cwd, undefined, signal));
|
|
190
|
+
async function getAgentMetadata(role = "worker", signal) {
|
|
191
|
+
const gitContext = await deps.getGitContext(signal);
|
|
191
192
|
const { cwd, repo, repoRoot } = gitContext;
|
|
192
193
|
const resolvedRepoRoot = repoRoot ?? cwd;
|
|
193
194
|
const tools = detectProjectTools(resolvedRepoRoot, cwd);
|
|
194
195
|
const scope = buildSlackCompatibilityScope();
|
|
195
196
|
let dynamic;
|
|
196
197
|
try {
|
|
197
|
-
dynamic = await dynamicGitProbe(cwd);
|
|
198
|
+
dynamic = await dynamicGitProbe(cwd, signal);
|
|
198
199
|
}
|
|
199
200
|
catch {
|
|
201
|
+
signal?.throwIfAborted();
|
|
200
202
|
dynamic = { probeFailed: true };
|
|
201
203
|
}
|
|
202
204
|
const branch = dynamic.branch;
|
|
@@ -215,12 +217,16 @@ export function createRuntimeAgentContext(deps) {
|
|
|
215
217
|
const skinAssignment = resolveSkinAssignment(role, getIdentitySeedForRole(role));
|
|
216
218
|
const brokerManaged = role === "worker" && process.env.PINET_BROKER_MANAGED === "1";
|
|
217
219
|
const parentAgentId = process.env.PINET_PARENT_AGENT_ID?.trim() || undefined;
|
|
220
|
+
const launchSource = process.env.PINET_LAUNCH_SOURCE?.trim() || "broker-tmux";
|
|
221
|
+
const herdrLaunch = launchSource === "broker-herdr" || launchSource === "subtree-broker-herdr";
|
|
222
|
+
const tmuxSession = process.env.PINET_TMUX_SESSION?.trim() || undefined;
|
|
218
223
|
const brokerManagedMetadata = brokerManaged
|
|
219
224
|
? {
|
|
220
225
|
brokerManaged: true,
|
|
221
226
|
brokerManagedBy: process.env.PINET_BROKER_AGENT_ID?.trim() || undefined,
|
|
222
|
-
launchSource
|
|
223
|
-
|
|
227
|
+
launchSource,
|
|
228
|
+
runtimeKind: herdrLaunch ? "herdr" : "tmux",
|
|
229
|
+
...(!herdrLaunch && tmuxSession ? { runtimeLocator: tmuxSession, tmuxSession } : {}),
|
|
224
230
|
brokerManagedAt: new Date().toISOString(),
|
|
225
231
|
...(parentAgentId ? { parentAgentId, pinetParentAgentId: parentAgentId } : {}),
|
|
226
232
|
...(process.env.PINET_ROOT_AGENT_ID?.trim()
|
package/dist/runtime-mode.js
CHANGED
|
@@ -16,8 +16,12 @@ export function isBrokerManagedFollowerLaunch(env = process.env) {
|
|
|
16
16
|
return false;
|
|
17
17
|
if (env.PINET_LAUNCH_SOURCE === "broker-tmux")
|
|
18
18
|
return true;
|
|
19
|
+
if (env.PINET_LAUNCH_SOURCE === "broker-herdr")
|
|
20
|
+
return true;
|
|
19
21
|
if (env.PINET_LAUNCH_SOURCE === "subtree-broker-tmux")
|
|
20
22
|
return true;
|
|
23
|
+
if (env.PINET_LAUNCH_SOURCE === "subtree-broker-herdr")
|
|
24
|
+
return true;
|
|
21
25
|
return Boolean(env.PINET_SOCKET_PATH?.trim());
|
|
22
26
|
}
|
|
23
27
|
export function resolveSlackBridgeStartupRuntimeMode(settings, options = {}) {
|
|
@@ -43,6 +43,7 @@ export function createSessionUiRuntime(deps) {
|
|
|
43
43
|
let terminalInputUnsubscribe = null;
|
|
44
44
|
let deferredDrainTimer = null;
|
|
45
45
|
let extCtx = null;
|
|
46
|
+
let statusState = "ok";
|
|
46
47
|
function getExtensionContext() {
|
|
47
48
|
return extCtx;
|
|
48
49
|
}
|
|
@@ -50,7 +51,7 @@ export function createSessionUiRuntime(deps) {
|
|
|
50
51
|
extCtx = ctx;
|
|
51
52
|
}
|
|
52
53
|
function updateBadge() {
|
|
53
|
-
if (!extCtx?.hasUI)
|
|
54
|
+
if (statusState !== "ok" || !extCtx?.hasUI)
|
|
54
55
|
return;
|
|
55
56
|
const t = extCtx.ui.theme;
|
|
56
57
|
const n = deps.getInboxLength();
|
|
@@ -60,6 +61,7 @@ export function createSessionUiRuntime(deps) {
|
|
|
60
61
|
extCtx.ui.setStatus("slack-bridge", label);
|
|
61
62
|
}
|
|
62
63
|
function setExtStatus(ctx, state) {
|
|
64
|
+
statusState = state;
|
|
63
65
|
if (!ctx.hasUI)
|
|
64
66
|
return;
|
|
65
67
|
extCtx = ctx;
|
|
@@ -59,7 +59,7 @@ export interface SinglePlayerRuntimeDeps {
|
|
|
59
59
|
beginThreadStatus?: (channelId: string, threadTs: string, status: string) => Promise<void>;
|
|
60
60
|
}
|
|
61
61
|
export interface SinglePlayerRuntime {
|
|
62
|
-
connect: (ctx: ExtensionContext) => Promise<void>;
|
|
62
|
+
connect: (ctx: ExtensionContext, signal?: AbortSignal) => Promise<void>;
|
|
63
63
|
disconnect: () => Promise<void>;
|
|
64
64
|
getBotUserId: () => string | null;
|
|
65
65
|
isConnected: () => boolean;
|
|
@@ -356,7 +356,8 @@ export function createSinglePlayerRuntime(deps) {
|
|
|
356
356
|
},
|
|
357
357
|
};
|
|
358
358
|
return {
|
|
359
|
-
async connect(ctx) {
|
|
359
|
+
async connect(ctx, signal) {
|
|
360
|
+
signal?.throwIfAborted();
|
|
360
361
|
shuttingDown = false;
|
|
361
362
|
const socket = new SlackSocketModeClient({
|
|
362
363
|
slack: deps.slack,
|
|
@@ -371,9 +372,14 @@ export function createSinglePlayerRuntime(deps) {
|
|
|
371
372
|
deps.setExtStatus(ctx, "reconnecting");
|
|
372
373
|
}
|
|
373
374
|
},
|
|
374
|
-
onError: (error) => {
|
|
375
|
-
if (
|
|
376
|
-
|
|
375
|
+
onError: (error, source) => {
|
|
376
|
+
if (isAbortError(error))
|
|
377
|
+
return;
|
|
378
|
+
if (source === "connection") {
|
|
379
|
+
deps.setExtStatus(ctx, "error");
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
ctx.ui.notify(`Slack event failed: ${deps.formatError(error)}`, "error");
|
|
377
383
|
}
|
|
378
384
|
},
|
|
379
385
|
onThreadStarted: (event) => onThreadStarted(event),
|
|
@@ -398,7 +404,17 @@ export function createSinglePlayerRuntime(deps) {
|
|
|
398
404
|
onInteractive: (event) => queueInteractiveInboxEvent(event, ctx),
|
|
399
405
|
});
|
|
400
406
|
slackSocket = socket;
|
|
401
|
-
|
|
407
|
+
const abortConnect = () => {
|
|
408
|
+
void socket.disconnect().catch(() => undefined);
|
|
409
|
+
};
|
|
410
|
+
signal?.addEventListener("abort", abortConnect, { once: true });
|
|
411
|
+
try {
|
|
412
|
+
await socket.connect();
|
|
413
|
+
signal?.throwIfAborted();
|
|
414
|
+
}
|
|
415
|
+
finally {
|
|
416
|
+
signal?.removeEventListener("abort", abortConnect);
|
|
417
|
+
}
|
|
402
418
|
},
|
|
403
419
|
async disconnect() {
|
|
404
420
|
shuttingDown = true;
|
package/dist/slack-access.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RuntimeScopeCarrier } from "@pinet/transport-core";
|
|
2
|
+
import { type SlackMessageFileMetadata } from "./slack-message-context.js";
|
|
2
3
|
import { type SlackInteractiveInboxEvent } from "./slack-block-kit.js";
|
|
3
4
|
import { type ThreadOwnerHint } from "./broker/router.js";
|
|
4
5
|
export { SLACK_SOCKET_DELIVERY_DEDUP_MAX_SIZE, SLACK_SOCKET_DELIVERY_DEDUP_TTL_MS, } from "./slack-socket-dedup.js";
|
|
@@ -70,6 +71,10 @@ export declare function extractAppHomeOpened(evt: Record<string, unknown>): Pars
|
|
|
70
71
|
* Classification result for an incoming message event.
|
|
71
72
|
* Uses a discriminated union so TypeScript narrows fields when relevant is true.
|
|
72
73
|
*/
|
|
74
|
+
export interface SlackMessageClassificationMetadata extends Record<string, unknown> {
|
|
75
|
+
slackSubtype?: string;
|
|
76
|
+
slackFiles?: SlackMessageFileMetadata[];
|
|
77
|
+
}
|
|
73
78
|
export type MessageClassification = {
|
|
74
79
|
relevant: false;
|
|
75
80
|
} | {
|
|
@@ -81,7 +86,7 @@ export type MessageClassification = {
|
|
|
81
86
|
isDM: boolean;
|
|
82
87
|
isChannelMention: boolean;
|
|
83
88
|
messageTs: string;
|
|
84
|
-
metadata?:
|
|
89
|
+
metadata?: SlackMessageClassificationMetadata;
|
|
85
90
|
};
|
|
86
91
|
/**
|
|
87
92
|
* Classify an incoming Slack message event. Determines whether the
|
|
@@ -157,6 +162,7 @@ export interface ResolveSlackThreadOwnerHintInput {
|
|
|
157
162
|
}
|
|
158
163
|
export declare function resolveSlackThreadOwnerHint(input: ResolveSlackThreadOwnerHintInput): Promise<ThreadOwnerHint | null>;
|
|
159
164
|
export declare const RECONNECT_DELAY_MS = 5000;
|
|
165
|
+
export type SlackSocketErrorSource = "connection" | "event";
|
|
160
166
|
export interface SlackSocketModeClientConfig {
|
|
161
167
|
slack: SlackCall;
|
|
162
168
|
botToken: string;
|
|
@@ -167,7 +173,7 @@ export interface SlackSocketModeClientConfig {
|
|
|
167
173
|
abortAndWait?: () => Promise<void>;
|
|
168
174
|
onOpen?: () => void;
|
|
169
175
|
onReconnectScheduled?: () => void;
|
|
170
|
-
onError?: (error: unknown) => void;
|
|
176
|
+
onError?: (error: unknown, source: SlackSocketErrorSource) => void;
|
|
171
177
|
onThreadStarted?: (event: ParsedThreadStarted) => Promise<void> | void;
|
|
172
178
|
onThreadContextChanged?: (event: ParsedThreadContextChanged) => Promise<void> | void;
|
|
173
179
|
onMessage?: (event: Record<string, unknown>) => Promise<void> | void;
|
|
@@ -184,7 +190,9 @@ export declare class SlackSocketModeClient {
|
|
|
184
190
|
private readonly config;
|
|
185
191
|
private botUserId;
|
|
186
192
|
private ws;
|
|
193
|
+
private pendingSocket;
|
|
187
194
|
private reconnectTimer;
|
|
195
|
+
private connecting;
|
|
188
196
|
private shuttingDown;
|
|
189
197
|
constructor(config: SlackSocketModeClientConfig);
|
|
190
198
|
getBotUserId(): string | null;
|
package/dist/slack-access.js
CHANGED
|
@@ -330,7 +330,9 @@ export class SlackSocketModeClient {
|
|
|
330
330
|
config;
|
|
331
331
|
botUserId = null;
|
|
332
332
|
ws = null;
|
|
333
|
+
pendingSocket = null;
|
|
333
334
|
reconnectTimer = null;
|
|
335
|
+
connecting = false;
|
|
334
336
|
shuttingDown = false;
|
|
335
337
|
constructor(config) {
|
|
336
338
|
this.config = config;
|
|
@@ -343,11 +345,22 @@ export class SlackSocketModeClient {
|
|
|
343
345
|
}
|
|
344
346
|
async connect() {
|
|
345
347
|
this.shuttingDown = false;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
348
|
+
const authPromise = (this.config.resolveBotUserIdOnConnect ?? true)
|
|
349
|
+
? this.config.slack("auth.test", this.config.botToken)
|
|
350
|
+
: Promise.resolve(null);
|
|
351
|
+
const socketResponsePromise = this.config.slack("apps.connections.open", this.config.appToken);
|
|
352
|
+
void socketResponsePromise.catch(() => undefined);
|
|
353
|
+
try {
|
|
354
|
+
const auth = await authPromise;
|
|
355
|
+
if (auth) {
|
|
356
|
+
this.botUserId = typeof auth.user_id === "string" ? auth.user_id : null;
|
|
357
|
+
}
|
|
358
|
+
await this.connectSocketMode(socketResponsePromise);
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
await this.disconnect().catch(() => undefined);
|
|
362
|
+
throw error;
|
|
349
363
|
}
|
|
350
|
-
await this.connectSocketMode();
|
|
351
364
|
}
|
|
352
365
|
async disconnect() {
|
|
353
366
|
this.shuttingDown = true;
|
|
@@ -357,51 +370,78 @@ export class SlackSocketModeClient {
|
|
|
357
370
|
}
|
|
358
371
|
try {
|
|
359
372
|
this.ws?.close();
|
|
373
|
+
this.pendingSocket?.close();
|
|
360
374
|
}
|
|
361
375
|
catch {
|
|
362
376
|
/* ignore close errors */
|
|
363
377
|
}
|
|
364
378
|
this.ws = null;
|
|
379
|
+
this.pendingSocket = null;
|
|
365
380
|
await this.config.abortAndWait?.();
|
|
366
381
|
}
|
|
367
|
-
async connectSocketMode() {
|
|
368
|
-
if (this.shuttingDown)
|
|
382
|
+
async connectSocketMode(pendingResponse) {
|
|
383
|
+
if (this.shuttingDown || this.connecting)
|
|
369
384
|
return;
|
|
385
|
+
this.connecting = true;
|
|
386
|
+
let shouldReconnect = false;
|
|
370
387
|
try {
|
|
371
|
-
const response = await
|
|
372
|
-
|
|
373
|
-
this.
|
|
388
|
+
const response = await (pendingResponse ??
|
|
389
|
+
this.config.slack("apps.connections.open", this.config.appToken));
|
|
390
|
+
if (this.shuttingDown)
|
|
391
|
+
return;
|
|
392
|
+
const previous = this.ws;
|
|
393
|
+
const socket = new WebSocket(response.url);
|
|
394
|
+
this.pendingSocket = socket;
|
|
395
|
+
socket.addEventListener("open", () => {
|
|
396
|
+
if (this.pendingSocket !== socket || this.shuttingDown)
|
|
397
|
+
return;
|
|
398
|
+
this.pendingSocket = null;
|
|
399
|
+
this.ws = socket;
|
|
400
|
+
previous?.close();
|
|
374
401
|
this.config.onOpen?.();
|
|
375
402
|
});
|
|
376
|
-
|
|
377
|
-
void this.handleFrame(String(event.data)).catch((error) => {
|
|
378
|
-
this.
|
|
403
|
+
socket.addEventListener("message", (event) => {
|
|
404
|
+
void this.handleFrame(socket, String(event.data)).catch((error) => {
|
|
405
|
+
if (!this.shuttingDown) {
|
|
406
|
+
this.config.onError?.(error, "event");
|
|
407
|
+
}
|
|
379
408
|
});
|
|
380
409
|
});
|
|
381
|
-
|
|
382
|
-
if (
|
|
410
|
+
socket.addEventListener("close", () => {
|
|
411
|
+
if (this.pendingSocket === socket) {
|
|
412
|
+
this.pendingSocket = null;
|
|
413
|
+
this.scheduleReconnect();
|
|
414
|
+
}
|
|
415
|
+
else if (this.ws === socket) {
|
|
416
|
+
this.ws = null;
|
|
383
417
|
this.scheduleReconnect();
|
|
384
418
|
}
|
|
385
419
|
});
|
|
386
|
-
|
|
420
|
+
socket.addEventListener("error", () => {
|
|
387
421
|
/* close fires after error — handled there */
|
|
388
422
|
});
|
|
389
423
|
}
|
|
390
424
|
catch (error) {
|
|
391
|
-
if (!isAbortError(error)) {
|
|
392
|
-
this.config.onError?.(error);
|
|
425
|
+
if (!this.shuttingDown && !isAbortError(error)) {
|
|
426
|
+
this.config.onError?.(error, "connection");
|
|
393
427
|
}
|
|
428
|
+
shouldReconnect = !this.shuttingDown;
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
this.connecting = false;
|
|
432
|
+
}
|
|
433
|
+
if (shouldReconnect) {
|
|
394
434
|
this.scheduleReconnect();
|
|
395
435
|
}
|
|
396
436
|
}
|
|
397
|
-
async handleFrame(raw) {
|
|
437
|
+
async handleFrame(socket, raw) {
|
|
398
438
|
if (this.shuttingDown)
|
|
399
439
|
return;
|
|
400
440
|
const envelope = parseSocketFrame(raw);
|
|
401
441
|
if (!envelope)
|
|
402
442
|
return;
|
|
403
|
-
if (envelope.envelopeId) {
|
|
404
|
-
|
|
443
|
+
if (envelope.envelopeId && socket.readyState === WebSocket.OPEN) {
|
|
444
|
+
socket.send(JSON.stringify({ envelope_id: envelope.envelopeId }));
|
|
405
445
|
}
|
|
406
446
|
const dedupKey = envelope.dedupKey ?? null;
|
|
407
447
|
try {
|
|
@@ -412,7 +452,9 @@ export class SlackSocketModeClient {
|
|
|
412
452
|
this.config.dedup?.add(dedupKey);
|
|
413
453
|
}
|
|
414
454
|
if (envelope.type === "disconnect") {
|
|
415
|
-
this.
|
|
455
|
+
if (this.ws === socket) {
|
|
456
|
+
this.scheduleReconnect();
|
|
457
|
+
}
|
|
416
458
|
return;
|
|
417
459
|
}
|
|
418
460
|
if (envelope.interactivePayload) {
|
|
@@ -480,7 +522,7 @@ export class SlackSocketModeClient {
|
|
|
480
522
|
}
|
|
481
523
|
}
|
|
482
524
|
scheduleReconnect() {
|
|
483
|
-
if (this.shuttingDown || this.reconnectTimer)
|
|
525
|
+
if (this.shuttingDown || this.connecting || this.pendingSocket || this.reconnectTimer)
|
|
484
526
|
return;
|
|
485
527
|
this.config.onReconnectScheduled?.();
|
|
486
528
|
this.reconnectTimer = setTimeout(() => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DecodedSlackModalPrivateMetadata } from "./slack-modals.js";
|
|
1
2
|
export type SlackBlock = Record<string, unknown>;
|
|
2
3
|
export interface SlackNormalizedBlockAction {
|
|
3
4
|
actionId: string;
|
|
@@ -10,13 +11,54 @@ export interface SlackNormalizedBlockAction {
|
|
|
10
11
|
parsedValue?: unknown;
|
|
11
12
|
actionTs?: string;
|
|
12
13
|
}
|
|
14
|
+
export interface SlackSanitizedBlockAction extends Record<string, unknown> {
|
|
15
|
+
actionId: string;
|
|
16
|
+
blockId: string | null;
|
|
17
|
+
text: string | null;
|
|
18
|
+
type: string | null;
|
|
19
|
+
style: string | null;
|
|
20
|
+
value: string | null;
|
|
21
|
+
parsedValue: SlackNormalizedBlockAction["parsedValue"];
|
|
22
|
+
actionTs: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface SlackBlockActionMetadata extends Record<string, unknown> {
|
|
25
|
+
kind: "slack_block_action";
|
|
26
|
+
triggerId: string | null;
|
|
27
|
+
viewId: string | null;
|
|
28
|
+
callbackId: string | null;
|
|
29
|
+
viewHash: string | null;
|
|
30
|
+
blockId: string | null;
|
|
31
|
+
actionId: string;
|
|
32
|
+
value: string | null;
|
|
33
|
+
parsedValue: SlackNormalizedBlockAction["parsedValue"];
|
|
34
|
+
actionText: string | null;
|
|
35
|
+
channel: string;
|
|
36
|
+
threadTs: string;
|
|
37
|
+
messageTs: string | null;
|
|
38
|
+
modalPrivateMetadata: DecodedSlackModalPrivateMetadata["value"];
|
|
39
|
+
actions: SlackSanitizedBlockAction[];
|
|
40
|
+
}
|
|
41
|
+
export type SlackViewSubmissionStateValue = SlackBlock;
|
|
42
|
+
export type SlackViewSubmissionStateValues = Record<string, Record<string, SlackViewSubmissionStateValue>>;
|
|
43
|
+
export interface SlackViewSubmissionMetadata extends Record<string, unknown> {
|
|
44
|
+
kind: "slack_view_submission";
|
|
45
|
+
triggerId: string | null;
|
|
46
|
+
callbackId: string | null;
|
|
47
|
+
viewId: string;
|
|
48
|
+
externalId: string | null;
|
|
49
|
+
viewHash: string | null;
|
|
50
|
+
channel: string;
|
|
51
|
+
threadTs: string;
|
|
52
|
+
privateMetadata: DecodedSlackModalPrivateMetadata["value"];
|
|
53
|
+
stateValues: SlackViewSubmissionStateValues;
|
|
54
|
+
}
|
|
13
55
|
export interface SlackBlockActionInboxEvent {
|
|
14
56
|
channel: string;
|
|
15
57
|
threadTs: string;
|
|
16
58
|
userId: string;
|
|
17
59
|
text: string;
|
|
18
60
|
timestamp: string;
|
|
19
|
-
metadata:
|
|
61
|
+
metadata: SlackBlockActionMetadata;
|
|
20
62
|
}
|
|
21
63
|
export interface SlackViewSubmissionInboxEvent {
|
|
22
64
|
channel: string;
|
|
@@ -24,7 +66,7 @@ export interface SlackViewSubmissionInboxEvent {
|
|
|
24
66
|
userId: string;
|
|
25
67
|
text: string;
|
|
26
68
|
timestamp: string;
|
|
27
|
-
metadata:
|
|
69
|
+
metadata: SlackViewSubmissionMetadata;
|
|
28
70
|
}
|
|
29
71
|
export type SlackInteractiveInboxEvent = SlackBlockActionInboxEvent | SlackViewSubmissionInboxEvent;
|
|
30
72
|
export declare function normalizeSlackBlocksInput(blocks: unknown): SlackBlock[];
|
package/dist/slack-block-kit.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { decodeSlackModalPrivateMetadata } from "./slack-modals.js";
|
|
2
|
-
function
|
|
1
|
+
import { decodeSlackModalPrivateMetadata, } from "./slack-modals.js";
|
|
2
|
+
function isSlackBlockKitObject(value) {
|
|
3
3
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
4
|
}
|
|
5
5
|
function asString(value) {
|
|
6
6
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
7
7
|
}
|
|
8
8
|
function asRecord(value) {
|
|
9
|
-
return
|
|
9
|
+
return isSlackBlockKitObject(value) ? value : undefined;
|
|
10
10
|
}
|
|
11
11
|
function asRecordArray(value) {
|
|
12
|
-
if (!Array.isArray(value) || value.some((item) => !
|
|
12
|
+
if (!Array.isArray(value) || value.some((item) => !isSlackBlockKitObject(item))) {
|
|
13
13
|
throw new Error("Slack blocks must be a JSON array of objects.");
|
|
14
14
|
}
|
|
15
15
|
return value;
|
|
@@ -122,7 +122,7 @@ export function extractSlackInteractivePayloadFromEnvelope(envelope) {
|
|
|
122
122
|
return null;
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
if (!
|
|
125
|
+
if (!isSlackBlockKitObject(payload))
|
|
126
126
|
return null;
|
|
127
127
|
return payload;
|
|
128
128
|
}
|
|
@@ -19,8 +19,18 @@ export interface SlackFileLookupContext {
|
|
|
19
19
|
threadTs?: string;
|
|
20
20
|
messageTs?: string;
|
|
21
21
|
}
|
|
22
|
+
type SlackFileObject = Record<string, unknown>;
|
|
23
|
+
export interface SlackThreadRepliesRequest extends SlackFileObject {
|
|
24
|
+
channel: string;
|
|
25
|
+
ts: string;
|
|
26
|
+
limit: number;
|
|
27
|
+
}
|
|
28
|
+
export interface SlackFilesInfoRequest extends SlackFileObject {
|
|
29
|
+
file: string;
|
|
30
|
+
}
|
|
31
|
+
export type SlackFileLookupRequest = SlackThreadRepliesRequest | SlackFilesInfoRequest;
|
|
22
32
|
export interface SlackFileDownloadDeps {
|
|
23
|
-
slack: (method: string, token: string, body?:
|
|
33
|
+
slack: (method: string, token: string, body?: SlackFileLookupRequest) => Promise<SlackResult>;
|
|
24
34
|
token: string;
|
|
25
35
|
fetchImpl?: (url: string, init: RequestInit) => Promise<Pick<Response, "ok" | "status" | "statusText" | "arrayBuffer" | "text"> & {
|
|
26
36
|
body?: Response["body"];
|
|
@@ -32,3 +42,4 @@ export interface SlackFileDownloadDeps {
|
|
|
32
42
|
}
|
|
33
43
|
export declare function fetchSlackFileToCache(fileId: string, context: SlackFileLookupContext, deps: SlackFileDownloadDeps): Promise<SlackFileDescriptor>;
|
|
34
44
|
export declare function readCachedSlackFile(pathname: string): Promise<Buffer>;
|
|
45
|
+
export {};
|
|
@@ -10,7 +10,7 @@ const SLACK_FILE_DOWNLOAD_HOSTS = new Set([
|
|
|
10
10
|
"files.slack-edge.com",
|
|
11
11
|
"downloads.slack-edge.com",
|
|
12
12
|
]);
|
|
13
|
-
function
|
|
13
|
+
function isSlackFileObject(value) {
|
|
14
14
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
15
|
}
|
|
16
16
|
function getString(record, key) {
|
|
@@ -44,7 +44,7 @@ function assertSafeSlackFileDownloadUrl(rawUrl) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
function metadataFromSlackFile(value, expectedFileId) {
|
|
47
|
-
if (!
|
|
47
|
+
if (!isSlackFileObject(value))
|
|
48
48
|
return null;
|
|
49
49
|
const id = getString(value, "id");
|
|
50
50
|
if (id !== expectedFileId)
|
|
@@ -65,7 +65,7 @@ function metadataFromSlackFile(value, expectedFileId) {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
function filesFromMessage(message) {
|
|
68
|
-
if (!
|
|
68
|
+
if (!isSlackFileObject(message) || !Array.isArray(message.files))
|
|
69
69
|
return [];
|
|
70
70
|
return message.files;
|
|
71
71
|
}
|
|
@@ -73,7 +73,7 @@ function findFileInMessages(messages, fileId, messageTs) {
|
|
|
73
73
|
if (!Array.isArray(messages))
|
|
74
74
|
return null;
|
|
75
75
|
for (const message of messages) {
|
|
76
|
-
if (messageTs && (!
|
|
76
|
+
if (messageTs && (!isSlackFileObject(message) || getString(message, "ts") !== messageTs))
|
|
77
77
|
continue;
|
|
78
78
|
for (const file of filesFromMessage(message)) {
|
|
79
79
|
const metadata = metadataFromSlackFile(file, fileId);
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
type SlackMessageContextObject = Record<string, unknown>;
|
|
2
|
+
export interface SlackMessageContextEvent extends SlackMessageContextObject {
|
|
3
|
+
blocks?: unknown;
|
|
4
|
+
attachments?: unknown;
|
|
5
|
+
files?: unknown;
|
|
6
|
+
}
|
|
1
7
|
export interface SlackMessageFileMetadata {
|
|
2
8
|
id?: string;
|
|
3
9
|
name?: string;
|
|
@@ -10,5 +16,6 @@ export interface SlackMessageFileMetadata {
|
|
|
10
16
|
size?: number;
|
|
11
17
|
}
|
|
12
18
|
export declare function extractSlackMessageFileMetadata(files: unknown): SlackMessageFileMetadata[];
|
|
13
|
-
export declare function extractSlackMessageContextLines(evt:
|
|
14
|
-
export declare function buildSlackInboundMessageText(baseText: string, evt:
|
|
19
|
+
export declare function extractSlackMessageContextLines(evt: SlackMessageContextEvent, baseText?: string): string[];
|
|
20
|
+
export declare function buildSlackInboundMessageText(baseText: string, evt: SlackMessageContextEvent): string;
|
|
21
|
+
export {};
|
package/dist/slack-modals.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type SlackModalObject = Record<string, unknown>;
|
|
2
|
+
export interface SlackModalView extends SlackModalObject {
|
|
3
|
+
type: "modal";
|
|
4
|
+
}
|
|
2
5
|
export declare function normalizeSlackModalViewInput(view: unknown): SlackModalView;
|
|
3
6
|
export interface SlackModalThreadContext {
|
|
4
7
|
threadTs: string;
|
package/dist/slack-modals.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const PI_SLACK_MODAL_CONTEXT_KEY = "__piSlackModalContext";
|
|
2
|
-
function
|
|
2
|
+
function isSlackModalObject(value) {
|
|
3
3
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
4
|
}
|
|
5
5
|
function asString(value) {
|
|
@@ -9,7 +9,7 @@ function asOptionalString(value) {
|
|
|
9
9
|
return typeof value === "string" ? value : null;
|
|
10
10
|
}
|
|
11
11
|
export function normalizeSlackModalViewInput(view) {
|
|
12
|
-
if (!
|
|
12
|
+
if (!isSlackModalObject(view)) {
|
|
13
13
|
throw new Error("Slack modal view must be a JSON object.");
|
|
14
14
|
}
|
|
15
15
|
const cloned = structuredClone(view);
|
|
@@ -32,7 +32,7 @@ export function encodeSlackModalPrivateMetadata(privateMetadata, threadContext)
|
|
|
32
32
|
value = raw;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
if (
|
|
35
|
+
if (isSlackModalObject(value)) {
|
|
36
36
|
return JSON.stringify({
|
|
37
37
|
...value,
|
|
38
38
|
[PI_SLACK_MODAL_CONTEXT_KEY]: threadContext,
|
|
@@ -50,9 +50,9 @@ export function decodeSlackModalPrivateMetadata(privateMetadata) {
|
|
|
50
50
|
}
|
|
51
51
|
try {
|
|
52
52
|
const parsed = JSON.parse(raw);
|
|
53
|
-
if (
|
|
53
|
+
if (isSlackModalObject(parsed)) {
|
|
54
54
|
const context = parsed[PI_SLACK_MODAL_CONTEXT_KEY];
|
|
55
|
-
const threadContext =
|
|
55
|
+
const threadContext = isSlackModalObject(context)
|
|
56
56
|
? {
|
|
57
57
|
threadTs: asString(context.threadTs) ?? "",
|
|
58
58
|
channel: asString(context.channel) ?? "",
|
|
@@ -11,6 +11,7 @@ export interface SlackPinetRuntimeAdapterDeps {
|
|
|
11
11
|
getAppToken: () => string;
|
|
12
12
|
getAllowedUsers: () => Set<string> | null;
|
|
13
13
|
shouldAllowAllWorkspaceUsers: () => boolean;
|
|
14
|
+
setExtStatus: (ctx: ExtensionContext, state: "ok" | "reconnecting" | "error") => void;
|
|
14
15
|
onAppHomeOpened: (userId: string, ctx: ExtensionContext) => Promise<void> | void;
|
|
15
16
|
onSlashCommand?: (event: ParsedSlashCommand, ctx: ExtensionContext) => Promise<string | null> | string | null;
|
|
16
17
|
}
|