paperclip-plugin-slack-socket 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,136 @@
1
+ import { ASK_HUMAN_TOOL_DECLARATION, DEFAULT_CONFIG, JOB_KEYS, PLUGIN_ID, PLUGIN_VERSION, } from "./constants.js";
2
+ const manifest = {
3
+ id: PLUGIN_ID,
4
+ apiVersion: 1,
5
+ version: PLUGIN_VERSION,
6
+ displayName: "Slack (Socket Mode)",
7
+ description: "Connect Slack over Socket Mode — no public URL required. Chat with a Paperclip agent in DMs and mentions, get configurable notifications, decide approvals with buttons, let agents ask humans questions, and create issues with /paperclip.",
8
+ author: "cvh",
9
+ categories: ["connector", "automation"],
10
+ capabilities: [
11
+ "issues.create",
12
+ "issue.comments.create",
13
+ "issues.wakeup",
14
+ "agent.sessions.create",
15
+ "agent.sessions.send",
16
+ "agent.sessions.close",
17
+ "agent.tools.register",
18
+ "http.outbound",
19
+ "events.subscribe",
20
+ "plugin.state.read",
21
+ "plugin.state.write",
22
+ "secrets.read-ref",
23
+ "instance.settings.register",
24
+ "activity.log.write",
25
+ "metrics.write",
26
+ "jobs.schedule",
27
+ ],
28
+ entrypoints: {
29
+ worker: "./dist/worker.js",
30
+ },
31
+ instanceConfigSchema: {
32
+ type: "object",
33
+ properties: {
34
+ slackBotTokenRef: {
35
+ type: "string",
36
+ format: "secret-ref",
37
+ title: "Slack Bot Token (secret reference)",
38
+ description: "Secret UUID holding your Slack Bot OAuth token (xoxb-…). Create the secret in Settings → Secrets, then paste its UUID here.",
39
+ default: DEFAULT_CONFIG.slackBotTokenRef,
40
+ },
41
+ slackAppTokenRef: {
42
+ type: "string",
43
+ format: "secret-ref",
44
+ title: "Slack App-Level Token (secret reference)",
45
+ description: "Secret UUID holding your Slack App-Level token (xapp-…) with the connections:write scope.",
46
+ default: DEFAULT_CONFIG.slackAppTokenRef,
47
+ },
48
+ companyId: {
49
+ type: "string",
50
+ title: "Company ID",
51
+ description: "Paperclip company UUID used for sessions, issues, and approvals.",
52
+ default: DEFAULT_CONFIG.companyId,
53
+ },
54
+ defaultAgentId: {
55
+ type: "string",
56
+ title: "Default Agent ID",
57
+ description: "Agent that handles DM and @mention conversations.",
58
+ default: DEFAULT_CONFIG.defaultAgentId,
59
+ },
60
+ defaultChannelId: {
61
+ type: "string",
62
+ title: "Default Slack Channel ID",
63
+ description: "Fallback channel for notifications (e.g. C01ABC2DEF3).",
64
+ default: DEFAULT_CONFIG.defaultChannelId,
65
+ },
66
+ notifyOnIssueCreated: {
67
+ type: "boolean",
68
+ title: "Notify on issue created",
69
+ default: DEFAULT_CONFIG.notifyOnIssueCreated,
70
+ },
71
+ notifyOnIssueDone: {
72
+ type: "boolean",
73
+ title: "Notify on issue completed",
74
+ default: DEFAULT_CONFIG.notifyOnIssueDone,
75
+ },
76
+ notifyOnAgentRunFailed: {
77
+ type: "boolean",
78
+ title: "Notify on agent run failure",
79
+ default: DEFAULT_CONFIG.notifyOnAgentRunFailed,
80
+ },
81
+ notifyOnApprovalCreated: {
82
+ type: "boolean",
83
+ title: "Notify on approval requested",
84
+ default: DEFAULT_CONFIG.notifyOnApprovalCreated,
85
+ },
86
+ issuesChannelId: {
87
+ type: "string",
88
+ title: "Issues Channel ID",
89
+ description: "Optional channel for issue notifications (falls back to default).",
90
+ default: DEFAULT_CONFIG.issuesChannelId,
91
+ },
92
+ errorsChannelId: {
93
+ type: "string",
94
+ title: "Errors Channel ID",
95
+ description: "Optional channel for agent failure notifications (falls back to default).",
96
+ default: DEFAULT_CONFIG.errorsChannelId,
97
+ },
98
+ approvalsChannelId: {
99
+ type: "string",
100
+ title: "Approvals Channel ID",
101
+ description: "Optional channel for approval notifications (falls back to default).",
102
+ default: DEFAULT_CONFIG.approvalsChannelId,
103
+ },
104
+ paperclipBaseUrl: {
105
+ type: "string",
106
+ title: "Paperclip Base URL",
107
+ description: "Base URL of your Paperclip instance. Load-bearing: used both to build dashboard links and as the target of the approval decision REST calls (POST {paperclipBaseUrl}/api/approvals/:id/approve|reject).",
108
+ default: DEFAULT_CONFIG.paperclipBaseUrl,
109
+ },
110
+ paperclipApiKeyRef: {
111
+ type: "string",
112
+ format: "secret-ref",
113
+ title: "Paperclip Board API Key (secret reference)",
114
+ description: "Secret reference holding a Paperclip API key for a board-role user, sent as an Authorization: Bearer header on approval decision requests. Leave empty for local_trusted deployments, where every request is implicitly authenticated as board and no header is needed. Required for authenticated deployments so approval decisions (Approve/Reject button clicks) authenticate as a board user.",
115
+ default: DEFAULT_CONFIG.paperclipApiKeyRef,
116
+ },
117
+ sessionIdleHours: {
118
+ type: "number",
119
+ title: "Session Idle Hours",
120
+ description: "Close agent sessions idle longer than this many hours.",
121
+ default: DEFAULT_CONFIG.sessionIdleHours,
122
+ },
123
+ },
124
+ required: ["slackBotTokenRef", "slackAppTokenRef", "companyId", "defaultAgentId", "defaultChannelId"],
125
+ },
126
+ jobs: [
127
+ {
128
+ jobKey: JOB_KEYS.cleanup,
129
+ displayName: "Cleanup idle sessions and expired questions",
130
+ description: "Closes agent sessions idle beyond the configured TTL and expires unanswered ask-human questions.",
131
+ schedule: "*/15 * * * *",
132
+ },
133
+ ],
134
+ tools: [ASK_HUMAN_TOOL_DECLARATION],
135
+ };
136
+ export default manifest;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Converts Markdown text to Slack mrkdwn. Fenced (```…```) and inline
3
+ * (`…`) code spans are stashed before any other conversion and restored
4
+ * verbatim at the end, so Markdown-looking text inside code is never touched.
5
+ */
6
+ export declare function markdownToMrkdwn(text: string): string;
package/dist/mrkdwn.js ADDED
Binary file
@@ -0,0 +1,8 @@
1
+ import type { PluginContext } from "@paperclipai/plugin-sdk";
2
+ import type { SlackGateway, SlackSocketConfig } from "./types.js";
3
+ export interface NotificationDeps {
4
+ ctx: PluginContext;
5
+ gateway: SlackGateway;
6
+ getConfig: () => Promise<SlackSocketConfig>;
7
+ }
8
+ export declare function registerNotifications({ ctx, gateway, getConfig }: NotificationDeps): void;
@@ -0,0 +1,75 @@
1
+ import { STATE_KEYS, stateScope } from "./constants.js";
2
+ import { formatAgentRunFailed, formatIssueCreated, formatIssueDone } from "./formatters.js";
3
+ import { errString } from "./redact.js";
4
+ import { updateIndex } from "./state-index.js";
5
+ export function registerNotifications({ ctx, gateway, getConfig }) {
6
+ const post = async (channel, content, type, threadTs) => {
7
+ try {
8
+ const posted = await gateway.postMessage({ channel, ...content, threadTs });
9
+ await ctx.metrics.write("slack.notifications.sent", 1, { type }).catch(() => { });
10
+ return posted;
11
+ }
12
+ catch (err) {
13
+ ctx.logger.warn("Slack notification failed", { err: errString(err), channel });
14
+ await ctx.metrics.write("slack.notifications.failed", 1, { type }).catch(() => { });
15
+ return null;
16
+ }
17
+ };
18
+ ctx.events.on("issue.created", async (event) => {
19
+ const e = event;
20
+ const cfg = await getConfig();
21
+ if (!cfg.notifyOnIssueCreated)
22
+ return;
23
+ const channel = cfg.issuesChannelId || cfg.defaultChannelId;
24
+ if (!channel)
25
+ return;
26
+ const issueId = e.entityId ?? "";
27
+ const posted = await post(channel, formatIssueCreated(e.payload, issueId, cfg.paperclipBaseUrl), "issue_created");
28
+ if (posted && issueId) {
29
+ const key = STATE_KEYS.issueThread(issueId);
30
+ const entry = {
31
+ channel: posted.channel,
32
+ ts: posted.ts,
33
+ createdAt: new Date().toISOString(),
34
+ };
35
+ await ctx.state.set(stateScope(key), entry);
36
+ await updateIndex(ctx, STATE_KEYS.issueThreadIndex, (current) => current.includes(key) ? current : [...current, key]);
37
+ }
38
+ });
39
+ ctx.events.on("issue.updated", async (event) => {
40
+ const e = event;
41
+ const cfg = await getConfig();
42
+ if (!cfg.notifyOnIssueDone)
43
+ return;
44
+ const payload = e.payload;
45
+ if (payload?.status !== "done")
46
+ return;
47
+ const channel = cfg.issuesChannelId || cfg.defaultChannelId;
48
+ if (!channel)
49
+ return;
50
+ const issueId = e.entityId ?? "";
51
+ const key = issueId ? STATE_KEYS.issueThread(issueId) : null;
52
+ const entry = key ? (await ctx.state.get(stateScope(key))) : null;
53
+ if (entry && key && entry.channel === channel) {
54
+ // Post the completion notice as a threaded reply on the original
55
+ // "issue created" message, then the link is no longer needed — the
56
+ // issue is finished.
57
+ await post(channel, formatIssueDone(payload, issueId, cfg.paperclipBaseUrl), "issue_done", entry.ts);
58
+ await ctx.state.delete(stateScope(key));
59
+ await updateIndex(ctx, STATE_KEYS.issueThreadIndex, (current) => current.filter((k) => k !== key));
60
+ }
61
+ else {
62
+ await post(channel, formatIssueDone(payload, issueId, cfg.paperclipBaseUrl), "issue_done");
63
+ }
64
+ });
65
+ ctx.events.on("agent.run.failed", async (event) => {
66
+ const e = event;
67
+ const cfg = await getConfig();
68
+ if (!cfg.notifyOnAgentRunFailed)
69
+ return;
70
+ const channel = cfg.errorsChannelId || cfg.defaultChannelId;
71
+ if (!channel)
72
+ return;
73
+ await post(channel, formatAgentRunFailed(e.payload), "agent_run_failed");
74
+ });
75
+ }
@@ -0,0 +1,3 @@
1
+ export declare function redactSecrets(s: string): string;
2
+ /** String(err), with any Slack tokens in the result redacted. */
3
+ export declare function errString(err: unknown): string;
package/dist/redact.js ADDED
@@ -0,0 +1,13 @@
1
+ // Log-redaction safety net: Slack bot/app tokens should never reach logs in
2
+ // the first place, but errors thrown by the Slack SDK sometimes echo back
3
+ // request details (including a token) in their message. Scrub known Slack
4
+ // token shapes before any error ever reaches a logger call.
5
+ const BOT_TOKEN_PATTERN = /xox[a-z]-[A-Za-z0-9-]+/gi;
6
+ const APP_TOKEN_PATTERN = /xapp-[A-Za-z0-9-]+/gi;
7
+ export function redactSecrets(s) {
8
+ return s.replace(BOT_TOKEN_PATTERN, "[REDACTED]").replace(APP_TOKEN_PATTERN, "[REDACTED]");
9
+ }
10
+ /** String(err), with any Slack tokens in the result redacted. */
11
+ export function errString(err) {
12
+ return redactSecrets(String(err));
13
+ }
@@ -0,0 +1,8 @@
1
+ import type { PluginContext } from "@paperclipai/plugin-sdk";
2
+ /**
3
+ * Atomically (within this process) reads the string[] index stored at
4
+ * `indexKey`, applies `updater` to it, writes the result back, and returns
5
+ * it. Concurrent calls for the same `indexKey` are serialized so none of
6
+ * them observe a stale pre-update value.
7
+ */
8
+ export declare function updateIndex(ctx: PluginContext, indexKey: string, updater: (current: string[]) => string[]): Promise<string[]>;
@@ -0,0 +1,23 @@
1
+ import { stateScope } from "./constants.js";
2
+ // Per-process chain of pending updates, keyed by the index's state key. Each
3
+ // call is appended to the chain for its key so updates run strictly in
4
+ // order: read current -> derive next -> write next, with no interleaving.
5
+ const chains = new Map();
6
+ /**
7
+ * Atomically (within this process) reads the string[] index stored at
8
+ * `indexKey`, applies `updater` to it, writes the result back, and returns
9
+ * it. Concurrent calls for the same `indexKey` are serialized so none of
10
+ * them observe a stale pre-update value.
11
+ */
12
+ export async function updateIndex(ctx, indexKey, updater) {
13
+ const previous = chains.get(indexKey) ?? Promise.resolve();
14
+ let result;
15
+ const next = previous.catch(() => { }).then(async () => {
16
+ const current = (await ctx.state.get(stateScope(indexKey))) ?? [];
17
+ result = updater(current);
18
+ await ctx.state.set(stateScope(indexKey), result);
19
+ });
20
+ chains.set(indexKey, next);
21
+ await next;
22
+ return result;
23
+ }
@@ -0,0 +1,103 @@
1
+ import type { EnvSecretRefBinding } from "@paperclipai/plugin-sdk";
2
+ export type SecretRef = string | EnvSecretRefBinding;
3
+ export interface SlackSocketConfig {
4
+ slackBotTokenRef: SecretRef;
5
+ slackAppTokenRef: SecretRef;
6
+ paperclipApiKeyRef: SecretRef;
7
+ companyId: string;
8
+ defaultAgentId: string;
9
+ defaultChannelId: string;
10
+ notifyOnIssueCreated: boolean;
11
+ notifyOnIssueDone: boolean;
12
+ notifyOnAgentRunFailed: boolean;
13
+ notifyOnApprovalCreated: boolean;
14
+ issuesChannelId: string;
15
+ errorsChannelId: string;
16
+ approvalsChannelId: string;
17
+ paperclipBaseUrl: string;
18
+ sessionIdleHours: number;
19
+ }
20
+ export interface SessionEntry {
21
+ sessionId: string;
22
+ channel: string;
23
+ threadTs: string;
24
+ lastActivityAt: string;
25
+ }
26
+ export interface IssueThreadEntry {
27
+ channel: string;
28
+ ts: string;
29
+ createdAt: string;
30
+ }
31
+ export type QuestionMode = "reaction" | "answer";
32
+ export interface PendingQuestion {
33
+ channel: string;
34
+ ts: string;
35
+ issueId: string;
36
+ companyId: string;
37
+ mode: QuestionMode;
38
+ question: string;
39
+ askedAt: string;
40
+ timeoutMinutes: number;
41
+ }
42
+ export interface InboundMessage {
43
+ channel: string;
44
+ channelType: "im" | "channel" | "group";
45
+ user: string;
46
+ text: string;
47
+ ts: string;
48
+ threadTs?: string;
49
+ }
50
+ export interface InboundReaction {
51
+ channel: string;
52
+ messageTs: string;
53
+ user: string;
54
+ reaction: string;
55
+ }
56
+ export interface InboundAction {
57
+ actionId: string;
58
+ value: string;
59
+ user: string;
60
+ userName: string;
61
+ channel: string;
62
+ messageTs: string;
63
+ }
64
+ export interface InboundCommand {
65
+ command: string;
66
+ text: string;
67
+ user: string;
68
+ channel: string;
69
+ }
70
+ export interface OutboundMessage {
71
+ channel: string;
72
+ text: string;
73
+ blocks?: unknown[];
74
+ threadTs?: string;
75
+ }
76
+ export interface SlackGateway {
77
+ start(): Promise<void>;
78
+ stop(): Promise<void>;
79
+ isConnected(): boolean;
80
+ botUserId(): string | undefined;
81
+ postMessage(msg: OutboundMessage): Promise<{
82
+ channel: string;
83
+ ts: string;
84
+ }>;
85
+ updateMessage(msg: {
86
+ channel: string;
87
+ ts: string;
88
+ text: string;
89
+ blocks?: unknown[];
90
+ }): Promise<void>;
91
+ postEphemeral(msg: {
92
+ channel: string;
93
+ user: string;
94
+ text: string;
95
+ }): Promise<void>;
96
+ openDm(userId: string): Promise<string>;
97
+ getUserDisplayName(userId: string): Promise<string>;
98
+ onMessage(handler: (msg: InboundMessage) => Promise<void>): void;
99
+ onMention(handler: (msg: InboundMessage) => Promise<void>): void;
100
+ onReaction(handler: (reaction: InboundReaction) => Promise<void>): void;
101
+ onAction(pattern: RegExp, handler: (action: InboundAction) => Promise<void>): void;
102
+ onCommand(command: string, handler: (cmd: InboundCommand) => Promise<void>): void;
103
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ // Shared types for the Slack Socket Mode plugin.
2
+ export {};
@@ -0,0 +1,12 @@
1
+ import { type PluginContext, type PluginHealthDiagnostics } from "@paperclipai/plugin-sdk";
2
+ import type { SlackGateway } from "./types.js";
3
+ export type GatewayFactory = (opts: {
4
+ botToken: string;
5
+ appToken: string;
6
+ }) => SlackGateway;
7
+ type Health = PluginHealthDiagnostics & {
8
+ message?: string;
9
+ };
10
+ export declare function startRuntime(ctx: PluginContext, makeGateway: GatewayFactory): Promise<Health>;
11
+ declare const plugin: import("@paperclipai/plugin-sdk").PaperclipPlugin;
12
+ export default plugin;
package/dist/worker.js ADDED
@@ -0,0 +1,144 @@
1
+ import { definePlugin, runWorker, } from "@paperclipai/plugin-sdk";
2
+ import { createApprovals } from "./approvals.js";
3
+ import { createAskHuman } from "./ask-human.js";
4
+ import { BoltGateway } from "./bolt-gateway.js";
5
+ import { createChat } from "./chat.js";
6
+ import { runCleanup } from "./cleanup.js";
7
+ import { createCommands } from "./commands.js";
8
+ import { loadConfig } from "./config.js";
9
+ import { DEFAULT_CONFIG, JOB_KEYS, SLASH_COMMAND } from "./constants.js";
10
+ import { createEventDeduper } from "./event-dedup.js";
11
+ import { registerNotifications } from "./notifications.js";
12
+ import { errString } from "./redact.js";
13
+ const REQUIRED_FIELDS = ["slackBotTokenRef", "slackAppTokenRef", "companyId", "defaultAgentId"];
14
+ let health = { status: "ok" };
15
+ let gateway = null;
16
+ let lastCtx = null;
17
+ export async function startRuntime(ctx, makeGateway) {
18
+ const cfg = await loadConfig(ctx);
19
+ const missing = REQUIRED_FIELDS.filter((field) => !cfg[field]);
20
+ if (missing.length > 0) {
21
+ health = { status: "degraded", message: `Slack Socket plugin not configured: missing ${missing.join(", ")}` };
22
+ ctx.logger.warn("Slack Socket plugin not configured; runtime disabled", { missing });
23
+ return health;
24
+ }
25
+ let botToken;
26
+ let appToken;
27
+ try {
28
+ botToken = await ctx.secrets.resolve(cfg.slackBotTokenRef);
29
+ appToken = await ctx.secrets.resolve(cfg.slackAppTokenRef);
30
+ }
31
+ catch (err) {
32
+ health = { status: "degraded", message: "Failed to resolve Slack token secrets; check the secret references" };
33
+ ctx.logger.error("Slack token secret resolution failed", { err: errString(err) });
34
+ return health;
35
+ }
36
+ gateway = makeGateway({ botToken, appToken });
37
+ const getConfig = () => loadConfig(ctx);
38
+ const chat = createChat({ ctx, gateway, getConfig });
39
+ const askHuman = createAskHuman({ ctx, gateway });
40
+ const approvals = createApprovals({ ctx, gateway, getConfig });
41
+ const commands = createCommands({ ctx, gateway, getConfig });
42
+ registerNotifications({ ctx, gateway, getConfig });
43
+ askHuman.registerTool();
44
+ // Slack Socket Mode redelivers events at-least-once, and a reconnect can
45
+ // replay a backlog of stale events. Dedupe/stale-filter mention and
46
+ // message dispatch before it reaches ask-human's answer routing or chat —
47
+ // reactions, actions, and commands are not deduped (they're not prone to
48
+ // the same at-least-once redelivery pattern here and are already
49
+ // effectively idempotent or externally acked). Keys are namespaced by
50
+ // event type ("mention:"/"message:") because a single channel @mention
51
+ // arrives as two distinct Slack events sharing the same ts (app_mention +
52
+ // message.channels) — without the prefix, consuming one event's key would
53
+ // shadow the other's and silently drop it as a "duplicate".
54
+ const eventDeduper = createEventDeduper();
55
+ gateway.onMention(async (msg) => {
56
+ if (!eventDeduper.shouldProcess(`mention:${msg.channel}:${msg.ts}`))
57
+ return;
58
+ await chat.handleMention(msg);
59
+ });
60
+ gateway.onMessage(async (msg) => {
61
+ if (!eventDeduper.shouldProcess(`message:${msg.channel}:${msg.ts}`))
62
+ return;
63
+ if (await askHuman.tryHandleAnswer(msg))
64
+ return;
65
+ await chat.handleMessage(msg);
66
+ });
67
+ gateway.onReaction((reaction) => askHuman.handleReaction(reaction));
68
+ gateway.onAction(/^approval_(approve|reject)$/, (action) => approvals.handleAction(action));
69
+ gateway.onCommand(SLASH_COMMAND, (cmd) => commands.handleCommand(cmd));
70
+ ctx.jobs.register(JOB_KEYS.cleanup, async () => {
71
+ if (!gateway)
72
+ return;
73
+ await runCleanup(ctx, gateway, await loadConfig(ctx));
74
+ });
75
+ await gateway.start();
76
+ health = { status: "ok" };
77
+ ctx.logger.info("Slack Socket Mode connected");
78
+ return health;
79
+ }
80
+ const plugin = definePlugin({
81
+ async setup(ctx) {
82
+ lastCtx = ctx;
83
+ try {
84
+ await startRuntime(ctx, (opts) => new BoltGateway({ ...opts, logger: ctx.logger }));
85
+ }
86
+ catch (err) {
87
+ health = { status: "degraded", message: `Slack Socket startup failed: ${errString(err)}` };
88
+ ctx.logger.error("Slack Socket startup failed", { err: errString(err) });
89
+ }
90
+ },
91
+ async onShutdown() {
92
+ await gateway?.stop().catch(() => { });
93
+ },
94
+ async onHealth() {
95
+ if (health.status !== "ok")
96
+ return health;
97
+ if (gateway && !gateway.isConnected()) {
98
+ return { status: "degraded", message: "Slack Socket Mode disconnected; Bolt is reconnecting" };
99
+ }
100
+ return { status: "ok" };
101
+ },
102
+ async onValidateConfig(config) {
103
+ const cfg = { ...DEFAULT_CONFIG, ...config };
104
+ const errors = [];
105
+ for (const field of [...REQUIRED_FIELDS, "defaultChannelId"]) {
106
+ if (!cfg[field])
107
+ errors.push(`${field} is required`);
108
+ }
109
+ if (errors.length > 0)
110
+ return { ok: false, errors };
111
+ if (!lastCtx) {
112
+ return { ok: false, errors: ["Validation unavailable: plugin context not initialized"] };
113
+ }
114
+ let WebClient;
115
+ try {
116
+ ({ WebClient } = await import("@slack/web-api"));
117
+ }
118
+ catch (err) {
119
+ errors.push(`Validation unavailable: ${errString(err)}`);
120
+ return { ok: false, errors };
121
+ }
122
+ try {
123
+ const botToken = await lastCtx.secrets.resolve(cfg.slackBotTokenRef);
124
+ const auth = await new WebClient(botToken).auth.test();
125
+ if (!auth.ok)
126
+ errors.push("Slack auth.test failed for the bot token");
127
+ }
128
+ catch (err) {
129
+ errors.push(`Bot token check failed: ${errString(err)}`);
130
+ }
131
+ try {
132
+ const appToken = await lastCtx.secrets.resolve(cfg.slackAppTokenRef);
133
+ const conn = await new WebClient(appToken).apps.connections.open();
134
+ if (!conn.ok)
135
+ errors.push("apps.connections.open failed for the app token (needs connections:write)");
136
+ }
137
+ catch (err) {
138
+ errors.push(`App token check failed: ${errString(err)}`);
139
+ }
140
+ return { ok: errors.length === 0, errors };
141
+ },
142
+ });
143
+ export default plugin;
144
+ runWorker(plugin, import.meta.url);
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "paperclip-plugin-slack-socket",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Slack Socket Mode plugin for Paperclip: agent chat, notifications, approvals, ask-human tool. No public URL required.",
6
+ "main": "./dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
10
+ "test": "vitest run",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "dependencies": {
14
+ "@slack/bolt": "^5.0.0",
15
+ "@slack/web-api": "^8.0.0"
16
+ },
17
+ "peerDependencies": {
18
+ "@paperclipai/plugin-sdk": "*"
19
+ },
20
+ "devDependencies": {
21
+ "@paperclipai/plugin-sdk": "2026.722.0",
22
+ "@types/node": "^25.5.2",
23
+ "typescript": "^5.7.0",
24
+ "vitest": "^3.2.6"
25
+ },
26
+ "files": ["dist/", "slack-app-manifest.json"],
27
+ "paperclipPlugin": {
28
+ "manifest": "./dist/manifest.js",
29
+ "worker": "./dist/worker.js"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/0xCVH/paperclip-plugin-slack-socket.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/0xCVH/paperclip-plugin-slack-socket/issues"
37
+ },
38
+ "homepage": "https://github.com/0xCVH/paperclip-plugin-slack-socket#readme",
39
+ "author": "0xCVH",
40
+ "license": "MIT"
41
+ }
@@ -0,0 +1,54 @@
1
+ {
2
+ "display_information": {
3
+ "name": "Paperclip",
4
+ "description": "Chat with your Paperclip agents from Slack",
5
+ "background_color": "#1f2430"
6
+ },
7
+ "features": {
8
+ "bot_user": {
9
+ "display_name": "paperclip",
10
+ "always_online": true
11
+ },
12
+ "slash_commands": [
13
+ {
14
+ "command": "/paperclip",
15
+ "description": "Paperclip commands",
16
+ "usage_hint": "issue <title> | help",
17
+ "should_escape": false
18
+ }
19
+ ]
20
+ },
21
+ "oauth_config": {
22
+ "scopes": {
23
+ "bot": [
24
+ "app_mentions:read",
25
+ "chat:write",
26
+ "channels:history",
27
+ "groups:history",
28
+ "im:history",
29
+ "im:read",
30
+ "im:write",
31
+ "reactions:read",
32
+ "users:read",
33
+ "commands"
34
+ ]
35
+ }
36
+ },
37
+ "settings": {
38
+ "event_subscriptions": {
39
+ "bot_events": [
40
+ "app_mention",
41
+ "message.channels",
42
+ "message.groups",
43
+ "message.im",
44
+ "reaction_added"
45
+ ]
46
+ },
47
+ "interactivity": {
48
+ "is_enabled": true
49
+ },
50
+ "org_deploy_enabled": false,
51
+ "socket_mode_enabled": true,
52
+ "token_rotation_enabled": false
53
+ }
54
+ }