@pinet/slack-bridge 0.1.2 → 0.2.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/README.md +59 -32
- package/dist/broker/adapters/slack.d.ts +19 -1
- package/dist/broker/adapters/slack.js +111 -22
- package/dist/broker/client.d.ts +2 -1
- package/dist/broker/client.js +1 -0
- package/dist/broker/socket-server.js +18 -0
- package/dist/deploy-manifest.d.ts +5 -0
- package/dist/deploy-manifest.js +30 -1
- package/dist/follower-runtime.js +5 -1
- package/dist/helpers.d.ts +14 -0
- package/dist/helpers.js +45 -21
- package/dist/index.js +60 -0
- package/dist/pinet-commands.d.ts +6 -1
- package/dist/pinet-commands.js +166 -1
- package/dist/pinet-mesh-ops.d.ts +11 -0
- package/dist/pinet-mesh-ops.js +17 -0
- package/dist/pinet-tools.d.ts +47 -0
- package/dist/pinet-tools.js +506 -38
- package/dist/prompts/broker/tmux.md +2 -2
- package/dist/reaction-triggers.d.ts +1 -0
- package/dist/reaction-triggers.js +26 -15
- package/dist/runtime-agent-context.js +19 -0
- package/dist/runtime-mode.js +7 -1
- package/dist/single-player-runtime.js +22 -26
- package/dist/slack-access.d.ts +11 -0
- package/dist/slack-access.js +30 -0
- package/dist/slack-agents-command.d.ts +19 -0
- package/dist/slack-agents-command.js +90 -0
- package/dist/slack-export.d.ts +1 -1
- package/dist/slack-export.js +6 -4
- package/dist/slack-file-access.d.ts +34 -0
- package/dist/slack-file-access.js +209 -0
- package/dist/slack-message-context.d.ts +0 -1
- package/dist/slack-message-context.js +1 -6
- package/dist/slack-pinet-runtime-adapter.d.ts +4 -2
- package/dist/slack-pinet-runtime-adapter.js +12 -0
- package/dist/slack-tools.d.ts +6 -0
- package/dist/slack-tools.js +290 -36
- package/dist/slack-upload.d.ts +13 -1
- package/dist/slack-upload.js +29 -2
- package/dist/stale-slack-messages.d.ts +12 -0
- package/dist/stale-slack-messages.js +29 -0
- package/dist/subtree-broker-runtime.d.ts +109 -0
- package/dist/subtree-broker-runtime.js +558 -0
- package/manifest.yaml +9 -0
- package/package.json +9 -7
- package/skills/slack-bridge/SKILL.md +60 -1
|
@@ -24,7 +24,7 @@ FRESH TMUX WORKERS: On the Mac mini, launch a fresh tmux-backed Pinet follower w
|
|
|
24
24
|
|
|
25
25
|
WORKER GRACE PERIOD: After a worker reports completion, keep the same Pi instance available for a one-hour grace period before spinning it down. During that window, route follow-up, review-fix, or clarification work back to the same instance whenever possible so context and thread ownership remain stable. Do not assign unrelated new work to a grace-reserved worker unless the maintainer explicitly prioritizes reuse over follow-up stability. After the grace period, ask an idle, healthy, broker-managed worker to confirm it is free, then ask it to `/pinet exit` or use `pinet action=exit` only when available Pinet agent/lane/status signals and/or the worker response show no pending inbox/thread ownership and no active lane assignment. If any required signal is unavailable or ambiguous, fail closed: report the worker as a cleanup candidate instead of exiting or closing tmux. After the worker exits, close only the broker-managed tmux session recorded for that lane when it is still present. If it does not exit cleanly, let conservative broker-managed ghost reaping handle the verified process rather than killing unmarked local PIDs.
|
|
26
26
|
|
|
27
|
-
WORKER CAP AND TMUX HYGIENE: Keep broker-managed tmux capacity bounded by pruning, not by recycling old context into new lanes. Start at most one fresh worker per new lane unless the maintainer asks for fan-out, and flag multiple idle broker-managed workers past their grace windows as cleanup candidates. Broker-created tmux session names must be lane/repo-scoped and recorded in lane metadata; do not leave anonymous private-socket sessions that operators cannot map back to a lane.
|
|
27
|
+
WORKER CAP AND TMUX HYGIENE: Keep broker-managed tmux capacity bounded by pruning, not by recycling old context into new lanes. Start at most one fresh worker per new lane unless the maintainer asks for fan-out, and flag multiple idle broker-managed workers past their grace windows as cleanup candidates. If multiple worker identities report the same broker-managed tmux session, repo/worktree, or lane, treat it as an immediate duplicate-owner anomaly: stop routing new work to the duplicates, choose the canonical live worker from the tmux pane/current PID and lane metadata, ask non-canonical idle workers to `/pinet exit` once they have no pending inbox/thread ownership/active lane, and report ambiguous or private-socket cases instead of guessing. Broker-created tmux session names must be lane/repo-scoped and recorded in lane metadata; do not leave anonymous private-socket sessions that operators cannot map back to a lane.
|
|
28
28
|
|
|
29
29
|
THREAD OWNERSHIP AND REPORTING: Slack thread ownership is first-responder-wins. If direct Slack posting is blocked because another agent owns the thread, report through the Pinet/broker channel for relay instead of trying alternate Slack channel posts. When delegating, tell workers exactly where to report progress and final outcomes; if a worker cannot post to Slack, require it to report back to the broker for relay.
|
|
30
30
|
|
|
@@ -46,4 +46,4 @@ Clean up worktrees after PRs merge: `git worktree remove .worktrees/<name>`. Fla
|
|
|
46
46
|
|
|
47
47
|
GITHUB AND SECRET HANDLING: Use the repo's documented GitHub authentication path when filing issues, creating PRs, or merging. Never echo tokens, mesh secrets, private prompt contents, local absolute secret paths, or credential material into Slack, Pinet, issues, PRs, logs, or diagnostics. Prefer high-level auth/secret status such as "auth unavailable" or "secret configured" without revealing values.
|
|
48
48
|
|
|
49
|
-
RALPH LOOP: Run autonomous maintenance every cycle unless RALPH snooze is active for empty/no-work cycles. Don't wait to be asked when work, anomalies, blockers, or human-triggered messages appear; snooze must not hide active work. Proactively: (1) REAP — ping idle agents, keep completed workers in their one-hour grace window, then ask grace-expired healthy broker-managed workers to exit only after inspectable Pinet signals or the worker's own response confirm they have no pending inbox/thread ownership or active lane assignment; report ambiguous cases as cleanup candidates instead of guessing. (2) NUDGE — check assigned work, poll branches for commits, escalate stalled agents. (3) REASSIGN — if an assigned lane worker is dead, start a fresh repo-scoped tmux worker for that lane unless the maintainer explicitly asked to reuse another worker. (4) DRAIN — assign queued tasks to fresh lane-scoped tmux workers; do not recycle unrelated idle workers as a shortcut. (5) TMUX HYGIENE — inspect lane metadata for broker-managed tmux sessions, close sessions whose worker exited after grace, and report orphaned/private sessions that cannot be mapped safely instead of killing blindly. (6) SNOOZE — after repeated empty cycles, use `/pinet snooze` or `pinet action=snooze` only for non-urgent quiet time; wake/route normally when new work or anomalies appear. (7) SELF-REPAIR — verify main is on `main`, check mesh health, report anomalies.
|
|
49
|
+
RALPH LOOP: Run autonomous maintenance every cycle unless RALPH snooze is active for empty/no-work cycles. Don't wait to be asked when work, anomalies, blockers, or human-triggered messages appear; snooze must not hide active work. Proactively: (1) REAP — ping idle agents, keep completed workers in their one-hour grace window, then ask grace-expired healthy broker-managed workers to exit only after inspectable Pinet signals or the worker's own response confirm they have no pending inbox/thread ownership or active lane assignment; report ambiguous cases as cleanup candidates instead of guessing. (2) NUDGE — check assigned work, poll branches for commits, escalate stalled agents. (3) REASSIGN — if an assigned lane worker is dead, start a fresh repo-scoped tmux worker for that lane unless the maintainer explicitly asked to reuse another worker. (4) DRAIN — assign queued tasks to fresh lane-scoped tmux workers; do not recycle unrelated idle workers as a shortcut. (5) TMUX HYGIENE — inspect lane metadata for broker-managed tmux sessions every cycle; resolve duplicate owners immediately, close sessions whose worker exited after grace, and report orphaned/private sessions that cannot be mapped safely instead of killing blindly. (6) SNOOZE — after repeated empty cycles, use `/pinet snooze` or `pinet action=snooze` only for non-urgent quiet time; wake/route normally when new work or anomalies appear. (7) SELF-REPAIR — verify main is on `main`, check mesh health, report anomalies.
|
|
@@ -7,6 +7,7 @@ export type ReactionCommandSetting = string | {
|
|
|
7
7
|
prompt?: string;
|
|
8
8
|
};
|
|
9
9
|
export type ReactionCommandSettings = Record<string, ReactionCommandSetting>;
|
|
10
|
+
export declare const REACTION_COMMAND_PRESETS: Record<string, ReactionCommandTemplate>;
|
|
10
11
|
export declare const DEFAULT_REACTION_COMMANDS: Record<string, ReactionCommandTemplate>;
|
|
11
12
|
export declare function normalizeReactionName(input: string): string;
|
|
12
13
|
export declare function resolveReactionCommands(settings: ReactionCommandSettings | undefined): Map<string, ReactionCommandTemplate>;
|
|
@@ -35,7 +35,7 @@ const REACTION_DISPLAY = {
|
|
|
35
35
|
arrow_up: "⬆️",
|
|
36
36
|
octagonal_sign: "🛑",
|
|
37
37
|
};
|
|
38
|
-
export const
|
|
38
|
+
export const REACTION_COMMAND_PRESETS = {
|
|
39
39
|
memo: {
|
|
40
40
|
action: "summarize",
|
|
41
41
|
prompt: "Summarize the reacted-to message and the surrounding thread. Focus on key decisions, action items, and open questions.",
|
|
@@ -77,6 +77,10 @@ export const DEFAULT_REACTION_COMMANDS = {
|
|
|
77
77
|
prompt: "If the reacted message contains a URL, fetch the linked page and summarize the important information for the user.",
|
|
78
78
|
},
|
|
79
79
|
};
|
|
80
|
+
// Slack emoji reactions are deliberately no-op by default. Operators must opt in
|
|
81
|
+
// per reaction through settings.reactionCommands before any reaction can enqueue
|
|
82
|
+
// Pinet work, steering, reviews, or interrupt controls.
|
|
83
|
+
export const DEFAULT_REACTION_COMMANDS = {};
|
|
80
84
|
function normalizeReactionNameOrNull(input) {
|
|
81
85
|
const trimmed = input.trim();
|
|
82
86
|
if (!trimmed)
|
|
@@ -96,25 +100,25 @@ export function normalizeReactionName(input) {
|
|
|
96
100
|
function buildDefaultPromptForAction(action) {
|
|
97
101
|
switch (action) {
|
|
98
102
|
case "summarize":
|
|
99
|
-
return
|
|
103
|
+
return REACTION_COMMAND_PRESETS.memo.prompt;
|
|
100
104
|
case "file-issue":
|
|
101
|
-
return
|
|
105
|
+
return REACTION_COMMAND_PRESETS.bug.prompt;
|
|
102
106
|
case "review":
|
|
103
|
-
return
|
|
107
|
+
return REACTION_COMMAND_PRESETS.eyes.prompt;
|
|
104
108
|
case "approve":
|
|
105
|
-
return
|
|
109
|
+
return REACTION_COMMAND_PRESETS.white_check_mark.prompt;
|
|
106
110
|
case "retry":
|
|
107
|
-
return
|
|
111
|
+
return REACTION_COMMAND_PRESETS.repeat.prompt;
|
|
108
112
|
case "steer":
|
|
109
|
-
return
|
|
113
|
+
return REACTION_COMMAND_PRESETS.arrow_up.prompt;
|
|
110
114
|
case "interrupt":
|
|
111
|
-
return
|
|
115
|
+
return REACTION_COMMAND_PRESETS.octagonal_sign.prompt;
|
|
112
116
|
case "search":
|
|
113
|
-
return
|
|
117
|
+
return REACTION_COMMAND_PRESETS.mag.prompt;
|
|
114
118
|
case "track":
|
|
115
|
-
return
|
|
119
|
+
return REACTION_COMMAND_PRESETS.pushpin.prompt;
|
|
116
120
|
case "fetch-url":
|
|
117
|
-
return
|
|
121
|
+
return REACTION_COMMAND_PRESETS.globe_with_meridians.prompt;
|
|
118
122
|
default:
|
|
119
123
|
return `Carry out the "${action}" action for the reacted Slack message or thread, using the included context before you decide what to do.`;
|
|
120
124
|
}
|
|
@@ -137,8 +141,15 @@ export function resolveReactionCommands(settings) {
|
|
|
137
141
|
}
|
|
138
142
|
if (!config || typeof config !== "object")
|
|
139
143
|
continue;
|
|
140
|
-
const
|
|
141
|
-
const
|
|
144
|
+
const preset = REACTION_COMMAND_PRESETS[normalizedReaction];
|
|
145
|
+
const configuredAction = config.action?.trim();
|
|
146
|
+
const action = configuredAction ||
|
|
147
|
+
resolved.get(normalizedReaction)?.action ||
|
|
148
|
+
preset?.action ||
|
|
149
|
+
normalizedReaction;
|
|
150
|
+
const prompt = config.prompt?.trim() ||
|
|
151
|
+
(configuredAction ? buildDefaultPromptForAction(action) : preset?.prompt) ||
|
|
152
|
+
buildDefaultPromptForAction(action);
|
|
142
153
|
resolved.set(normalizedReaction, { action, prompt });
|
|
143
154
|
}
|
|
144
155
|
return resolved;
|
|
@@ -165,7 +176,7 @@ export function buildReactionTriggerMessage(input) {
|
|
|
165
176
|
}
|
|
166
177
|
export function buildReactionPromptGuidelines() {
|
|
167
178
|
return [
|
|
168
|
-
"
|
|
169
|
-
"
|
|
179
|
+
"Slack emoji reactions are ignored by default and should not be treated as work unless the extension has already delivered an explicit structured 'Reaction trigger from Slack:' inbox message from an authorized Pinet thread.",
|
|
180
|
+
"If such an opt-in reaction-triggered request appears, treat it as an explicit user instruction tied to the referenced Slack message or thread; never infer work from a plain emoji reaction alone or from reactions in ordinary uninvoked Slack threads.",
|
|
170
181
|
];
|
|
171
182
|
}
|
|
@@ -214,6 +214,7 @@ export function createRuntimeAgentContext(deps) {
|
|
|
214
214
|
];
|
|
215
215
|
const skinAssignment = resolveSkinAssignment(role, getIdentitySeedForRole(role));
|
|
216
216
|
const brokerManaged = role === "worker" && process.env.PINET_BROKER_MANAGED === "1";
|
|
217
|
+
const parentAgentId = process.env.PINET_PARENT_AGENT_ID?.trim() || undefined;
|
|
217
218
|
const brokerManagedMetadata = brokerManaged
|
|
218
219
|
? {
|
|
219
220
|
brokerManaged: true,
|
|
@@ -221,6 +222,24 @@ export function createRuntimeAgentContext(deps) {
|
|
|
221
222
|
launchSource: process.env.PINET_LAUNCH_SOURCE?.trim() || "broker-tmux",
|
|
222
223
|
tmuxSession: process.env.PINET_TMUX_SESSION?.trim() || undefined,
|
|
223
224
|
brokerManagedAt: new Date().toISOString(),
|
|
225
|
+
...(parentAgentId ? { parentAgentId, pinetParentAgentId: parentAgentId } : {}),
|
|
226
|
+
...(process.env.PINET_ROOT_AGENT_ID?.trim()
|
|
227
|
+
? { rootAgentId: process.env.PINET_ROOT_AGENT_ID.trim() }
|
|
228
|
+
: {}),
|
|
229
|
+
...(process.env.PINET_SPAWNED_BY_AGENT_ID?.trim()
|
|
230
|
+
? { spawnedByAgentId: process.env.PINET_SPAWNED_BY_AGENT_ID.trim() }
|
|
231
|
+
: parentAgentId
|
|
232
|
+
? { spawnedByAgentId: parentAgentId }
|
|
233
|
+
: {}),
|
|
234
|
+
...(process.env.PINET_LAUNCH_ID?.trim()
|
|
235
|
+
? { launchId: process.env.PINET_LAUNCH_ID.trim() }
|
|
236
|
+
: {}),
|
|
237
|
+
...(process.env.PINET_SUBTREE_ROLE?.trim()
|
|
238
|
+
? { subtreeRole: process.env.PINET_SUBTREE_ROLE.trim() }
|
|
239
|
+
: {}),
|
|
240
|
+
...(process.env.PINET_LANE_ID?.trim()
|
|
241
|
+
? { laneId: process.env.PINET_LANE_ID.trim() }
|
|
242
|
+
: {}),
|
|
224
243
|
}
|
|
225
244
|
: {};
|
|
226
245
|
return {
|
package/dist/runtime-mode.js
CHANGED
|
@@ -12,7 +12,13 @@ export function isPinetRuntimeMode(mode) {
|
|
|
12
12
|
return mode === "broker" || mode === "follower";
|
|
13
13
|
}
|
|
14
14
|
export function isBrokerManagedFollowerLaunch(env = process.env) {
|
|
15
|
-
|
|
15
|
+
if (env.PINET_BROKER_MANAGED !== "1")
|
|
16
|
+
return false;
|
|
17
|
+
if (env.PINET_LAUNCH_SOURCE === "broker-tmux")
|
|
18
|
+
return true;
|
|
19
|
+
if (env.PINET_LAUNCH_SOURCE === "subtree-broker-tmux")
|
|
20
|
+
return true;
|
|
21
|
+
return Boolean(env.PINET_SOCKET_PATH?.trim());
|
|
16
22
|
}
|
|
17
23
|
export function resolveSlackBridgeStartupRuntimeMode(settings, options = {}) {
|
|
18
24
|
const explicitMode = normalizeSlackBridgeRuntimeMode(settings.runtimeMode);
|
|
@@ -166,23 +166,13 @@ export function createSinglePlayerRuntime(deps) {
|
|
|
166
166
|
try {
|
|
167
167
|
const isInterruptReaction = command.action === "interrupt";
|
|
168
168
|
const reactedMessage = await deps.fetchSlackMessageByTs(item.channel, item.ts);
|
|
169
|
-
if (!reactedMessage) {
|
|
170
|
-
const reason = isInterruptReaction
|
|
171
|
-
? "identify Slack thread for interrupt reaction"
|
|
172
|
-
: "fetch reacted message";
|
|
173
|
-
throw new Error(`Unable to ${reason} ${item.ts} in channel ${item.channel}`);
|
|
174
|
-
}
|
|
175
169
|
const threadTs = reactedMessage?.thread_ts ??
|
|
176
170
|
reactedMessage?.ts ??
|
|
177
171
|
item.ts;
|
|
178
172
|
const threads = deps.getThreads();
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
threadTs,
|
|
183
|
-
userId: reactedMessage?.user ?? user,
|
|
184
|
-
source: "slack",
|
|
185
|
-
});
|
|
173
|
+
const threadInfo = threads.get(threadTs);
|
|
174
|
+
if (!threadInfo || threadInfo.channelId !== item.channel) {
|
|
175
|
+
return;
|
|
186
176
|
}
|
|
187
177
|
const ownership = await ensureLocalThreadOwnership(item.channel, threadTs);
|
|
188
178
|
if (ownership !== "continue") {
|
|
@@ -209,7 +199,9 @@ export function createSinglePlayerRuntime(deps) {
|
|
|
209
199
|
return;
|
|
210
200
|
const reactedMessageText = typeof reactedMessage?.text === "string" && reactedMessage.text.trim().length > 0
|
|
211
201
|
? reactedMessage.text
|
|
212
|
-
:
|
|
202
|
+
: reactedMessage
|
|
203
|
+
? "(no text)"
|
|
204
|
+
: "(message text unavailable; Slack did not return the reacted message, so use the channel/thread/message ids for context)";
|
|
213
205
|
const reactionMessage = buildReactionTriggerMessage({
|
|
214
206
|
reactionName,
|
|
215
207
|
command,
|
|
@@ -221,7 +213,6 @@ export function createSinglePlayerRuntime(deps) {
|
|
|
221
213
|
reactedMessageAuthor,
|
|
222
214
|
});
|
|
223
215
|
ctx.ui.notify(`${reactorName} reacted with :${reactionName}:`, "info");
|
|
224
|
-
const threadInfo = threads.get(threadTs);
|
|
225
216
|
deps.pushInboxMessage({
|
|
226
217
|
channel: item.channel,
|
|
227
218
|
threadTs,
|
|
@@ -251,9 +242,6 @@ export function createSinglePlayerRuntime(deps) {
|
|
|
251
242
|
if (!classified.relevant)
|
|
252
243
|
return;
|
|
253
244
|
const { threadTs, channel, userId, text, isDM, isChannelMention, messageTs, metadata } = classified;
|
|
254
|
-
if (!threads.has(threadTs)) {
|
|
255
|
-
threads.set(threadTs, { channelId: channel, threadTs, userId, source: "slack" });
|
|
256
|
-
}
|
|
257
245
|
const ownership = await ensureLocalThreadOwnership(channel, threadTs);
|
|
258
246
|
if (ownership !== "continue") {
|
|
259
247
|
return;
|
|
@@ -266,6 +254,12 @@ export function createSinglePlayerRuntime(deps) {
|
|
|
266
254
|
});
|
|
267
255
|
return;
|
|
268
256
|
}
|
|
257
|
+
// Track the thread only after the allowlist gate so unauthorized traffic
|
|
258
|
+
// cannot mint known-thread state that later admits replies or opt-in
|
|
259
|
+
// reaction commands (#812).
|
|
260
|
+
if (!threads.has(threadTs)) {
|
|
261
|
+
threads.set(threadTs, { channelId: channel, threadTs, userId, source: "slack" });
|
|
262
|
+
}
|
|
269
263
|
if (isDM) {
|
|
270
264
|
deps.setLastDmChannel(channel);
|
|
271
265
|
}
|
|
@@ -304,14 +298,6 @@ export function createSinglePlayerRuntime(deps) {
|
|
|
304
298
|
}
|
|
305
299
|
async function queueInteractiveInboxEvent(normalized, ctx) {
|
|
306
300
|
const threads = deps.getThreads();
|
|
307
|
-
if (!threads.has(normalized.threadTs)) {
|
|
308
|
-
threads.set(normalized.threadTs, {
|
|
309
|
-
channelId: normalized.channel,
|
|
310
|
-
threadTs: normalized.threadTs,
|
|
311
|
-
userId: normalized.userId,
|
|
312
|
-
source: "slack",
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
301
|
const ownership = await ensureLocalThreadOwnership(normalized.channel, normalized.threadTs);
|
|
316
302
|
if (ownership !== "continue") {
|
|
317
303
|
return;
|
|
@@ -324,6 +310,16 @@ export function createSinglePlayerRuntime(deps) {
|
|
|
324
310
|
});
|
|
325
311
|
return;
|
|
326
312
|
}
|
|
313
|
+
// Track the thread only after the allowlist gate so unauthorized
|
|
314
|
+
// interactive events cannot mint known-thread state (#812).
|
|
315
|
+
if (!threads.has(normalized.threadTs)) {
|
|
316
|
+
threads.set(normalized.threadTs, {
|
|
317
|
+
channelId: normalized.channel,
|
|
318
|
+
threadTs: normalized.threadTs,
|
|
319
|
+
userId: normalized.userId,
|
|
320
|
+
source: "slack",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
327
323
|
if (normalized.channel.startsWith("D")) {
|
|
328
324
|
deps.setLastDmChannel(normalized.channel);
|
|
329
325
|
}
|
package/dist/slack-access.d.ts
CHANGED
|
@@ -22,12 +22,22 @@ export declare function buildSlackThreadRuntimeScope(input: {
|
|
|
22
22
|
channelId?: string | null;
|
|
23
23
|
context?: SlackThreadContext | null;
|
|
24
24
|
}): RuntimeScopeCarrier;
|
|
25
|
+
export interface ParsedSlashCommand {
|
|
26
|
+
command: string;
|
|
27
|
+
text: string;
|
|
28
|
+
channelId: string;
|
|
29
|
+
userId: string;
|
|
30
|
+
responseUrl?: string;
|
|
31
|
+
triggerId?: string;
|
|
32
|
+
teamId?: string;
|
|
33
|
+
}
|
|
25
34
|
export interface ParsedEnvelope {
|
|
26
35
|
envelopeId?: string;
|
|
27
36
|
type: string;
|
|
28
37
|
dedupKey?: string;
|
|
29
38
|
event?: Record<string, unknown>;
|
|
30
39
|
interactivePayload?: Record<string, unknown>;
|
|
40
|
+
slashCommand?: ParsedSlashCommand;
|
|
31
41
|
}
|
|
32
42
|
export declare function isSlackUserAllowed(allowlist: Set<string> | null, userId: string): boolean;
|
|
33
43
|
/**
|
|
@@ -168,6 +178,7 @@ export interface SlackSocketModeClientConfig {
|
|
|
168
178
|
}) => Promise<void> | void;
|
|
169
179
|
onAppHomeOpened?: (event: ParsedAppHomeOpened) => Promise<void> | void;
|
|
170
180
|
onInteractive?: (event: SlackInteractiveInboxEvent) => Promise<void> | void;
|
|
181
|
+
onSlashCommand?: (event: ParsedSlashCommand) => Promise<void> | void;
|
|
171
182
|
}
|
|
172
183
|
export declare class SlackSocketModeClient {
|
|
173
184
|
private readonly config;
|
package/dist/slack-access.js
CHANGED
|
@@ -50,12 +50,38 @@ export function parseSocketFrame(raw) {
|
|
|
50
50
|
if (interactivePayload) {
|
|
51
51
|
result.interactivePayload = interactivePayload;
|
|
52
52
|
}
|
|
53
|
+
if (data.type === "slash_commands") {
|
|
54
|
+
const slashCommand = extractSlackSlashCommandPayload(data.payload);
|
|
55
|
+
if (slashCommand) {
|
|
56
|
+
result.slashCommand = slashCommand;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
53
59
|
return result;
|
|
54
60
|
}
|
|
55
61
|
catch {
|
|
56
62
|
return null;
|
|
57
63
|
}
|
|
58
64
|
}
|
|
65
|
+
function extractSlackSlashCommandPayload(payload) {
|
|
66
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload))
|
|
67
|
+
return null;
|
|
68
|
+
const record = payload;
|
|
69
|
+
const command = typeof record.command === "string" ? record.command.trim() : "";
|
|
70
|
+
const text = typeof record.text === "string" ? record.text : "";
|
|
71
|
+
const channelId = typeof record.channel_id === "string" ? record.channel_id : "";
|
|
72
|
+
const userId = typeof record.user_id === "string" ? record.user_id : "";
|
|
73
|
+
if (!command || !channelId || !userId)
|
|
74
|
+
return null;
|
|
75
|
+
return {
|
|
76
|
+
command,
|
|
77
|
+
text,
|
|
78
|
+
channelId,
|
|
79
|
+
userId,
|
|
80
|
+
...(typeof record.response_url === "string" ? { responseUrl: record.response_url } : {}),
|
|
81
|
+
...(typeof record.trigger_id === "string" ? { triggerId: record.trigger_id } : {}),
|
|
82
|
+
...(typeof record.team_id === "string" ? { teamId: record.team_id } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
59
85
|
/**
|
|
60
86
|
* Extract thread info from an assistant_thread_started event.
|
|
61
87
|
*/
|
|
@@ -402,6 +428,10 @@ export class SlackSocketModeClient {
|
|
|
402
428
|
}
|
|
403
429
|
return;
|
|
404
430
|
}
|
|
431
|
+
if (envelope.slashCommand) {
|
|
432
|
+
await this.config.onSlashCommand?.(envelope.slashCommand);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
405
435
|
if (!envelope.event)
|
|
406
436
|
return;
|
|
407
437
|
const evt = envelope.event;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BrokerControlPlaneDashboardSnapshot } from "./broker/control-plane-dashboard.js";
|
|
2
|
+
export interface SlackAgentCommandSettings {
|
|
3
|
+
skinTheme?: string | null;
|
|
4
|
+
slackCommandName?: string | null;
|
|
5
|
+
slackCommandNames?: string[] | null;
|
|
6
|
+
}
|
|
7
|
+
export interface SlackAgentsCommandInput {
|
|
8
|
+
command: string;
|
|
9
|
+
text: string;
|
|
10
|
+
channelId: string;
|
|
11
|
+
userId: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function normalizeSlackCommandName(value: string | null | undefined): string | null;
|
|
14
|
+
export declare function resolveSlackAgentCommandNames(settings: SlackAgentCommandSettings, activeSkinTheme?: string | null): string[];
|
|
15
|
+
export declare function formatSlackAgentsUsage(commandNames: string[]): string;
|
|
16
|
+
export declare function isSlackAgentCommand(command: string, commandNames: string[]): boolean;
|
|
17
|
+
export declare function isSlackAgentsListCommand(command: string, text: string, commandNames: string[]): boolean;
|
|
18
|
+
export declare function shouldIncludeSlackAgentsGhosts(text: string): boolean;
|
|
19
|
+
export declare function formatSlackAgentsDashboard(snapshot: BrokerControlPlaneDashboardSnapshot, includeGhosts: boolean): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const DEFAULT_SLACK_COMMAND_NAME = "/pinet";
|
|
2
|
+
const OATHGATE_SLACK_COMMAND_NAME = "/oathgate";
|
|
3
|
+
export function normalizeSlackCommandName(value) {
|
|
4
|
+
const trimmed = value?.trim().toLowerCase();
|
|
5
|
+
if (!trimmed)
|
|
6
|
+
return null;
|
|
7
|
+
const command = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
8
|
+
return /^\/[a-z0-9_-]+$/.test(command) ? command : null;
|
|
9
|
+
}
|
|
10
|
+
function defaultSlackCommandNameForSkin(skinTheme) {
|
|
11
|
+
const normalized = skinTheme?.trim().toLowerCase();
|
|
12
|
+
if (normalized === "oathgate" || normalized === "cosmere" || normalized === "cosmere-inspired") {
|
|
13
|
+
return OATHGATE_SLACK_COMMAND_NAME;
|
|
14
|
+
}
|
|
15
|
+
return DEFAULT_SLACK_COMMAND_NAME;
|
|
16
|
+
}
|
|
17
|
+
export function resolveSlackAgentCommandNames(settings, activeSkinTheme) {
|
|
18
|
+
const configured = [
|
|
19
|
+
...(settings.slackCommandName ? [settings.slackCommandName] : []),
|
|
20
|
+
...(Array.isArray(settings.slackCommandNames) ? settings.slackCommandNames : []),
|
|
21
|
+
]
|
|
22
|
+
.map(normalizeSlackCommandName)
|
|
23
|
+
.filter((command) => command !== null);
|
|
24
|
+
const names = configured.length > 0
|
|
25
|
+
? configured
|
|
26
|
+
: [defaultSlackCommandNameForSkin(activeSkinTheme ?? settings.skinTheme)];
|
|
27
|
+
return [...new Set(names)];
|
|
28
|
+
}
|
|
29
|
+
export function formatSlackAgentsUsage(commandNames) {
|
|
30
|
+
return `Usage: ${commandNames[0] ?? DEFAULT_SLACK_COMMAND_NAME} agents list [all]`;
|
|
31
|
+
}
|
|
32
|
+
function parseSlackCommandTokens(text) {
|
|
33
|
+
return text.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
export function isSlackAgentCommand(command, commandNames) {
|
|
36
|
+
const normalized = normalizeSlackCommandName(command);
|
|
37
|
+
return normalized !== null && commandNames.includes(normalized);
|
|
38
|
+
}
|
|
39
|
+
export function isSlackAgentsListCommand(command, text, commandNames) {
|
|
40
|
+
if (!isSlackAgentCommand(command, commandNames))
|
|
41
|
+
return false;
|
|
42
|
+
const [subject, action] = parseSlackCommandTokens(text);
|
|
43
|
+
return subject === "agents" && action === "list";
|
|
44
|
+
}
|
|
45
|
+
export function shouldIncludeSlackAgentsGhosts(text) {
|
|
46
|
+
return parseSlackCommandTokens(text).some((token) => token === "all" || token === "ghosts" || token === "--all" || token === "--ghosts");
|
|
47
|
+
}
|
|
48
|
+
export function formatSlackAgentsDashboard(snapshot, includeGhosts) {
|
|
49
|
+
const rows = includeGhosts
|
|
50
|
+
? snapshot.roster
|
|
51
|
+
: snapshot.roster.filter((row) => !row.health.includes("ghost") && !row.health.includes("resumable"));
|
|
52
|
+
const visibleRows = rows.slice(0, 20);
|
|
53
|
+
const headerParts = [
|
|
54
|
+
`Pinet agents: ${visibleRows.length}${rows.length !== visibleRows.length ? ` of ${rows.length}` : ""} shown`,
|
|
55
|
+
`${snapshot.liveAgents} live`,
|
|
56
|
+
`${snapshot.workerCount} workers`,
|
|
57
|
+
`${snapshot.workingWorkers} working`,
|
|
58
|
+
`${snapshot.idleWorkers} idle`,
|
|
59
|
+
`backlog ${snapshot.pendingBacklogCount}`,
|
|
60
|
+
];
|
|
61
|
+
if (snapshot.ghostAgents > 0) {
|
|
62
|
+
headerParts.push(`${snapshot.ghostAgents} ghost${snapshot.ghostAgents === 1 ? "" : "s"}`);
|
|
63
|
+
}
|
|
64
|
+
if (snapshot.stuckAgents > 0) {
|
|
65
|
+
headerParts.push(`${snapshot.stuckAgents} stuck`);
|
|
66
|
+
}
|
|
67
|
+
const rosterLines = visibleRows.length
|
|
68
|
+
? visibleRows.flatMap((row) => [
|
|
69
|
+
`${row.label} · ${row.role} · ${row.status} · ${row.health}`,
|
|
70
|
+
` Workload: ${row.workload}`,
|
|
71
|
+
` Task: ${row.taskSummary}`,
|
|
72
|
+
` Heartbeat: ${row.heartbeat}`,
|
|
73
|
+
` Branch: ${row.branch} · Worktree: ${row.worktree}`,
|
|
74
|
+
])
|
|
75
|
+
: ["No agents are currently registered."];
|
|
76
|
+
const activeLanes = snapshot.activeLanes.slice(0, 8);
|
|
77
|
+
const detachedLanes = snapshot.detachedLanes.slice(0, 4);
|
|
78
|
+
return [
|
|
79
|
+
headerParts.join(" · "),
|
|
80
|
+
rosterLines.join("\n"),
|
|
81
|
+
activeLanes.length > 0
|
|
82
|
+
? ["Active lanes:", ...activeLanes.map((lane) => `• ${lane}`)].join("\n")
|
|
83
|
+
: null,
|
|
84
|
+
detachedLanes.length > 0
|
|
85
|
+
? ["Detached lanes:", ...detachedLanes.map((lane) => `• ${lane}`)].join("\n")
|
|
86
|
+
: null,
|
|
87
|
+
]
|
|
88
|
+
.filter((section) => Boolean(section))
|
|
89
|
+
.join("\n\n");
|
|
90
|
+
}
|
package/dist/slack-export.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export type SlackExportFormat = "markdown" | "json" | "plain";
|
|
2
2
|
export interface SlackExportFileInput {
|
|
3
|
+
id?: string;
|
|
3
4
|
name?: string;
|
|
4
5
|
title?: string;
|
|
5
6
|
mimetype?: string;
|
|
6
7
|
filetype?: string;
|
|
7
8
|
permalink?: string;
|
|
8
|
-
urlPrivate?: string;
|
|
9
9
|
preview?: string;
|
|
10
10
|
}
|
|
11
11
|
export interface SlackExportMessageInput {
|
package/dist/slack-export.js
CHANGED
|
@@ -93,14 +93,16 @@ export function formatSlackExportTimestamp(ts) {
|
|
|
93
93
|
}
|
|
94
94
|
function buildSlackFileSummary(file) {
|
|
95
95
|
const label = file.title ?? file.name ?? "attachment";
|
|
96
|
-
const url = file.permalink ?? file.urlPrivate;
|
|
97
96
|
const type = file.filetype ?? file.mimetype;
|
|
98
97
|
const parts = [`- ${label}`];
|
|
99
98
|
if (type) {
|
|
100
99
|
parts.push(`(${type})`);
|
|
101
100
|
}
|
|
102
|
-
if (
|
|
103
|
-
parts.push(
|
|
101
|
+
if (file.id) {
|
|
102
|
+
parts.push(`id=${file.id}`);
|
|
103
|
+
}
|
|
104
|
+
if (file.permalink) {
|
|
105
|
+
parts.push(`— ${file.permalink}`);
|
|
104
106
|
}
|
|
105
107
|
let line = parts.join(" ");
|
|
106
108
|
const preview = file.preview?.trim();
|
|
@@ -183,11 +185,11 @@ function buildJsonExport(options) {
|
|
|
183
185
|
: {}),
|
|
184
186
|
text: convertSlackMrkdwnToMarkdown(message.text ?? "", mentionNames),
|
|
185
187
|
files: (message.files ?? []).map((file) => ({
|
|
188
|
+
id: file.id,
|
|
186
189
|
title: file.title,
|
|
187
190
|
name: file.name,
|
|
188
191
|
type: file.filetype ?? file.mimetype,
|
|
189
192
|
permalink: file.permalink,
|
|
190
|
-
url_private: file.urlPrivate,
|
|
191
193
|
preview: file.preview,
|
|
192
194
|
})),
|
|
193
195
|
})),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SlackResult } from "./slack-api.js";
|
|
2
|
+
export declare const DEFAULT_SLACK_FILE_CACHE_TTL_MS: number;
|
|
3
|
+
export declare const DEFAULT_SLACK_FILE_MAX_DOWNLOAD_BYTES: number;
|
|
4
|
+
export interface SlackFileDescriptor {
|
|
5
|
+
fileId: string;
|
|
6
|
+
path: string;
|
|
7
|
+
filename: string;
|
|
8
|
+
mimetype?: string;
|
|
9
|
+
filetype?: string;
|
|
10
|
+
prettyType?: string;
|
|
11
|
+
size: number;
|
|
12
|
+
sha256: string;
|
|
13
|
+
cacheDir: string;
|
|
14
|
+
expiresAt: string;
|
|
15
|
+
residualRisks: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface SlackFileLookupContext {
|
|
18
|
+
channelId?: string;
|
|
19
|
+
threadTs?: string;
|
|
20
|
+
messageTs?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface SlackFileDownloadDeps {
|
|
23
|
+
slack: (method: string, token: string, body?: Record<string, unknown>) => Promise<SlackResult>;
|
|
24
|
+
token: string;
|
|
25
|
+
fetchImpl?: (url: string, init: RequestInit) => Promise<Pick<Response, "ok" | "status" | "statusText" | "arrayBuffer" | "text"> & {
|
|
26
|
+
body?: Response["body"];
|
|
27
|
+
}>;
|
|
28
|
+
now?: () => Date;
|
|
29
|
+
cacheDir?: string;
|
|
30
|
+
ttlMs?: number;
|
|
31
|
+
maxBytes?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function fetchSlackFileToCache(fileId: string, context: SlackFileLookupContext, deps: SlackFileDownloadDeps): Promise<SlackFileDescriptor>;
|
|
34
|
+
export declare function readCachedSlackFile(pathname: string): Promise<Buffer>;
|