@cursor/july 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agent-serve.js +31 -2
- package/dist/channels/slack/channel-watch.d.ts +6 -1
- package/dist/channels/slack/channel-watch.d.ts.map +1 -1
- package/dist/channels/slack/channel-watch.js +22 -1
- package/dist/channels/slack/cursor-account.d.ts +4 -0
- package/dist/channels/slack/cursor-account.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.js +12 -2
- package/dist/channels/slack/defaults.d.ts +10 -5
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +39 -17
- package/dist/channels/slack/dispatch.d.ts +18 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +141 -38
- package/dist/channels/slack/inbound.d.ts +13 -3
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +17 -9
- package/dist/channels/slack/index.d.ts +4 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +4 -0
- package/dist/channels/slack/live-delivery.d.ts.map +1 -1
- package/dist/channels/slack/live-delivery.js +1 -0
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +3 -0
- package/dist/channels/slack/nudge-carrier.d.ts +19 -0
- package/dist/channels/slack/nudge-carrier.d.ts.map +1 -0
- package/dist/channels/slack/nudge-carrier.js +166 -0
- package/dist/channels/slack/nudge-store.d.ts +61 -0
- package/dist/channels/slack/nudge-store.d.ts.map +1 -0
- package/dist/channels/slack/nudge-store.js +266 -0
- package/dist/channels/slack/post-update-delivery.js +318 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +74 -25
- package/dist/channels/slack/stop.d.ts +14 -0
- package/dist/channels/slack/stop.d.ts.map +1 -0
- package/dist/channels/slack/stop.js +22 -0
- package/dist/channels/slack/thread-context.d.ts +81 -0
- package/dist/channels/slack/thread-context.d.ts.map +1 -0
- package/dist/channels/slack/thread-context.js +470 -0
- package/dist/channels/slack/types.d.ts +27 -0
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.COTN7wgo.js → app.Ciosf24M.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.D154Detj.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BW3TBdT0.js → VPLocalSearchBox.BcRDly7n.js} +1 -1
- package/dist/docs/assets/chunks/{theme.BEJW0vE7.js → theme.CMpbjdfx.js} +2 -2
- package/dist/docs/assets/{deployment.md.BtfEsc9S.js → deployment.md.DC5-ImcM.js} +1 -1
- package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.js → example-agents_approval-buddy.md.DFGBYLcc.js} +2 -2
- package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.lean.js → example-agents_approval-buddy.md.DFGBYLcc.lean.js} +1 -1
- package/dist/docs/assets/example-agents_index.md.BPXjUtRB.js +2 -0
- package/dist/docs/assets/example-agents_index.md.BPXjUtRB.lean.js +1 -0
- package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.js +10 -0
- package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.lean.js +1 -0
- package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.js → example-agents_security-reviewer.md.CE9v-aKm.js} +2 -2
- package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.lean.js → example-agents_security-reviewer.md.CE9v-aKm.lean.js} +1 -1
- package/dist/docs/assets/{guides_slack.md.CCwqHvSV.js → guides_slack.md.FWale6Ip.js} +4 -3
- package/dist/docs/assets/{guides_slack.md.CCwqHvSV.lean.js → guides_slack.md.FWale6Ip.lean.js} +1 -1
- package/dist/docs/assets/{reference_sessions.md.DdjWabnN.js → reference_sessions.md.DLd6mvbv.js} +2 -2
- package/dist/docs/assets/{reference_sessions.md.DdjWabnN.lean.js → reference_sessions.md.DLd6mvbv.lean.js} +1 -1
- package/dist/docs/assets/{storage.md.CVnInNiN.js → storage.md.DRTdnFvd.js} +9 -2
- package/dist/docs/assets/{storage.md.CVnInNiN.lean.js → storage.md.DRTdnFvd.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +6 -6
- package/dist/docs/evals.html +4 -4
- package/dist/docs/example-agents/approval-buddy.html +5 -5
- package/dist/docs/example-agents/benny.html +5 -5
- package/dist/docs/example-agents/bugbot.html +5 -5
- package/dist/docs/example-agents/codebase-wiki.html +4 -4
- package/dist/docs/example-agents/codeowners-review.html +4 -4
- package/dist/docs/example-agents/concierge.html +4 -4
- package/dist/docs/example-agents/fsd.html +4 -4
- package/dist/docs/example-agents/index.html +6 -6
- package/dist/docs/example-agents/knowledge-base.html +4 -4
- package/dist/docs/example-agents/oncall.html +34 -0
- package/dist/docs/example-agents/security-reviewer.html +6 -6
- package/dist/docs/example-agents/slack-agent.html +4 -4
- package/dist/docs/example-agents/weather-agent.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-runtime.html +4 -4
- package/dist/docs/guides/github.html +4 -4
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/slack.html +7 -6
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +4 -4
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +4 -4
- package/dist/docs/reference/connections.html +4 -4
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +4 -4
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +5 -5
- package/dist/docs/reference/skills.html +4 -4
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +4 -4
- package/dist/docs/scaffolding-agents.html +4 -4
- package/dist/docs/storage.html +13 -6
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +45 -18
- package/dist/internal/cli-mcp.d.ts +64 -0
- package/dist/internal/cli-mcp.d.ts.map +1 -0
- package/dist/internal/cli-mcp.js +570 -0
- package/dist/internal/deploy-client.d.ts +7 -1
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +3 -0
- package/dist/internal/deploy-source.d.ts +3 -0
- package/dist/internal/deploy-source.d.ts.map +1 -1
- package/dist/internal/deploy-source.js +25 -7
- package/dist/internal/discovery.js +1 -1
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +3 -0
- package/dist/internal/host-kv.d.ts +17 -0
- package/dist/internal/host-kv.d.ts.map +1 -0
- package/dist/internal/host-kv.js +77 -0
- package/dist/internal/hosting.d.ts +12 -2
- package/dist/internal/hosting.d.ts.map +1 -1
- package/dist/internal/hosting.js +21 -4
- package/dist/internal/json-dir-store.js +100 -0
- package/dist/internal/logs-client.d.ts +16 -2
- package/dist/internal/logs-client.d.ts.map +1 -1
- package/dist/internal/logs-client.js +51 -16
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +8 -0
- package/dist/internal/session-engine.d.ts +42 -7
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +276 -66
- package/dist/internal/storage-coordinator.d.ts +18 -1
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +43 -0
- package/dist/internal/workspace.d.ts +19 -0
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +47 -13
- package/dist/playground/assets/{index-72vCOBWO.js → index-C0-QEG4O.js} +42 -42
- package/dist/playground/assets/index-z3JY9O2x.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +65 -0
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -0
- package/dist/storage-backends/cursor-hosted.js +203 -0
- package/dist/storage-backends/file-kv.d.ts +42 -0
- package/dist/storage-backends/file-kv.d.ts.map +1 -0
- package/dist/storage-backends/file-kv.js +190 -0
- package/dist/storage-backends/postgres-kv.d.ts +82 -0
- package/dist/storage-backends/postgres-kv.d.ts.map +1 -0
- package/dist/storage-backends/postgres-kv.js +185 -0
- package/dist/storage.d.ts +10 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +10 -1
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/.vitepress/cache/deps/package.json +3 -0
- package/docs/deployment.md +7 -3
- package/docs/example-agents/approval-buddy.md +1 -1
- package/docs/example-agents/index.md +15 -10
- package/docs/example-agents/oncall.md +216 -0
- package/docs/example-agents/security-reviewer.md +5 -5
- package/docs/guides/slack.md +11 -2
- package/docs/reference/sessions.md +13 -4
- package/docs/storage.md +41 -0
- package/package.json +22 -1
- package/skills/framework-map/SKILL.md +6 -4
- package/src/bin/agent-serve.ts +32 -2
- package/src/channels/slack/channel-watch.ts +28 -1
- package/src/channels/slack/cursor-account.ts +20 -0
- package/src/channels/slack/defaults.ts +51 -20
- package/src/channels/slack/dispatch.ts +215 -40
- package/src/channels/slack/inbound.ts +30 -10
- package/src/channels/slack/index.ts +4 -0
- package/src/channels/slack/live-delivery.ts +1 -0
- package/src/channels/slack/manifest.ts +3 -0
- package/src/channels/slack/nudge-carrier.ts +182 -0
- package/src/channels/slack/nudge-store.ts +308 -0
- package/src/channels/slack/slack-channel.ts +106 -31
- package/src/channels/slack/stop.ts +25 -0
- package/src/channels/slack/thread-context.ts +572 -0
- package/src/channels/slack/types.ts +27 -0
- package/src/internal/cli-deploy.ts +58 -20
- package/src/internal/cli-mcp.ts +708 -0
- package/src/internal/deploy-client.ts +11 -0
- package/src/internal/deploy-source.ts +24 -6
- package/src/internal/discovery.ts +1 -1
- package/src/internal/distribution.ts +3 -0
- package/src/internal/host-kv.ts +85 -0
- package/src/internal/hosting.ts +24 -4
- package/src/internal/logs-client.ts +67 -21
- package/src/internal/server.ts +31 -0
- package/src/internal/session-engine.ts +379 -61
- package/src/internal/storage-coordinator.ts +41 -0
- package/src/internal/workspace.ts +48 -11
- package/src/storage-backends/cursor-hosted.ts +246 -0
- package/src/storage-backends/file-kv.ts +221 -0
- package/src/storage-backends/postgres-kv.ts +253 -0
- package/src/storage.ts +11 -1
- package/src/types.ts +83 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.B7UcKvIn.js +0 -1
- package/dist/docs/assets/example-agents_index.md.CRqJlnIf.js +0 -2
- package/dist/docs/assets/example-agents_index.md.CRqJlnIf.lean.js +0 -1
- package/dist/internal/persistence-coordinator.d.ts +0 -127
- package/dist/internal/persistence-coordinator.d.ts.map +0 -1
- package/dist/persistence.d.ts +0 -184
- package/dist/persistence.d.ts.map +0 -1
- package/dist/playground/assets/index-BjnMwYoR.css +0 -1
- /package/dist/docs/assets/{deployment.md.BtfEsc9S.lean.js → deployment.md.DC5-ImcM.lean.js} +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-side nudge carrier: a small Cursor hooks.json + script that drains
|
|
3
|
+
* the durable Slack nudge store at tool boundaries (MCP / builtin tools).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
export const SLACK_NUDGE_ROOT_REL = ".agent-serve/slack-nudge-root";
|
|
10
|
+
export const SLACK_NUDGE_CONTINUATION_REL =
|
|
11
|
+
".agent-serve/slack-nudge-continuation";
|
|
12
|
+
export const SLACK_NUDGE_CARRIER_REL = ".agent-serve/slack-nudge-carrier.mjs";
|
|
13
|
+
|
|
14
|
+
/** Carrier script body — reads nudge-root + continuation, drains pending. */
|
|
15
|
+
export const SLACK_NUDGE_CARRIER_SCRIPT: string = `#!/usr/bin/env node
|
|
16
|
+
import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const workspace = dirname(here);
|
|
22
|
+
|
|
23
|
+
function safeKey(value) {
|
|
24
|
+
const trimmed = String(value ?? "").trim();
|
|
25
|
+
if (trimmed === "" || trimmed === "." || trimmed === "..") return "_empty";
|
|
26
|
+
const sanitized = trimmed
|
|
27
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
|
28
|
+
.replace(/^\\.+/, "")
|
|
29
|
+
.replace(/\\.+/g, "_")
|
|
30
|
+
.slice(0, 180);
|
|
31
|
+
if (sanitized === "" || sanitized === "." || sanitized === "..") return "_empty";
|
|
32
|
+
return sanitized;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readText(rel) {
|
|
36
|
+
try {
|
|
37
|
+
return (await readFile(join(workspace, rel), "utf8")).trim();
|
|
38
|
+
} catch {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function main() {
|
|
44
|
+
const root = await readText(${JSON.stringify(SLACK_NUDGE_ROOT_REL)});
|
|
45
|
+
const continuation = await readText(
|
|
46
|
+
${JSON.stringify(SLACK_NUDGE_CONTINUATION_REL)}
|
|
47
|
+
);
|
|
48
|
+
if (root === "" || continuation === "") {
|
|
49
|
+
process.stdout.write("{}\\n");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const pendingDir = join(root, safeKey(continuation), "pending");
|
|
53
|
+
const deliveredDir = join(root, safeKey(continuation), "delivered");
|
|
54
|
+
let names = [];
|
|
55
|
+
try {
|
|
56
|
+
names = await readdir(pendingDir);
|
|
57
|
+
} catch {
|
|
58
|
+
process.stdout.write("{}\\n");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const nudges = [];
|
|
62
|
+
await mkdir(deliveredDir, { recursive: true });
|
|
63
|
+
for (const name of names) {
|
|
64
|
+
if (!name.endsWith(".json")) continue;
|
|
65
|
+
const pendingPath = join(pendingDir, name);
|
|
66
|
+
let record;
|
|
67
|
+
try {
|
|
68
|
+
record = JSON.parse(await readFile(pendingPath, "utf8"));
|
|
69
|
+
} catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const sourceTs = typeof record.sourceTs === "string" ? record.sourceTs : "";
|
|
73
|
+
if (sourceTs === "") continue;
|
|
74
|
+
const dest = join(deliveredDir, safeKey(sourceTs));
|
|
75
|
+
try {
|
|
76
|
+
await readFile(dest, "utf8");
|
|
77
|
+
await rm(pendingPath, { force: true });
|
|
78
|
+
continue;
|
|
79
|
+
} catch {
|
|
80
|
+
// not delivered yet
|
|
81
|
+
}
|
|
82
|
+
const tmp = dest + "." + process.pid + ".tmp";
|
|
83
|
+
try {
|
|
84
|
+
await writeFile(tmp, JSON.stringify(record) + "\\n", "utf8");
|
|
85
|
+
await rename(tmp, dest);
|
|
86
|
+
await rm(pendingPath, { force: true });
|
|
87
|
+
nudges.push(record);
|
|
88
|
+
} catch {
|
|
89
|
+
await rm(tmp, { force: true }).catch(() => {});
|
|
90
|
+
await rm(pendingPath, { force: true }).catch(() => {});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (nudges.length === 0) {
|
|
94
|
+
process.stdout.write("{}\\n");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const lines = [
|
|
98
|
+
"<slack_followup>",
|
|
99
|
+
"New Slack follow-up(s) arrived while you were working. Incorporate them before your final reply. If an ask was already answered in-thread, ack briefly instead of re-answering.",
|
|
100
|
+
"",
|
|
101
|
+
];
|
|
102
|
+
for (const nudge of nudges) {
|
|
103
|
+
const who =
|
|
104
|
+
typeof nudge.author === "string" && nudge.author !== ""
|
|
105
|
+
? nudge.author
|
|
106
|
+
: "someone";
|
|
107
|
+
lines.push("- " + who + " @ " + nudge.sourceTs + ":");
|
|
108
|
+
lines.push(
|
|
109
|
+
typeof nudge.text === "string" && nudge.text.trim() !== ""
|
|
110
|
+
? nudge.text.trim()
|
|
111
|
+
: "(empty)"
|
|
112
|
+
);
|
|
113
|
+
lines.push("");
|
|
114
|
+
}
|
|
115
|
+
lines.push("</slack_followup>");
|
|
116
|
+
process.stdout.write(
|
|
117
|
+
JSON.stringify({ additional_context: lines.join("\\n").trimEnd() }) + "\\n"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main().catch(() => {
|
|
122
|
+
process.stdout.write("{}\\n");
|
|
123
|
+
});
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
type HooksConfig = {
|
|
127
|
+
version?: number;
|
|
128
|
+
hooks?: Record<string, Array<{ command: string; failClosed?: boolean }>>;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Write carrier script + pointer files and ensure pre/postToolUse hooks invoke it.
|
|
133
|
+
* Safe to call every turn (idempotent overwrite of carrier + pointers).
|
|
134
|
+
*/
|
|
135
|
+
export async function materializeSlackNudgeCarrier(args: {
|
|
136
|
+
harnessCwd: string;
|
|
137
|
+
nudgeRootDir: string;
|
|
138
|
+
continuationKey: string;
|
|
139
|
+
}): Promise<void> {
|
|
140
|
+
if (args.continuationKey.trim() === "") {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
await mkdir(join(args.harnessCwd, ".agent-serve"), { recursive: true });
|
|
144
|
+
await mkdir(join(args.harnessCwd, ".cursor"), { recursive: true });
|
|
145
|
+
await writeFile(
|
|
146
|
+
join(args.harnessCwd, SLACK_NUDGE_ROOT_REL),
|
|
147
|
+
`${args.nudgeRootDir}\n`,
|
|
148
|
+
"utf8"
|
|
149
|
+
);
|
|
150
|
+
await writeFile(
|
|
151
|
+
join(args.harnessCwd, SLACK_NUDGE_CONTINUATION_REL),
|
|
152
|
+
`${args.continuationKey}\n`,
|
|
153
|
+
"utf8"
|
|
154
|
+
);
|
|
155
|
+
await writeFile(
|
|
156
|
+
join(args.harnessCwd, SLACK_NUDGE_CARRIER_REL),
|
|
157
|
+
SLACK_NUDGE_CARRIER_SCRIPT,
|
|
158
|
+
"utf8"
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const hooksPath = join(args.harnessCwd, ".cursor", "hooks.json");
|
|
162
|
+
let existing: HooksConfig = { version: 1, hooks: {} };
|
|
163
|
+
try {
|
|
164
|
+
existing = JSON.parse(await readFile(hooksPath, "utf8")) as HooksConfig;
|
|
165
|
+
} catch {
|
|
166
|
+
// fresh
|
|
167
|
+
}
|
|
168
|
+
const hooks = { ...(existing.hooks ?? {}) };
|
|
169
|
+
const command = `node ${SLACK_NUDGE_CARRIER_REL}`;
|
|
170
|
+
for (const step of ["preToolUse", "postToolUse"] as const) {
|
|
171
|
+
const list = [...(hooks[step] ?? [])].filter(
|
|
172
|
+
(entry) => !entry.command.includes("slack-nudge-carrier")
|
|
173
|
+
);
|
|
174
|
+
list.push({ command, failClosed: false });
|
|
175
|
+
hooks[step] = list;
|
|
176
|
+
}
|
|
177
|
+
const next: HooksConfig = {
|
|
178
|
+
version: existing.version ?? 1,
|
|
179
|
+
hooks,
|
|
180
|
+
};
|
|
181
|
+
await writeFile(hooksPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
182
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable Slack follow-up nudge store (Benny-style).
|
|
3
|
+
*
|
|
4
|
+
* Mid-turn inject and boundary resume share one exactly-once contract:
|
|
5
|
+
* pending nudges rename into delivered/ keyed by source message ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
9
|
+
import {
|
|
10
|
+
mkdir,
|
|
11
|
+
readdir,
|
|
12
|
+
readFile,
|
|
13
|
+
rename,
|
|
14
|
+
rm,
|
|
15
|
+
writeFile,
|
|
16
|
+
} from "node:fs/promises";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
export interface SlackNudgeRecord {
|
|
20
|
+
id: string;
|
|
21
|
+
continuationKey: string;
|
|
22
|
+
sourceTs: string;
|
|
23
|
+
text: string;
|
|
24
|
+
author?: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
/** Content hash for pending dedupe. */
|
|
27
|
+
contentHash: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SlackNudgeAppendInput {
|
|
31
|
+
continuationKey: string;
|
|
32
|
+
sourceTs: string;
|
|
33
|
+
text: string;
|
|
34
|
+
author?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Map an arbitrary key onto a single path segment under the store root.
|
|
39
|
+
* Rejects empty / `.` / `..` and strips remaining dots so path traversal
|
|
40
|
+
* cannot escape `<stateRoot>/slack-nudges` (or `slack-asks`).
|
|
41
|
+
*/
|
|
42
|
+
function safeKey(continuationKey: string): string {
|
|
43
|
+
const trimmed = continuationKey.trim();
|
|
44
|
+
if (trimmed === "" || trimmed === "." || trimmed === "..") {
|
|
45
|
+
return "_empty";
|
|
46
|
+
}
|
|
47
|
+
const sanitized = trimmed
|
|
48
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
|
49
|
+
.replace(/^\.+/, "")
|
|
50
|
+
.replace(/\.+/g, "_")
|
|
51
|
+
.slice(0, 180);
|
|
52
|
+
if (sanitized === "" || sanitized === "." || sanitized === "..") {
|
|
53
|
+
return "_empty";
|
|
54
|
+
}
|
|
55
|
+
return sanitized;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function contentHash(text: string): string {
|
|
59
|
+
return createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function newNudgeId(): string {
|
|
63
|
+
return `nudge_${randomBytes(8).toString("hex")}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatSlackFollowupNudgeBlock(
|
|
67
|
+
nudges: readonly SlackNudgeRecord[]
|
|
68
|
+
): string | undefined {
|
|
69
|
+
if (nudges.length === 0) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const lines = [
|
|
73
|
+
"<slack_followup>",
|
|
74
|
+
"New Slack follow-up(s) arrived while you were working. Incorporate them before your final reply. If an ask was already answered in-thread, ack briefly instead of re-answering.",
|
|
75
|
+
"",
|
|
76
|
+
];
|
|
77
|
+
for (const nudge of nudges) {
|
|
78
|
+
const who =
|
|
79
|
+
nudge.author === undefined || nudge.author === ""
|
|
80
|
+
? "someone"
|
|
81
|
+
: nudge.author;
|
|
82
|
+
lines.push(`- ${who} @ ${nudge.sourceTs}:`);
|
|
83
|
+
lines.push(nudge.text.trim() === "" ? "(empty)" : nudge.text.trim());
|
|
84
|
+
lines.push("");
|
|
85
|
+
}
|
|
86
|
+
lines.push("</slack_followup>");
|
|
87
|
+
return lines.join("\n").trimEnd();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Filesystem nudge store under `<root>/<continuation>/pending|delivered`.
|
|
92
|
+
*/
|
|
93
|
+
export class SlackNudgeStore {
|
|
94
|
+
private readonly rootDir: string;
|
|
95
|
+
|
|
96
|
+
constructor(rootDir: string) {
|
|
97
|
+
this.rootDir = rootDir;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private continuationDir(continuationKey: string): string {
|
|
101
|
+
return join(this.rootDir, safeKey(continuationKey));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private pendingDir(continuationKey: string): string {
|
|
105
|
+
return join(this.continuationDir(continuationKey), "pending");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private deliveredDir(continuationKey: string): string {
|
|
109
|
+
return join(this.continuationDir(continuationKey), "delivered");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private deliveredPath(continuationKey: string, sourceTs: string): string {
|
|
113
|
+
return join(this.deliveredDir(continuationKey), safeKey(sourceTs));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async append(input: SlackNudgeAppendInput): Promise<boolean> {
|
|
117
|
+
const continuationKey = input.continuationKey.trim();
|
|
118
|
+
const sourceTs = input.sourceTs.trim();
|
|
119
|
+
const text = input.text.trim();
|
|
120
|
+
if (continuationKey === "" || sourceTs === "" || text === "") {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (await this.isDelivered(continuationKey, sourceTs)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const pending = await this.listPending(continuationKey);
|
|
127
|
+
const hash = contentHash(text);
|
|
128
|
+
if (
|
|
129
|
+
pending.some(
|
|
130
|
+
(item) => item.sourceTs === sourceTs || item.contentHash === hash
|
|
131
|
+
)
|
|
132
|
+
) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const record: SlackNudgeRecord = {
|
|
136
|
+
id: newNudgeId(),
|
|
137
|
+
continuationKey,
|
|
138
|
+
sourceTs,
|
|
139
|
+
text,
|
|
140
|
+
...(input.author === undefined || input.author === ""
|
|
141
|
+
? {}
|
|
142
|
+
: { author: input.author }),
|
|
143
|
+
createdAt: new Date().toISOString(),
|
|
144
|
+
contentHash: hash,
|
|
145
|
+
};
|
|
146
|
+
const dir = this.pendingDir(continuationKey);
|
|
147
|
+
await mkdir(dir, { recursive: true });
|
|
148
|
+
const target = join(dir, `${record.id}.json`);
|
|
149
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
150
|
+
await writeFile(tmp, `${JSON.stringify(record)}\n`, "utf8");
|
|
151
|
+
await rename(tmp, target);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async listPending(continuationKey: string): Promise<SlackNudgeRecord[]> {
|
|
156
|
+
const dir = this.pendingDir(continuationKey);
|
|
157
|
+
let names: string[];
|
|
158
|
+
try {
|
|
159
|
+
names = await readdir(dir);
|
|
160
|
+
} catch {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
const records: SlackNudgeRecord[] = [];
|
|
164
|
+
for (const name of names) {
|
|
165
|
+
if (!name.endsWith(".json")) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const raw = await readFile(join(dir, name), "utf8");
|
|
170
|
+
const parsed = JSON.parse(raw) as SlackNudgeRecord;
|
|
171
|
+
if (
|
|
172
|
+
typeof parsed.id === "string" &&
|
|
173
|
+
typeof parsed.sourceTs === "string" &&
|
|
174
|
+
typeof parsed.text === "string"
|
|
175
|
+
) {
|
|
176
|
+
records.push(parsed);
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// Skip corrupt pending files.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
records.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
183
|
+
return records;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async isDelivered(
|
|
187
|
+
continuationKey: string,
|
|
188
|
+
sourceTs: string
|
|
189
|
+
): Promise<boolean> {
|
|
190
|
+
if (continuationKey.trim() === "" || sourceTs.trim() === "") {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
await readFile(this.deliveredPath(continuationKey, sourceTs), "utf8");
|
|
195
|
+
return true;
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async hasUndelivered(continuationKey: string): Promise<boolean> {
|
|
202
|
+
return (await this.listPending(continuationKey)).length > 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Atomically move all pending nudges to delivered and return them.
|
|
207
|
+
* Already-delivered source ts values are dropped (exactly-once).
|
|
208
|
+
*/
|
|
209
|
+
async drainForInject(continuationKey: string): Promise<SlackNudgeRecord[]> {
|
|
210
|
+
const pending = await this.listPending(continuationKey);
|
|
211
|
+
if (pending.length === 0) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
await mkdir(this.deliveredDir(continuationKey), { recursive: true });
|
|
215
|
+
const delivered: SlackNudgeRecord[] = [];
|
|
216
|
+
for (const record of pending) {
|
|
217
|
+
const pendingPath = join(
|
|
218
|
+
this.pendingDir(continuationKey),
|
|
219
|
+
`${record.id}.json`
|
|
220
|
+
);
|
|
221
|
+
if (await this.isDelivered(continuationKey, record.sourceTs)) {
|
|
222
|
+
await rm(pendingPath, { force: true });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const dest = this.deliveredPath(continuationKey, record.sourceTs);
|
|
226
|
+
const tmp = `${dest}.${process.pid}.tmp`;
|
|
227
|
+
try {
|
|
228
|
+
await writeFile(tmp, `${JSON.stringify(record)}\n`, "utf8");
|
|
229
|
+
await rename(tmp, dest);
|
|
230
|
+
await rm(pendingPath, { force: true });
|
|
231
|
+
delivered.push(record);
|
|
232
|
+
} catch {
|
|
233
|
+
await rm(tmp, { force: true }).catch(() => {});
|
|
234
|
+
// Race: another drain won — treat as delivered.
|
|
235
|
+
await rm(pendingPath, { force: true }).catch(() => {});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return delivered;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async clear(continuationKey: string): Promise<void> {
|
|
242
|
+
await rm(this.continuationDir(continuationKey), {
|
|
243
|
+
recursive: true,
|
|
244
|
+
force: true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Drain + format for tool/hook inject. */
|
|
249
|
+
async drainFormatted(continuationKey: string): Promise<string | undefined> {
|
|
250
|
+
return formatSlackFollowupNudgeBlock(
|
|
251
|
+
await this.drainForInject(continuationKey)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Durable ask claim store: one marker per `channelId:message.ts` so delayed
|
|
258
|
+
* Slack redelivery cannot start a second full answer.
|
|
259
|
+
*/
|
|
260
|
+
export class SlackAskDedupeStore {
|
|
261
|
+
private readonly rootDir: string;
|
|
262
|
+
|
|
263
|
+
constructor(rootDir: string) {
|
|
264
|
+
this.rootDir = rootDir;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private markerPath(channelId: string, messageTs: string): string {
|
|
268
|
+
return join(
|
|
269
|
+
this.rootDir,
|
|
270
|
+
`${safeKey(channelId)}__${safeKey(messageTs)}.claimed`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Returns true the first time this ask is claimed. */
|
|
275
|
+
async tryClaim(channelId: string, messageTs: string): Promise<boolean> {
|
|
276
|
+
if (channelId.trim() === "" || messageTs.trim() === "") {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
const path = this.markerPath(channelId, messageTs);
|
|
280
|
+
await mkdir(this.rootDir, { recursive: true });
|
|
281
|
+
try {
|
|
282
|
+
await writeFile(path, `${new Date().toISOString()}\n`, {
|
|
283
|
+
encoding: "utf8",
|
|
284
|
+
flag: "wx",
|
|
285
|
+
});
|
|
286
|
+
return true;
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Drop a claim so a failed delivery can be retried. */
|
|
293
|
+
async release(channelId: string, messageTs: string): Promise<void> {
|
|
294
|
+
if (channelId.trim() === "" || messageTs.trim() === "") {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
await rm(this.markerPath(channelId, messageTs), { force: true });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async wasClaimed(channelId: string, messageTs: string): Promise<boolean> {
|
|
301
|
+
try {
|
|
302
|
+
await readFile(this.markerPath(channelId, messageTs), "utf8");
|
|
303
|
+
return true;
|
|
304
|
+
} catch {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -35,9 +35,8 @@ import {
|
|
|
35
35
|
} from "./cursor-account.js";
|
|
36
36
|
import {
|
|
37
37
|
buildDefaultEvents,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
defaultOnDirectMessage,
|
|
38
|
+
createDefaultOnChannelPost,
|
|
39
|
+
createDefaultOnInbound,
|
|
41
40
|
} from "./defaults.js";
|
|
42
41
|
import {
|
|
43
42
|
dispatchInboundFromEventBody,
|
|
@@ -130,9 +129,12 @@ export function slackChannel<const Name extends string = string>(
|
|
|
130
129
|
? { botToken: accountTransport }
|
|
131
130
|
: (options.credentials ?? credentialsFromEnv(options.envPrefix));
|
|
132
131
|
const envKeys = slackEnvKeys(options.envPrefix);
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
132
|
+
const loadingMessages = options.loadingMessages;
|
|
133
|
+
const defaultInbound = createDefaultOnInbound(loadingMessages);
|
|
134
|
+
const onAppMention = options.onAppMention ?? defaultInbound;
|
|
135
|
+
const onDirectMessage = options.onDirectMessage ?? defaultInbound;
|
|
136
|
+
const onChannelPost =
|
|
137
|
+
options.onChannelPost ?? createDefaultOnChannelPost(loadingMessages);
|
|
136
138
|
const engagement = options.engagement ?? {};
|
|
137
139
|
const mentionsEnabled = engagement.mentions !== false;
|
|
138
140
|
const directMessagesEnabled = engagement.directMessages !== false;
|
|
@@ -167,6 +169,7 @@ export function slackChannel<const Name extends string = string>(
|
|
|
167
169
|
thinkingSteps: options.thinkingSteps,
|
|
168
170
|
suggestedPrompts: options.suggestedPrompts,
|
|
169
171
|
suggestedPromptsTitle: options.suggestedPromptsTitle,
|
|
172
|
+
loadingMessages,
|
|
170
173
|
getLog,
|
|
171
174
|
})
|
|
172
175
|
);
|
|
@@ -183,6 +186,12 @@ export function slackChannel<const Name extends string = string>(
|
|
|
183
186
|
send: SendMessageFn;
|
|
184
187
|
waitUntil: (promise: Promise<unknown>) => void;
|
|
185
188
|
host: HostContext;
|
|
189
|
+
hasContinuationSession?: (continuationToken: string) => Promise<boolean>;
|
|
190
|
+
isContinuationBusy?: (continuationToken: string) => Promise<boolean>;
|
|
191
|
+
getContinuationLastBotMessageTs?: (
|
|
192
|
+
continuationToken: string
|
|
193
|
+
) => Promise<string | undefined>;
|
|
194
|
+
interruptContinuation?: (continuationToken: string) => Promise<boolean>;
|
|
186
195
|
}): SlackInboundDispatchDeps {
|
|
187
196
|
return {
|
|
188
197
|
send: args.send,
|
|
@@ -195,6 +204,21 @@ export function slackChannel<const Name extends string = string>(
|
|
|
195
204
|
log: getLog(),
|
|
196
205
|
mentionsEnabled,
|
|
197
206
|
directMessagesEnabled,
|
|
207
|
+
...(args.hasContinuationSession === undefined
|
|
208
|
+
? {}
|
|
209
|
+
: { hasContinuationSession: args.hasContinuationSession }),
|
|
210
|
+
...(args.isContinuationBusy === undefined
|
|
211
|
+
? {}
|
|
212
|
+
: { isContinuationBusy: args.isContinuationBusy }),
|
|
213
|
+
...(args.getContinuationLastBotMessageTs === undefined
|
|
214
|
+
? {}
|
|
215
|
+
: {
|
|
216
|
+
getContinuationLastBotMessageTs:
|
|
217
|
+
args.getContinuationLastBotMessageTs,
|
|
218
|
+
}),
|
|
219
|
+
...(args.interruptContinuation === undefined
|
|
220
|
+
? {}
|
|
221
|
+
: { interruptContinuation: args.interruptContinuation }),
|
|
198
222
|
...(channelWatch === undefined ? {} : { channelWatch }),
|
|
199
223
|
};
|
|
200
224
|
}
|
|
@@ -237,6 +261,7 @@ export function slackChannel<const Name extends string = string>(
|
|
|
237
261
|
|
|
238
262
|
return send(input.message, {
|
|
239
263
|
auth: input.auth,
|
|
264
|
+
admission: "coalesce",
|
|
240
265
|
continuationToken: slackContinuationKey(channelId, threadTs),
|
|
241
266
|
state: emptySlackChannelState({
|
|
242
267
|
channelId,
|
|
@@ -271,12 +296,15 @@ export function slackChannel<const Name extends string = string>(
|
|
|
271
296
|
if (channelPosts !== undefined) {
|
|
272
297
|
const allowlist = new SlackChannelAllowlist(channelPosts.allow);
|
|
273
298
|
let botUserId: string | undefined;
|
|
299
|
+
let botId: string | undefined;
|
|
274
300
|
try {
|
|
275
301
|
const { WebClient } = await import("@slack/web-api");
|
|
276
302
|
const client = new WebClient(botToken);
|
|
277
303
|
const identity = await client.auth.test();
|
|
278
304
|
botUserId =
|
|
279
305
|
typeof identity.user_id === "string" ? identity.user_id : undefined;
|
|
306
|
+
botId =
|
|
307
|
+
typeof identity.bot_id === "string" ? identity.bot_id : undefined;
|
|
280
308
|
const names = allowlist.unresolvedNames();
|
|
281
309
|
if (names.length > 0) {
|
|
282
310
|
allowlist.applyResolvedNames(
|
|
@@ -288,31 +316,62 @@ export function slackChannel<const Name extends string = string>(
|
|
|
288
316
|
error: error instanceof Error ? error.message : String(error),
|
|
289
317
|
});
|
|
290
318
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
319
|
+
if (
|
|
320
|
+
channelPosts.includeBotPosts === true &&
|
|
321
|
+
botUserId === undefined &&
|
|
322
|
+
botId === undefined
|
|
323
|
+
) {
|
|
324
|
+
getLog().error("channel watch disabled", {
|
|
325
|
+
reason:
|
|
326
|
+
"includeBotPosts requires bot identity from auth.test (loop safety)",
|
|
327
|
+
});
|
|
328
|
+
} else {
|
|
329
|
+
channelWatch = createChannelPostWatcher({
|
|
330
|
+
options: channelPosts,
|
|
331
|
+
allowlist,
|
|
332
|
+
getBotUserId: () => botUserId,
|
|
333
|
+
getBotId: () => botId,
|
|
334
|
+
log: getLog(),
|
|
335
|
+
dispatch: (message, eventId) => {
|
|
336
|
+
args.waitUntil(
|
|
337
|
+
dispatchInboundMessage({
|
|
338
|
+
kind: "channel_post",
|
|
339
|
+
message,
|
|
340
|
+
handler: onChannelPost,
|
|
341
|
+
send: args.send,
|
|
342
|
+
credentials,
|
|
343
|
+
host: args.host,
|
|
344
|
+
log: getLog(),
|
|
345
|
+
...(args.hasContinuationSession === undefined
|
|
346
|
+
? {}
|
|
347
|
+
: {
|
|
348
|
+
hasContinuationSession: args.hasContinuationSession,
|
|
349
|
+
}),
|
|
350
|
+
...(args.isContinuationBusy === undefined
|
|
351
|
+
? {}
|
|
352
|
+
: { isContinuationBusy: args.isContinuationBusy }),
|
|
353
|
+
...(args.getContinuationLastBotMessageTs === undefined
|
|
354
|
+
? {}
|
|
355
|
+
: {
|
|
356
|
+
getContinuationLastBotMessageTs:
|
|
357
|
+
args.getContinuationLastBotMessageTs,
|
|
358
|
+
}),
|
|
359
|
+
...(args.interruptContinuation === undefined
|
|
360
|
+
? {}
|
|
361
|
+
: {
|
|
362
|
+
interruptContinuation: args.interruptContinuation,
|
|
363
|
+
}),
|
|
364
|
+
...(eventId === undefined ? {} : { eventId }),
|
|
365
|
+
})
|
|
366
|
+
);
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
getLog().info("channel watch enabled", {
|
|
370
|
+
channels: channelPosts.allow.join(","),
|
|
371
|
+
posts: channelPosts.posts ?? "top-level",
|
|
372
|
+
debounce_ms: channelPosts.debounceMs ?? 0,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
316
375
|
}
|
|
317
376
|
|
|
318
377
|
const socketOptions: SlackSocketModeOptions = {
|
|
@@ -325,6 +384,21 @@ export function slackChannel<const Name extends string = string>(
|
|
|
325
384
|
send: args.send,
|
|
326
385
|
waitUntil: args.waitUntil,
|
|
327
386
|
host: args.host,
|
|
387
|
+
...(args.hasContinuationSession === undefined
|
|
388
|
+
? {}
|
|
389
|
+
: { hasContinuationSession: args.hasContinuationSession }),
|
|
390
|
+
...(args.isContinuationBusy === undefined
|
|
391
|
+
? {}
|
|
392
|
+
: { isContinuationBusy: args.isContinuationBusy }),
|
|
393
|
+
...(args.getContinuationLastBotMessageTs === undefined
|
|
394
|
+
? {}
|
|
395
|
+
: {
|
|
396
|
+
getContinuationLastBotMessageTs:
|
|
397
|
+
args.getContinuationLastBotMessageTs,
|
|
398
|
+
}),
|
|
399
|
+
...(args.interruptContinuation === undefined
|
|
400
|
+
? {}
|
|
401
|
+
: { interruptContinuation: args.interruptContinuation }),
|
|
328
402
|
})
|
|
329
403
|
);
|
|
330
404
|
},
|
|
@@ -366,6 +440,7 @@ export function slackChannel<const Name extends string = string>(
|
|
|
366
440
|
channelWatch = undefined;
|
|
367
441
|
await socketHandle?.stop();
|
|
368
442
|
socketHandle = null;
|
|
443
|
+
await options.onStop?.();
|
|
369
444
|
},
|
|
370
445
|
});
|
|
371
446
|
|