@alejandroroman/agent-kit 0.1.4 → 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/dist/agent/loop.js +213 -111
- package/dist/agent/types.d.ts +2 -0
- package/dist/api/errors.d.ts +3 -0
- package/dist/api/errors.js +37 -0
- package/dist/api/events.d.ts +5 -0
- package/dist/api/events.js +28 -0
- package/dist/api/router.js +10 -0
- package/dist/api/traces.d.ts +3 -0
- package/dist/api/traces.js +35 -0
- package/dist/api/types.d.ts +2 -0
- package/dist/bootstrap.d.ts +3 -1
- package/dist/bootstrap.js +26 -7
- package/dist/cli/chat.js +3 -1
- package/dist/cli/claude-md-template.d.ts +5 -0
- package/dist/cli/claude-md-template.js +220 -0
- package/dist/cli/config-writer.js +3 -0
- package/dist/cli/env.d.ts +14 -0
- package/dist/cli/env.js +68 -0
- package/dist/cli/init.js +10 -0
- package/dist/cli/setup-agent/index.js +61 -18
- package/dist/cli/slack-setup.d.ts +6 -0
- package/dist/cli/slack-setup.js +234 -0
- package/dist/cli/start.js +65 -16
- package/dist/cli/ui.d.ts +2 -0
- package/dist/cli/ui.js +4 -1
- package/dist/cli/whats-new.d.ts +1 -0
- package/dist/cli/whats-new.js +69 -0
- package/dist/cli.js +14 -0
- package/dist/config/resolve.d.ts +1 -0
- package/dist/config/resolve.js +1 -0
- package/dist/config/schema.d.ts +2 -0
- package/dist/config/schema.js +1 -0
- package/dist/config/writer.d.ts +18 -0
- package/dist/config/writer.js +85 -0
- package/dist/cron/scheduler.d.ts +4 -1
- package/dist/cron/scheduler.js +99 -52
- package/dist/gateways/slack/client.d.ts +1 -0
- package/dist/gateways/slack/client.js +9 -0
- package/dist/gateways/slack/handler.js +2 -1
- package/dist/gateways/slack/index.js +75 -29
- package/dist/gateways/slack/listener.d.ts +8 -1
- package/dist/gateways/slack/listener.js +36 -10
- package/dist/heartbeat/runner.js +99 -82
- package/dist/llm/anthropic.d.ts +1 -0
- package/dist/llm/anthropic.js +11 -2
- package/dist/llm/fallback.js +34 -2
- package/dist/llm/openai.d.ts +2 -0
- package/dist/llm/openai.js +33 -2
- package/dist/llm/types.d.ts +16 -2
- package/dist/llm/types.js +9 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +11 -0
- package/dist/media/sanitize.d.ts +5 -0
- package/dist/media/sanitize.js +53 -0
- package/dist/multi/spawn.js +29 -10
- package/dist/session/compaction.js +3 -1
- package/dist/session/prune-images.d.ts +9 -0
- package/dist/session/prune-images.js +42 -0
- package/dist/skills/activate.d.ts +6 -0
- package/dist/skills/activate.js +72 -27
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/telemetry/db.d.ts +63 -0
- package/dist/telemetry/db.js +193 -0
- package/dist/telemetry/index.d.ts +17 -0
- package/dist/telemetry/index.js +82 -0
- package/dist/telemetry/sanitize.d.ts +6 -0
- package/dist/telemetry/sanitize.js +48 -0
- package/dist/telemetry/sqlite-processor.d.ts +11 -0
- package/dist/telemetry/sqlite-processor.js +108 -0
- package/dist/telemetry/types.d.ts +30 -0
- package/dist/telemetry/types.js +31 -0
- package/dist/tools/builtin/index.d.ts +2 -0
- package/dist/tools/builtin/index.js +2 -0
- package/dist/tools/builtin/self-config.d.ts +4 -0
- package/dist/tools/builtin/self-config.js +182 -0
- package/package.json +10 -2
package/dist/cron/scheduler.js
CHANGED
|
@@ -4,10 +4,13 @@ import { setupAgentSession } from "../agent/setup.js";
|
|
|
4
4
|
import { resolveAgent, resolveWebSearch } from "../config/resolve.js";
|
|
5
5
|
import { createBuiltinRegistry } from "../tools/builtin/index.js";
|
|
6
6
|
import { registerSpawnWrappers } from "../tools/builtin/spawn.js";
|
|
7
|
+
import { createUpdateAgentConfigTool, createManageCronTool } from "../tools/builtin/self-config.js";
|
|
7
8
|
import * as path from "path";
|
|
8
|
-
import { createActivateSkillTool } from "../skills/index.js";
|
|
9
|
+
import { createActivateSkillTool, preActivateSkills } from "../skills/index.js";
|
|
9
10
|
import { createLogger } from "../logger.js";
|
|
10
11
|
import { dateContext } from "../text.js";
|
|
12
|
+
import { context, trace, SpanStatusCode } from "@opentelemetry/api";
|
|
13
|
+
import { getTracer, ATTR } from "../telemetry/index.js";
|
|
11
14
|
const log = createLogger("cron");
|
|
12
15
|
export class CronScheduler {
|
|
13
16
|
tasks = [];
|
|
@@ -17,13 +20,15 @@ export class CronScheduler {
|
|
|
17
20
|
dataDir;
|
|
18
21
|
skillsDir;
|
|
19
22
|
usageStore;
|
|
20
|
-
|
|
23
|
+
configWriter;
|
|
24
|
+
constructor(config, toolRegistry, agentRegistry, dataDir, skillsDir, usageStore, configWriter) {
|
|
21
25
|
this.config = config;
|
|
22
26
|
this.toolRegistry = toolRegistry;
|
|
23
27
|
this.agentRegistry = agentRegistry;
|
|
24
28
|
this.dataDir = dataDir;
|
|
25
29
|
this.skillsDir = skillsDir ?? path.join(process.cwd(), "skills");
|
|
26
30
|
this.usageStore = usageStore;
|
|
31
|
+
this.configWriter = configWriter;
|
|
27
32
|
}
|
|
28
33
|
getJobs() {
|
|
29
34
|
return this.config.cron;
|
|
@@ -32,59 +37,96 @@ export class CronScheduler {
|
|
|
32
37
|
const job = this.config.cron.find((j) => j.id === jobId);
|
|
33
38
|
if (!job)
|
|
34
39
|
return undefined;
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
webSearch: resolveWebSearch(job.agent, this.config),
|
|
40
|
+
const tracer = getTracer("cron");
|
|
41
|
+
const span = tracer.startSpan("source.cron", {
|
|
42
|
+
attributes: {
|
|
43
|
+
[ATTR.JOB_ID]: job.id,
|
|
44
|
+
[ATTR.AGENT]: job.agent,
|
|
45
|
+
[ATTR.SCHEDULE]: job.schedule,
|
|
46
|
+
[ATTR.SOURCE]: "cron",
|
|
47
|
+
},
|
|
44
48
|
});
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
const spanCtx = trace.setSpan(context.active(), span);
|
|
50
|
+
try {
|
|
51
|
+
return await context.with(spanCtx, async () => {
|
|
52
|
+
// Resolve sandbox for this job's agent
|
|
53
|
+
const agentDef = this.config.agents[job.agent];
|
|
54
|
+
const sandbox = agentDef?.sandbox ?? this.config.defaults.sandbox;
|
|
55
|
+
const memoryConfig = this.config.defaults.memory;
|
|
56
|
+
const jobRegistry = createBuiltinRegistry({
|
|
57
|
+
allowedCommands: sandbox?.allowedCommands,
|
|
58
|
+
allowedPaths: sandbox?.allowedPaths,
|
|
59
|
+
memoryConfig,
|
|
60
|
+
webSearch: resolveWebSearch(job.agent, this.config),
|
|
61
|
+
});
|
|
62
|
+
const resolved = resolveAgent(job.agent, this.config, jobRegistry, this.skillsDir);
|
|
63
|
+
// Skills setup for this job
|
|
64
|
+
const promptFragments = [];
|
|
65
|
+
let skillsIndex = "";
|
|
66
|
+
if (resolved.skills.length > 0) {
|
|
67
|
+
const ctx = {
|
|
68
|
+
manifests: resolved.skills,
|
|
69
|
+
skillsDir: this.skillsDir,
|
|
70
|
+
toolRegistry: jobRegistry,
|
|
71
|
+
promptFragments,
|
|
72
|
+
activatedSkills: new Set(),
|
|
73
|
+
};
|
|
74
|
+
const activateTool = createActivateSkillTool(ctx);
|
|
75
|
+
jobRegistry.register(activateTool);
|
|
76
|
+
if (resolved.autoActivateSkills) {
|
|
77
|
+
skillsIndex = await preActivateSkills(ctx, activateTool, log);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
skillsIndex = "\n\nYou have the following skills available:\n\n"
|
|
81
|
+
+ resolved.skills.map((s) => `- **${s.name}**: ${s.description}`).join("\n")
|
|
82
|
+
+ "\n\nTo use a skill, call the `activate_skill` tool with the skill name.";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Spawn wrapper registration
|
|
86
|
+
if (resolved.canSpawn.length > 0) {
|
|
87
|
+
registerSpawnWrappers(resolved.canSpawn, this.config, this.agentRegistry, jobRegistry, this.usageStore);
|
|
88
|
+
}
|
|
89
|
+
// Self-config tool registration
|
|
90
|
+
if (this.configWriter && agentDef) {
|
|
91
|
+
if (agentDef.tools.includes("update_agent_config")) {
|
|
92
|
+
jobRegistry.register(createUpdateAgentConfigTool(job.agent, this.configWriter));
|
|
93
|
+
}
|
|
94
|
+
if (agentDef.tools.includes("manage_cron")) {
|
|
95
|
+
jobRegistry.register(createManageCronTool(job.agent, this.configWriter));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const sessionId = `cron-${job.id}`;
|
|
99
|
+
const { soul, session } = setupAgentSession(this.dataDir, job.agent, sessionId);
|
|
100
|
+
const userMsg = { role: "user", content: job.prompt };
|
|
101
|
+
session.append(userMsg);
|
|
102
|
+
const systemPrompt = [soul, dateContext(), skillsIndex, ...promptFragments]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join("\n\n") || undefined;
|
|
105
|
+
const result = await runAgentLoop([userMsg], {
|
|
106
|
+
model: resolved.model,
|
|
107
|
+
fallbacks: resolved.fallbacks,
|
|
108
|
+
systemPrompt,
|
|
109
|
+
toolRegistry: jobRegistry,
|
|
110
|
+
maxIterations: resolved.maxIterations,
|
|
111
|
+
compactionThreshold: resolved.compactionThreshold,
|
|
112
|
+
maxToolResultSize: resolved.maxToolResultSize,
|
|
113
|
+
agentName: job.agent,
|
|
114
|
+
usageStore: this.usageStore,
|
|
115
|
+
source: "cron",
|
|
116
|
+
});
|
|
117
|
+
const lastMsg = result.messages[result.messages.length - 1];
|
|
118
|
+
session.append(lastMsg);
|
|
119
|
+
return result;
|
|
120
|
+
});
|
|
61
121
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
122
|
+
catch (err) {
|
|
123
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
|
|
124
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
span.end();
|
|
65
129
|
}
|
|
66
|
-
const sessionId = `cron-${job.id}`;
|
|
67
|
-
const { soul, session } = setupAgentSession(this.dataDir, job.agent, sessionId);
|
|
68
|
-
const userMsg = { role: "user", content: job.prompt };
|
|
69
|
-
session.append(userMsg);
|
|
70
|
-
const systemPrompt = [soul, dateContext(), skillsIndex, ...promptFragments]
|
|
71
|
-
.filter(Boolean)
|
|
72
|
-
.join("\n\n") || undefined;
|
|
73
|
-
const result = await runAgentLoop([userMsg], {
|
|
74
|
-
model: resolved.model,
|
|
75
|
-
fallbacks: resolved.fallbacks,
|
|
76
|
-
systemPrompt,
|
|
77
|
-
toolRegistry: jobRegistry,
|
|
78
|
-
maxIterations: resolved.maxIterations,
|
|
79
|
-
compactionThreshold: resolved.compactionThreshold,
|
|
80
|
-
maxToolResultSize: resolved.maxToolResultSize,
|
|
81
|
-
agentName: job.agent,
|
|
82
|
-
usageStore: this.usageStore,
|
|
83
|
-
source: "cron",
|
|
84
|
-
});
|
|
85
|
-
const lastMsg = result.messages[result.messages.length - 1];
|
|
86
|
-
session.append(lastMsg);
|
|
87
|
-
return result;
|
|
88
130
|
}
|
|
89
131
|
start(callbacks) {
|
|
90
132
|
for (const job of this.config.cron) {
|
|
@@ -107,6 +149,11 @@ export class CronScheduler {
|
|
|
107
149
|
this.tasks.push(task);
|
|
108
150
|
}
|
|
109
151
|
}
|
|
152
|
+
reload(newConfig) {
|
|
153
|
+
this.stop();
|
|
154
|
+
this.config = newConfig;
|
|
155
|
+
log.info({ jobs: newConfig.cron.length }, "config reloaded");
|
|
156
|
+
}
|
|
110
157
|
stop() {
|
|
111
158
|
for (const task of this.tasks)
|
|
112
159
|
task.stop();
|
|
@@ -8,6 +8,7 @@ export interface SlackClient {
|
|
|
8
8
|
postMessage(channelId: string, text: string, blocks?: unknown[], threadTs?: string): Promise<{
|
|
9
9
|
ok: boolean;
|
|
10
10
|
}>;
|
|
11
|
+
downloadFile(urlPrivateDownload: string): Promise<Buffer>;
|
|
11
12
|
app: import("@slack/bolt").App;
|
|
12
13
|
}
|
|
13
14
|
export declare function createSlackClient(options: SlackClientOptions): SlackClient;
|
|
@@ -28,6 +28,15 @@ export function createSlackClient(options) {
|
|
|
28
28
|
async stop() {
|
|
29
29
|
await app.stop();
|
|
30
30
|
},
|
|
31
|
+
async downloadFile(urlPrivateDownload) {
|
|
32
|
+
const response = await fetch(urlPrivateDownload, {
|
|
33
|
+
headers: { Authorization: `Bearer ${options.botToken}` },
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`Failed to download Slack file: ${response.status} ${response.statusText}`);
|
|
37
|
+
}
|
|
38
|
+
return Buffer.from(await response.arrayBuffer());
|
|
39
|
+
},
|
|
31
40
|
async postMessage(channelId, text, blocks, threadTs) {
|
|
32
41
|
const result = await app.client.chat.postMessage({
|
|
33
42
|
channel: channelId,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { extractUserText } from "../../llm/types.js";
|
|
2
3
|
import { markdownToBlocks, markdownToPlainText } from "./format.js";
|
|
3
4
|
import { BudgetStatusSchema } from "./types.js";
|
|
4
5
|
import { createLogger } from "../../logger.js";
|
|
@@ -16,7 +17,7 @@ function findBudgetToolResults(messages) {
|
|
|
16
17
|
const results = [];
|
|
17
18
|
for (const msg of messages) {
|
|
18
19
|
if (msg.role === "tool_result" && toolCallNames.get(msg.tool_call_id) === "get_budget_status") {
|
|
19
|
-
results.push(msg.content);
|
|
20
|
+
results.push(extractUserText(msg.content));
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
return results;
|
|
@@ -4,7 +4,25 @@ import { createSlackListener } from "./listener.js";
|
|
|
4
4
|
import { createThreadSessionManager } from "./sessions.js";
|
|
5
5
|
import { markdownToBlocks, markdownToPlainText } from "./format.js";
|
|
6
6
|
import { createLogger } from "../../logger.js";
|
|
7
|
+
import { context, trace, SpanStatusCode } from "@opentelemetry/api";
|
|
8
|
+
import { getTracer, ATTR } from "../../telemetry/index.js";
|
|
7
9
|
const log = createLogger("slack");
|
|
10
|
+
function buildUserMessage(msg) {
|
|
11
|
+
if (!msg.files?.length) {
|
|
12
|
+
return { role: "user", content: msg.userText };
|
|
13
|
+
}
|
|
14
|
+
const blocks = [];
|
|
15
|
+
if (msg.userText) {
|
|
16
|
+
blocks.push({ type: "text", text: msg.userText });
|
|
17
|
+
}
|
|
18
|
+
for (const file of msg.files) {
|
|
19
|
+
blocks.push({
|
|
20
|
+
type: "image",
|
|
21
|
+
source: { type: "base64", media_type: file.mimetype, data: file.base64 },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return { role: "user", content: blocks };
|
|
25
|
+
}
|
|
8
26
|
export function createSlackGateway(config, options) {
|
|
9
27
|
const botToken = options?.botToken ?? process.env.SLACK_BOT_TOKEN;
|
|
10
28
|
const appToken = options?.appToken ?? process.env.SLACK_APP_TOKEN;
|
|
@@ -40,37 +58,65 @@ export function createSlackGateway(config, options) {
|
|
|
40
58
|
async function processMessage(msg) {
|
|
41
59
|
if (!executor)
|
|
42
60
|
return;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
await client.postMessage(msg.channelId, fallback, blocks, msg.threadTs);
|
|
58
|
-
log.info({ agent: msg.agentName, thread: msg.threadTs }, "reply posted");
|
|
59
|
-
}
|
|
60
|
-
catch (err) {
|
|
61
|
-
log.error({ err, agent: msg.agentName, thread: msg.threadTs }, "error handling inbound message");
|
|
62
|
-
// Roll back user message on failure to avoid malformed conversation history
|
|
63
|
-
const current = sessions.get(msg.threadTs);
|
|
64
|
-
if (current.length > 0 && current[current.length - 1].role === "user") {
|
|
65
|
-
sessions.set(msg.threadTs, current.slice(0, -1));
|
|
66
|
-
}
|
|
61
|
+
const tracer = getTracer("slack");
|
|
62
|
+
const span = tracer.startSpan("source.slack", {
|
|
63
|
+
attributes: {
|
|
64
|
+
[ATTR.CHANNEL]: msg.channelId,
|
|
65
|
+
[ATTR.AGENT]: msg.agentName,
|
|
66
|
+
[ATTR.THREAD_TS]: msg.threadTs,
|
|
67
|
+
[ATTR.SOURCE]: "slack",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const spanCtx = trace.setSpan(context.active(), span);
|
|
71
|
+
await context.with(spanCtx, async () => {
|
|
72
|
+
const userMsg = buildUserMessage(msg);
|
|
73
|
+
sessions.append(msg.threadTs, userMsg);
|
|
74
|
+
const messages = sessions.get(msg.threadTs);
|
|
67
75
|
try {
|
|
68
|
-
|
|
76
|
+
const start = Date.now();
|
|
77
|
+
log.info({ agent: msg.agentName, thread: msg.threadTs, history: messages.length }, "executing agent");
|
|
78
|
+
const result = await executor(msg.agentName, messages);
|
|
79
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
80
|
+
log.info({ agent: msg.agentName, thread: msg.threadTs, elapsed, tokens: result.usage.inputTokens + result.usage.outputTokens }, "agent completed");
|
|
81
|
+
// Replace session with full message history from agent
|
|
82
|
+
sessions.set(msg.threadTs, result.messages);
|
|
83
|
+
const text = result.text || "(completed with no text response)";
|
|
84
|
+
const blocks = markdownToBlocks(text);
|
|
85
|
+
const fallback = markdownToPlainText(text);
|
|
86
|
+
const postSpan = tracer.startSpan("slack.post", {}, spanCtx);
|
|
87
|
+
try {
|
|
88
|
+
await client.postMessage(msg.channelId, fallback, blocks, msg.threadTs);
|
|
89
|
+
log.info({ agent: msg.agentName, thread: msg.threadTs }, "reply posted");
|
|
90
|
+
}
|
|
91
|
+
catch (postErr) {
|
|
92
|
+
postSpan.setStatus({ code: SpanStatusCode.ERROR, message: String(postErr) });
|
|
93
|
+
postSpan.recordException(postErr instanceof Error ? postErr : new Error(String(postErr)));
|
|
94
|
+
throw postErr;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
postSpan.end();
|
|
98
|
+
}
|
|
69
99
|
}
|
|
70
|
-
catch (
|
|
71
|
-
|
|
100
|
+
catch (err) {
|
|
101
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
|
|
102
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
103
|
+
log.error({ err, agent: msg.agentName, thread: msg.threadTs }, "error handling inbound message");
|
|
104
|
+
// Roll back user message on failure to avoid malformed conversation history
|
|
105
|
+
const current = sessions.get(msg.threadTs);
|
|
106
|
+
if (current.length > 0 && current[current.length - 1].role === "user") {
|
|
107
|
+
sessions.set(msg.threadTs, current.slice(0, -1));
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
await client.postMessage(msg.channelId, "Sorry, I encountered an error processing your message. Please try again in a moment.", undefined, msg.threadTs);
|
|
111
|
+
}
|
|
112
|
+
catch (postErr) {
|
|
113
|
+
log.error({ err: postErr, thread: msg.threadTs }, "failed to send error reply");
|
|
114
|
+
}
|
|
72
115
|
}
|
|
73
|
-
|
|
116
|
+
finally {
|
|
117
|
+
span.end();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
74
120
|
}
|
|
75
121
|
// Eviction timer handle
|
|
76
122
|
let evictionTimer;
|
|
@@ -78,7 +124,7 @@ export function createSlackGateway(config, options) {
|
|
|
78
124
|
async start() {
|
|
79
125
|
await client.start();
|
|
80
126
|
if (executor) {
|
|
81
|
-
createSlackListener(client.app, gatewayConfig, handleInbound);
|
|
127
|
+
createSlackListener(client.app, gatewayConfig, handleInbound, client);
|
|
82
128
|
// Evict stale threads every hour
|
|
83
129
|
evictionTimer = setInterval(() => {
|
|
84
130
|
sessions.evict();
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { App } from "@slack/bolt";
|
|
2
2
|
import type { SlackGatewayConfig } from "./types.js";
|
|
3
|
+
import type { SlackClient } from "./client.js";
|
|
4
|
+
export interface InboundFile {
|
|
5
|
+
mimetype: string;
|
|
6
|
+
name: string;
|
|
7
|
+
base64: string;
|
|
8
|
+
}
|
|
3
9
|
export interface InboundMessage {
|
|
4
10
|
agentName: string;
|
|
5
11
|
userText: string;
|
|
6
12
|
threadTs: string;
|
|
7
13
|
channelId: string;
|
|
14
|
+
files?: InboundFile[];
|
|
8
15
|
}
|
|
9
16
|
export type OnMessageCallback = (message: InboundMessage) => Promise<void>;
|
|
10
|
-
export declare function createSlackListener(app: App, config: SlackGatewayConfig, onMessage: OnMessageCallback): void;
|
|
17
|
+
export declare function createSlackListener(app: App, config: SlackGatewayConfig, onMessage: OnMessageCallback, client?: SlackClient): void;
|
|
@@ -1,33 +1,59 @@
|
|
|
1
|
+
import { sanitizeImage } from "../../media/sanitize.js";
|
|
1
2
|
import { createLogger } from "../../logger.js";
|
|
2
3
|
const log = createLogger("slack:listener");
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
const IMAGE_MIMES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
5
|
+
export function createSlackListener(app, config, onMessage, client) {
|
|
5
6
|
const channelToAgent = new Map();
|
|
6
7
|
for (const [agentName, binding] of Object.entries(config.channels)) {
|
|
7
8
|
channelToAgent.set(binding.channelId, agentName);
|
|
8
9
|
}
|
|
9
10
|
app.event("message", async (payload) => {
|
|
10
|
-
// Handle both Bolt's { event } wrapper and direct event (tests)
|
|
11
11
|
const msg = payload.event ?? payload;
|
|
12
12
|
// Filter out bot messages and subtypes (edits, deletes, etc.)
|
|
13
|
-
|
|
13
|
+
// Allow file_share through so we can process image attachments
|
|
14
|
+
if (msg.bot_id)
|
|
14
15
|
return;
|
|
15
|
-
if (
|
|
16
|
+
if (msg.subtype && msg.subtype !== "file_share")
|
|
17
|
+
return;
|
|
18
|
+
const hasText = msg.text && typeof msg.text === "string";
|
|
19
|
+
const hasFiles = Array.isArray(msg.files) && msg.files.length > 0;
|
|
20
|
+
if (!hasText && !hasFiles)
|
|
16
21
|
return;
|
|
17
22
|
const channelId = msg.channel;
|
|
18
23
|
const agentName = channelToAgent.get(channelId);
|
|
19
24
|
if (!agentName)
|
|
20
25
|
return;
|
|
21
|
-
// Use thread_ts if in a thread, otherwise use ts as the thread parent
|
|
22
26
|
const threadTs = msg.thread_ts ?? msg.ts;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
let files;
|
|
28
|
+
if (hasFiles && client) {
|
|
29
|
+
files = [];
|
|
30
|
+
for (const file of msg.files) {
|
|
31
|
+
if (!IMAGE_MIMES.has(file.mimetype)) {
|
|
32
|
+
log.debug({ name: file.name, mimetype: file.mimetype }, "skipping non-image file");
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const buffer = await client.downloadFile(file.url_private_download);
|
|
37
|
+
const sanitized = await sanitizeImage(buffer, file.mimetype);
|
|
38
|
+
files.push({
|
|
39
|
+
mimetype: sanitized.mediaType,
|
|
40
|
+
name: file.name ?? "image",
|
|
41
|
+
base64: sanitized.base64,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
log.warn({ err, name: file.name }, "failed to download/sanitize image");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (files.length === 0)
|
|
49
|
+
files = undefined;
|
|
50
|
+
}
|
|
26
51
|
onMessage({
|
|
27
52
|
agentName,
|
|
28
|
-
userText: msg.text,
|
|
53
|
+
userText: msg.text ?? "",
|
|
29
54
|
threadTs,
|
|
30
55
|
channelId,
|
|
56
|
+
files,
|
|
31
57
|
}).catch((err) => {
|
|
32
58
|
log.error({ err, channelId }, "unhandled error processing message");
|
|
33
59
|
});
|