@vellumai/assistant 0.4.11 → 0.4.13

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.
Files changed (111) hide show
  1. package/ARCHITECTURE.md +401 -385
  2. package/package.json +1 -1
  3. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
  4. package/src/__tests__/registry.test.ts +235 -187
  5. package/src/__tests__/secure-keys.test.ts +27 -0
  6. package/src/__tests__/session-agent-loop.test.ts +521 -256
  7. package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
  8. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  9. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  10. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  11. package/src/__tests__/skills.test.ts +334 -276
  12. package/src/__tests__/slack-skill.test.ts +124 -0
  13. package/src/__tests__/starter-task-flow.test.ts +7 -17
  14. package/src/agent/loop.ts +10 -3
  15. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
  16. package/src/config/bundled-skills/doordash/SKILL.md +171 -0
  17. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
  18. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
  19. package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
  20. package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
  21. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
  22. package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
  23. package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
  24. package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
  25. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
  26. package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
  27. package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
  28. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
  29. package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
  30. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
  31. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
  32. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
  33. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
  34. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
  35. package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
  36. package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
  37. package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
  38. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
  39. package/src/config/bundled-skills/messaging/SKILL.md +59 -42
  40. package/src/config/bundled-skills/messaging/TOOLS.json +14 -92
  41. package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
  42. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
  43. package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +8 -1
  44. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +12 -4
  45. package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
  46. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
  47. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +5 -2
  48. package/src/config/bundled-skills/notion/SKILL.md +240 -0
  49. package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
  50. package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
  51. package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
  52. package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
  53. package/src/config/bundled-skills/slack/SKILL.md +49 -0
  54. package/src/config/bundled-skills/slack/TOOLS.json +167 -0
  55. package/src/config/bundled-skills/slack/tools/shared.ts +23 -0
  56. package/src/config/bundled-skills/{messaging → slack}/tools/slack-add-reaction.ts +2 -5
  57. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +33 -0
  58. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +75 -0
  59. package/src/config/bundled-skills/{messaging → slack}/tools/slack-delete-message.ts +2 -5
  60. package/src/config/bundled-skills/{messaging → slack}/tools/slack-leave-channel.ts +2 -5
  61. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +193 -0
  62. package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
  63. package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
  64. package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
  65. package/src/config/bundled-tool-registry.ts +292 -267
  66. package/src/config/schema.ts +1 -1
  67. package/src/daemon/handlers/skills.ts +334 -234
  68. package/src/daemon/ipc-contract/messages.ts +2 -0
  69. package/src/daemon/ipc-contract/surfaces.ts +2 -0
  70. package/src/daemon/lifecycle.ts +358 -221
  71. package/src/daemon/response-tier.ts +2 -0
  72. package/src/daemon/server.ts +453 -193
  73. package/src/daemon/session-agent-loop-handlers.ts +43 -2
  74. package/src/daemon/session-agent-loop.ts +3 -0
  75. package/src/daemon/session-lifecycle.ts +3 -0
  76. package/src/daemon/session-process.ts +1 -0
  77. package/src/daemon/session-surfaces.ts +22 -20
  78. package/src/daemon/session-tool-setup.ts +1 -0
  79. package/src/daemon/session.ts +5 -2
  80. package/src/messaging/outreach-classifier.ts +12 -5
  81. package/src/messaging/provider-types.ts +5 -0
  82. package/src/messaging/provider.ts +1 -1
  83. package/src/messaging/providers/gmail/adapter.ts +11 -5
  84. package/src/messaging/providers/gmail/client.ts +2 -0
  85. package/src/messaging/providers/slack/adapter.ts +1 -0
  86. package/src/messaging/providers/slack/client.ts +8 -0
  87. package/src/messaging/providers/slack/types.ts +5 -0
  88. package/src/runtime/http-errors.ts +33 -20
  89. package/src/runtime/http-server.ts +706 -291
  90. package/src/runtime/http-types.ts +26 -16
  91. package/src/runtime/routes/secret-routes.ts +57 -2
  92. package/src/runtime/routes/surface-action-routes.ts +66 -0
  93. package/src/runtime/routes/trust-rules-routes.ts +140 -0
  94. package/src/security/keychain-to-encrypted-migration.ts +59 -0
  95. package/src/security/secure-keys.ts +17 -0
  96. package/src/skills/frontmatter.ts +9 -7
  97. package/src/tools/apps/executors.ts +2 -1
  98. package/src/tools/tool-manifest.ts +44 -42
  99. package/src/tools/types.ts +9 -0
  100. package/src/__tests__/skill-mirror-parity.test.ts +0 -176
  101. package/src/config/vellum-skills/catalog.json +0 -63
  102. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
  103. package/src/skills/vellum-catalog-remote.ts +0 -166
  104. package/src/tools/skills/vellum-catalog.ts +0 -168
  105. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
  106. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
  107. /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
  108. /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
  109. /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
  110. /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
  111. /package/src/config/{vellum-skills → bundled-skills}/trusted-contacts/SKILL.md +0 -0
@@ -0,0 +1,167 @@
1
+ {
2
+ "version": 1,
3
+ "tools": [
4
+ {
5
+ "name": "slack_scan_digest",
6
+ "description": "Scan multiple Slack channels and return a digest of recent activity including top threads, participants, and privacy status.",
7
+ "category": "slack",
8
+ "risk": "low",
9
+ "input_schema": {
10
+ "type": "object",
11
+ "properties": {
12
+ "channel_ids": {
13
+ "type": "array",
14
+ "items": { "type": "string" },
15
+ "description": "Specific channel IDs to scan. If omitted, scans preferred or top active channels."
16
+ },
17
+ "hours_back": {
18
+ "type": "number",
19
+ "description": "How many hours of history to scan (default 24)"
20
+ },
21
+ "include_threads": {
22
+ "type": "boolean",
23
+ "description": "Whether to fetch thread replies for top threads (default true)"
24
+ },
25
+ "max_channels": {
26
+ "type": "number",
27
+ "description": "Maximum number of channels to scan (default 20)"
28
+ }
29
+ }
30
+ },
31
+ "executor": "tools/slack-scan-digest.ts",
32
+ "execution_target": "host"
33
+ },
34
+ {
35
+ "name": "slack_channel_details",
36
+ "description": "Get detailed information about a single Slack channel including name, topic, purpose, privacy status, and member count.",
37
+ "category": "slack",
38
+ "risk": "low",
39
+ "input_schema": {
40
+ "type": "object",
41
+ "properties": {
42
+ "channel_id": {
43
+ "type": "string",
44
+ "description": "The Slack channel ID to get details for"
45
+ }
46
+ },
47
+ "required": ["channel_id"]
48
+ },
49
+ "executor": "tools/slack-channel-details.ts",
50
+ "execution_target": "host"
51
+ },
52
+ {
53
+ "name": "slack_configure_channels",
54
+ "description": "Manage preferred Slack channels for scanning. List, add, remove, or set the preferred channel list.",
55
+ "category": "slack",
56
+ "risk": "low",
57
+ "input_schema": {
58
+ "type": "object",
59
+ "properties": {
60
+ "action": {
61
+ "type": "string",
62
+ "enum": ["list", "add", "remove", "set"],
63
+ "description": "Action to perform on the preferred channels list"
64
+ },
65
+ "channel_ids": {
66
+ "type": "array",
67
+ "items": { "type": "string" },
68
+ "description": "Channel IDs to add, remove, or set (not needed for 'list')"
69
+ }
70
+ },
71
+ "required": ["action"]
72
+ },
73
+ "executor": "tools/slack-configure-channels.ts",
74
+ "execution_target": "host"
75
+ },
76
+ {
77
+ "name": "slack_add_reaction",
78
+ "description": "Add an emoji reaction to a Slack message. Include a confidence score (0-1).",
79
+ "category": "slack",
80
+ "risk": "medium",
81
+ "input_schema": {
82
+ "type": "object",
83
+ "properties": {
84
+ "channel": {
85
+ "type": "string",
86
+ "description": "Slack channel ID"
87
+ },
88
+ "timestamp": {
89
+ "type": "string",
90
+ "description": "Message timestamp (ts)"
91
+ },
92
+ "emoji": {
93
+ "type": "string",
94
+ "description": "Emoji name without colons (e.g. \"thumbsup\")"
95
+ },
96
+ "confidence": {
97
+ "type": "number",
98
+ "description": "Confidence score (0-1) for this action"
99
+ }
100
+ },
101
+ "required": [
102
+ "channel",
103
+ "timestamp",
104
+ "emoji",
105
+ "confidence"
106
+ ]
107
+ },
108
+ "executor": "tools/slack-add-reaction.ts",
109
+ "execution_target": "host"
110
+ },
111
+ {
112
+ "name": "slack_delete_message",
113
+ "description": "Delete a Slack message posted by the bot. Include a confidence score (0-1).",
114
+ "category": "slack",
115
+ "risk": "high",
116
+ "input_schema": {
117
+ "type": "object",
118
+ "properties": {
119
+ "channel": {
120
+ "type": "string",
121
+ "description": "Slack channel ID"
122
+ },
123
+ "timestamp": {
124
+ "type": "string",
125
+ "description": "Message timestamp (ts) to delete"
126
+ },
127
+ "confidence": {
128
+ "type": "number",
129
+ "description": "Confidence score (0-1) for this action"
130
+ }
131
+ },
132
+ "required": [
133
+ "channel",
134
+ "timestamp",
135
+ "confidence"
136
+ ]
137
+ },
138
+ "executor": "tools/slack-delete-message.ts",
139
+ "execution_target": "host"
140
+ },
141
+ {
142
+ "name": "slack_leave_channel",
143
+ "description": "Leave a Slack channel. Include a confidence score (0-1).",
144
+ "category": "slack",
145
+ "risk": "medium",
146
+ "input_schema": {
147
+ "type": "object",
148
+ "properties": {
149
+ "channel": {
150
+ "type": "string",
151
+ "description": "Slack channel ID to leave"
152
+ },
153
+ "confidence": {
154
+ "type": "number",
155
+ "description": "Confidence score (0-1) for this action"
156
+ }
157
+ },
158
+ "required": [
159
+ "channel",
160
+ "confidence"
161
+ ]
162
+ },
163
+ "executor": "tools/slack-leave-channel.ts",
164
+ "execution_target": "host"
165
+ }
166
+ ]
167
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared utilities for slack skill tools.
3
+ */
4
+
5
+ import { getMessagingProvider } from '../../../../messaging/registry.js';
6
+ import { withValidToken } from '../../../../security/token-manager.js';
7
+ import type { ToolExecutionResult } from '../../../../tools/types.js';
8
+
9
+ export function ok(content: string): ToolExecutionResult {
10
+ return { content, isError: false };
11
+ }
12
+
13
+ export function err(message: string): ToolExecutionResult {
14
+ return { content: message, isError: true };
15
+ }
16
+
17
+ /**
18
+ * Execute a callback with a valid Slack OAuth token.
19
+ */
20
+ export async function withSlackToken<T>(fn: (token: string) => Promise<T>): Promise<T> {
21
+ const provider = getMessagingProvider('slack');
22
+ return withValidToken(provider.credentialService, fn);
23
+ }
@@ -1,8 +1,6 @@
1
1
  import { addReaction } from '../../../../messaging/providers/slack/client.js';
2
- import { getMessagingProvider } from '../../../../messaging/registry.js';
3
- import { withValidToken } from '../../../../security/token-manager.js';
4
2
  import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
5
- import { err,ok } from './shared.js';
3
+ import { err, ok, withSlackToken } from './shared.js';
6
4
 
7
5
  export async function run(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
8
6
  const channel = input.channel as string;
@@ -14,8 +12,7 @@ export async function run(input: Record<string, unknown>, _context: ToolContext)
14
12
  }
15
13
 
16
14
  try {
17
- const provider = getMessagingProvider('slack');
18
- return withValidToken(provider.credentialService, async (token) => {
15
+ return withSlackToken(async (token) => {
19
16
  await addReaction(token, channel, timestamp, emoji);
20
17
  return ok(`Added :${emoji}: reaction.`);
21
18
  });
@@ -0,0 +1,33 @@
1
+ import * as slack from '../../../../messaging/providers/slack/client.js';
2
+ import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
3
+ import { err, ok, withSlackToken } from './shared.js';
4
+
5
+ export async function run(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
6
+ const channelId = input.channel_id as string;
7
+
8
+ if (!channelId) {
9
+ return err('channel_id is required.');
10
+ }
11
+
12
+ try {
13
+ return withSlackToken(async (token) => {
14
+ const resp = await slack.conversationInfo(token, channelId);
15
+ const conv = resp.channel;
16
+
17
+ const result = {
18
+ channelId: conv.id,
19
+ name: conv.name ?? conv.id,
20
+ topic: conv.topic?.value || null,
21
+ purpose: conv.purpose?.value || null,
22
+ isPrivate: conv.is_private ?? conv.is_group ?? false,
23
+ isArchived: conv.is_archived ?? false,
24
+ memberCount: conv.num_members ?? null,
25
+ latestActivityTs: conv.latest?.ts ?? null,
26
+ };
27
+
28
+ return ok(JSON.stringify(result, null, 2));
29
+ });
30
+ } catch (e) {
31
+ return err(e instanceof Error ? e.message : String(e));
32
+ }
33
+ }
@@ -0,0 +1,75 @@
1
+ import { getConfig, saveConfig } from '../../../../config/loader.js';
2
+ import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
3
+ import { err, ok } from './shared.js';
4
+
5
+ function getPreferredChannels(): string[] {
6
+ const config = getConfig();
7
+ const channels = config.skills?.entries?.slack?.config?.preferredChannels;
8
+ return Array.isArray(channels) ? channels as string[] : [];
9
+ }
10
+
11
+ function setPreferredChannels(channels: string[]): void {
12
+ const config = getConfig();
13
+ if (!config.skills) config.skills = {} as typeof config.skills;
14
+ if (!config.skills.entries) config.skills.entries = {};
15
+ if (!config.skills.entries.slack) config.skills.entries.slack = { enabled: true };
16
+ if (!config.skills.entries.slack.config) config.skills.entries.slack.config = {};
17
+ config.skills.entries.slack.config.preferredChannels = channels;
18
+ saveConfig(config);
19
+ }
20
+
21
+ export async function run(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
22
+ const action = input.action as string;
23
+ const channelIds = input.channel_ids as string[] | undefined;
24
+
25
+ if (!action) {
26
+ return err('action is required (list, add, remove, or set).');
27
+ }
28
+
29
+ try {
30
+ switch (action) {
31
+ case 'list': {
32
+ const channels = getPreferredChannels();
33
+ if (channels.length === 0) {
34
+ return ok('No preferred channels configured. Use action "set" or "add" to configure channels.');
35
+ }
36
+ return ok(JSON.stringify({ preferredChannels: channels }, null, 2));
37
+ }
38
+
39
+ case 'add': {
40
+ if (!channelIds?.length) {
41
+ return err('channel_ids required for "add" action.');
42
+ }
43
+ const current = getPreferredChannels();
44
+ const merged = [...new Set([...current, ...channelIds])];
45
+ setPreferredChannels(merged);
46
+ return ok(JSON.stringify({ preferredChannels: merged, added: channelIds.filter((id) => !current.includes(id)) }, null, 2));
47
+ }
48
+
49
+ case 'remove': {
50
+ if (!channelIds?.length) {
51
+ return err('channel_ids required for "remove" action.');
52
+ }
53
+ const current = getPreferredChannels();
54
+ const removeSet = new Set(channelIds);
55
+ const remaining = current.filter((id) => !removeSet.has(id));
56
+ setPreferredChannels(remaining);
57
+ return ok(JSON.stringify({ preferredChannels: remaining, removed: channelIds.filter((id) => current.includes(id)) }, null, 2));
58
+ }
59
+
60
+ case 'set': {
61
+ if (!channelIds) {
62
+ return err('channel_ids required for "set" action.');
63
+ }
64
+ const unique = [...new Set(channelIds)];
65
+ setPreferredChannels(unique);
66
+ return ok(JSON.stringify({ preferredChannels: unique }, null, 2));
67
+ }
68
+
69
+ default:
70
+ return err(`Unknown action "${action}". Use list, add, remove, or set.`);
71
+ }
72
+ } catch (e) {
73
+ return err(e instanceof Error ? e.message : String(e));
74
+ }
75
+ }
@@ -1,8 +1,6 @@
1
1
  import { deleteMessage } from '../../../../messaging/providers/slack/client.js';
2
- import { getMessagingProvider } from '../../../../messaging/registry.js';
3
- import { withValidToken } from '../../../../security/token-manager.js';
4
2
  import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
5
- import { err, ok } from './shared.js';
3
+ import { err, ok, withSlackToken } from './shared.js';
6
4
 
7
5
  export async function run(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
8
6
  const channel = input.channel as string;
@@ -13,8 +11,7 @@ export async function run(input: Record<string, unknown>, _context: ToolContext)
13
11
  }
14
12
 
15
13
  try {
16
- const provider = getMessagingProvider('slack');
17
- return withValidToken(provider.credentialService, async (token) => {
14
+ return withSlackToken(async (token) => {
18
15
  await deleteMessage(token, channel, timestamp);
19
16
  return ok(`Message deleted.`);
20
17
  });
@@ -1,8 +1,6 @@
1
1
  import { leaveConversation } from '../../../../messaging/providers/slack/client.js';
2
- import { getMessagingProvider } from '../../../../messaging/registry.js';
3
- import { withValidToken } from '../../../../security/token-manager.js';
4
2
  import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
5
- import { err,ok } from './shared.js';
3
+ import { err, ok, withSlackToken } from './shared.js';
6
4
 
7
5
  export async function run(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
8
6
  const channel = input.channel as string;
@@ -12,8 +10,7 @@ export async function run(input: Record<string, unknown>, _context: ToolContext)
12
10
  }
13
11
 
14
12
  try {
15
- const provider = getMessagingProvider('slack');
16
- return withValidToken(provider.credentialService, async (token) => {
13
+ return withSlackToken(async (token) => {
17
14
  await leaveConversation(token, channel);
18
15
  return ok('Left channel.');
19
16
  });
@@ -0,0 +1,193 @@
1
+ import * as slack from '../../../../messaging/providers/slack/client.js';
2
+ import type { SlackConversation } from '../../../../messaging/providers/slack/types.js';
3
+ import { getConfig } from '../../../../config/loader.js';
4
+ import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
5
+ import { err, ok, withSlackToken } from './shared.js';
6
+
7
+ interface ThreadSummary {
8
+ threadTs: string;
9
+ previewText: string;
10
+ replyCount: number;
11
+ participants: string[];
12
+ }
13
+
14
+ interface ChannelDigest {
15
+ channelId: string;
16
+ channelName: string;
17
+ isPrivate: boolean;
18
+ messageCount: number;
19
+ topThreads: ThreadSummary[];
20
+ keyParticipants: string[];
21
+ error?: string;
22
+ }
23
+
24
+ const userNameCache = new Map<string, string>();
25
+
26
+ async function resolveUserName(token: string, userId: string): Promise<string> {
27
+ if (!userId) return 'unknown';
28
+ const cached = userNameCache.get(userId);
29
+ if (cached) return cached;
30
+
31
+ try {
32
+ const resp = await slack.userInfo(token, userId);
33
+ const name = resp.user.profile?.display_name
34
+ || resp.user.profile?.real_name
35
+ || resp.user.real_name
36
+ || resp.user.name;
37
+ userNameCache.set(userId, name);
38
+ return name;
39
+ } catch {
40
+ return userId;
41
+ }
42
+ }
43
+
44
+ async function scanChannel(
45
+ token: string,
46
+ conv: SlackConversation,
47
+ oldestTs: string,
48
+ includeThreads: boolean,
49
+ ): Promise<ChannelDigest> {
50
+ const channelId = conv.id;
51
+ const channelName = conv.name ?? channelId;
52
+ const isPrivate = conv.is_private ?? conv.is_group ?? false;
53
+
54
+ try {
55
+ const history = await slack.conversationHistory(token, channelId, 100, undefined, oldestTs);
56
+ const messages = history.messages;
57
+
58
+ const participantIds = new Set<string>();
59
+ for (const msg of messages) {
60
+ if (msg.user) participantIds.add(msg.user);
61
+ }
62
+
63
+ const keyParticipants: string[] = [];
64
+ for (const uid of participantIds) {
65
+ keyParticipants.push(await resolveUserName(token, uid));
66
+ }
67
+
68
+ const threadMessages = messages
69
+ .filter((m) => (m.reply_count ?? 0) > 0)
70
+ .sort((a, b) => (b.reply_count ?? 0) - (a.reply_count ?? 0))
71
+ .slice(0, 3);
72
+
73
+ const topThreads: ThreadSummary[] = [];
74
+ for (const msg of threadMessages) {
75
+ let participants: string[] = [];
76
+
77
+ if (includeThreads) {
78
+ try {
79
+ const replies = await slack.conversationReplies(token, channelId, msg.ts, 10);
80
+ const threadParticipantIds = new Set<string>();
81
+ for (const reply of replies.messages) {
82
+ if (reply.user) threadParticipantIds.add(reply.user);
83
+ }
84
+ for (const uid of threadParticipantIds) {
85
+ participants.push(await resolveUserName(token, uid));
86
+ }
87
+ } catch {
88
+ participants = [await resolveUserName(token, msg.user ?? '')];
89
+ }
90
+ }
91
+
92
+ topThreads.push({
93
+ threadTs: msg.ts,
94
+ previewText: truncate(msg.text, 150),
95
+ replyCount: msg.reply_count ?? 0,
96
+ participants,
97
+ });
98
+ }
99
+
100
+ return {
101
+ channelId,
102
+ channelName,
103
+ isPrivate,
104
+ messageCount: messages.length,
105
+ topThreads,
106
+ keyParticipants,
107
+ };
108
+ } catch (e) {
109
+ return {
110
+ channelId,
111
+ channelName,
112
+ isPrivate,
113
+ messageCount: 0,
114
+ topThreads: [],
115
+ keyParticipants: [],
116
+ error: e instanceof Error ? e.message : String(e),
117
+ };
118
+ }
119
+ }
120
+
121
+ function truncate(text: string, maxLen: number): string {
122
+ if (text.length <= maxLen) return text;
123
+ return text.slice(0, maxLen - 3) + '...';
124
+ }
125
+
126
+ export async function run(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
127
+ const channelIds = input.channel_ids as string[] | undefined;
128
+ const hoursBack = (input.hours_back as number) ?? 24;
129
+ const includeThreads = (input.include_threads as boolean) ?? true;
130
+ const maxChannels = (input.max_channels as number) ?? 20;
131
+
132
+ try {
133
+ return withSlackToken(async (token) => {
134
+ const oldestTs = String((Date.now() - hoursBack * 60 * 60 * 1000) / 1000);
135
+
136
+ let channelsToScan: SlackConversation[];
137
+
138
+ if (channelIds?.length) {
139
+ const results = await Promise.allSettled(
140
+ channelIds.map((id) => slack.conversationInfo(token, id)),
141
+ );
142
+ channelsToScan = results
143
+ .filter((r): r is PromiseFulfilledResult<Awaited<ReturnType<typeof slack.conversationInfo>>> => r.status === 'fulfilled')
144
+ .map((r) => r.value.channel);
145
+ } else {
146
+ const config = getConfig();
147
+ const preferredIds = config.skills?.entries?.slack?.config?.preferredChannels as string[] | undefined;
148
+
149
+ if (preferredIds?.length) {
150
+ const results = await Promise.allSettled(
151
+ preferredIds.map((id) => slack.conversationInfo(token, id)),
152
+ );
153
+ channelsToScan = results
154
+ .filter((r): r is PromiseFulfilledResult<Awaited<ReturnType<typeof slack.conversationInfo>>> => r.status === 'fulfilled')
155
+ .map((r) => r.value.channel);
156
+ } else {
157
+ const resp = await slack.listConversations(token, 'public_channel,private_channel', true, 200);
158
+ channelsToScan = resp.channels
159
+ .filter((c) => c.is_member)
160
+ .sort((a, b) => {
161
+ const aTs = a.latest?.ts ? parseFloat(a.latest.ts) : 0;
162
+ const bTs = b.latest?.ts ? parseFloat(b.latest.ts) : 0;
163
+ return bTs - aTs;
164
+ })
165
+ .slice(0, maxChannels);
166
+ }
167
+ }
168
+
169
+ const scanResults = await Promise.allSettled(
170
+ channelsToScan.map((conv) => scanChannel(token, conv, oldestTs, includeThreads)),
171
+ );
172
+
173
+ const digests: ChannelDigest[] = scanResults
174
+ .filter((r): r is PromiseFulfilledResult<ChannelDigest> => r.status === 'fulfilled')
175
+ .map((r) => r.value)
176
+ .filter((d) => d.messageCount > 0 || d.error);
177
+
178
+ const skippedCount = scanResults.filter((r) => r.status === 'rejected').length;
179
+
180
+ const result = {
181
+ scannedChannels: digests.length,
182
+ totalChannelsAttempted: channelsToScan.length,
183
+ skippedDueToErrors: skippedCount,
184
+ hoursBack,
185
+ channels: digests,
186
+ };
187
+
188
+ return ok(JSON.stringify(result, null, 2));
189
+ });
190
+ } catch (e) {
191
+ return err(e instanceof Error ? e.message : String(e));
192
+ }
193
+ }