@nuvlore/extension-access-okta-slack 0.1.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/LICENSE +202 -0
- package/README.md +52 -0
- package/package.json +64 -0
- package/src/slackApi.mjs +68 -0
- package/src/slackBotChat.mjs +129 -0
- package/src/slackBotRead.mjs +106 -0
- package/src/slackCanvasCreate.mjs +165 -0
- package/src/slackConfig.mjs +18 -0
- package/src/slackMessageSend.mjs +71 -0
- package/src/slackSearch.mjs +103 -0
- package/src/slackSessionRead.mjs +200 -0
- package/src/slackSyncExport.mjs +346 -0
- package/src/slackSyncPaths.mjs +47 -0
- package/src/slackSyncState.mjs +35 -0
- package/tools/okta_slack_okta_read.mjs +41 -0
- package/tools/okta_slack_okta_verify.mjs +26 -0
- package/tools/slack_okta_ai_capabilities.mjs +26 -0
- package/tools/slack_okta_bot_ask.mjs +42 -0
- package/tools/slack_okta_bot_history.mjs +32 -0
- package/tools/slack_okta_create_canvas.mjs +38 -0
- package/tools/slack_okta_search_all.mjs +33 -0
- package/tools/slack_okta_search_messages.mjs +34 -0
- package/tools/slack_okta_send_message.mjs +42 -0
- package/tools/slack_okta_sync_all.mjs +35 -0
- package/tools/slack_okta_sync_status.mjs +26 -0
- package/vendor/extension-sso/LICENSE +202 -0
- package/vendor/extension-sso/README.md +35 -0
- package/vendor/extension-sso/commands/okta-login.md +10 -0
- package/vendor/extension-sso/package.json +53 -0
- package/vendor/extension-sso/skills/workday-okta-auth/SKILL.md +20 -0
- package/vendor/extension-sso/src/autoAuth.mjs +25 -0
- package/vendor/extension-sso/src/browserLogin.mjs +1 -0
- package/vendor/extension-sso/src/chromeCookies.mjs +1 -0
- package/vendor/extension-sso/src/cookieJar.mjs +1 -0
- package/vendor/extension-sso/src/extensionAccess.mjs +173 -0
- package/vendor/extension-sso/src/htmlSessionExtract.mjs +1 -0
- package/vendor/extension-sso/src/oktaAppAccess.mjs +133 -0
- package/vendor/extension-sso/src/oktaAppCatalog.mjs +118 -0
- package/vendor/extension-sso/src/oktaAppRead.mjs +119 -0
- package/vendor/extension-sso/src/oktaConfig.mjs +12 -0
- package/vendor/extension-sso/src/oktaSession.mjs +32 -0
- package/vendor/extension-sso/src/serviceOperations.mjs +407 -0
- package/vendor/extension-sso/src/serviceReadSearch.mjs +395 -0
- package/vendor/extension-sso/src/serviceReads.mjs +109 -0
- package/vendor/extension-sso/src/serviceRegistry.mjs +497 -0
- package/vendor/extension-sso/src/sessionAuth.mjs +339 -0
- package/vendor/extension-sso/src/sessionManager.mjs +212 -0
- package/vendor/extension-sso/src/sessionValidate.mjs +74 -0
- package/vendor/extension-sso/src/ssoConfig.mjs +31 -0
- package/vendor/extension-sso/src/ssoHttpRead.mjs +292 -0
- package/vendor/extension-sso/tools/extension-access-tools.shared.mjs +16 -0
- package/vendor/extension-sso/tools/okta-app-tools.shared.mjs +5 -0
- package/vendor/extension-sso/tools/okta-tools.shared.mjs +18 -0
- package/vendor/extension-sso/tools/workday_extension_read.mjs +51 -0
- package/vendor/extension-sso/tools/workday_extension_read_access.mjs +65 -0
- package/vendor/extension-sso/tools/workday_list_extension_access.mjs +39 -0
- package/vendor/extension-sso/tools/workday_list_okta_apps.mjs +18 -0
- package/vendor/extension-sso/tools/workday_list_service_operations.mjs +41 -0
- package/vendor/extension-sso/tools/workday_list_service_read_search_capabilities.mjs +34 -0
- package/vendor/extension-sso/tools/workday_list_services.mjs +31 -0
- package/vendor/extension-sso/tools/workday_okta_app_probe_all.mjs +24 -0
- package/vendor/extension-sso/tools/workday_okta_app_read.mjs +30 -0
- package/vendor/extension-sso/tools/workday_okta_app_read_access.mjs +25 -0
- package/vendor/extension-sso/tools/workday_okta_auth_header.mjs +58 -0
- package/vendor/extension-sso/tools/workday_okta_login.mjs +65 -0
- package/vendor/extension-sso/tools/workday_okta_open_app.mjs +63 -0
- package/vendor/extension-sso/tools/workday_okta_open_service.mjs +63 -0
- package/vendor/extension-sso/tools/workday_okta_status.mjs +35 -0
- package/vendor/extension-sso/tools/workday_service_auth.mjs +65 -0
- package/vendor/extension-sso/tools/workday_service_operation_read.mjs +51 -0
- package/vendor/extension-sso/tools/workday_service_read.mjs +56 -0
- package/vendor/extension-sso/tools/workday_service_search.mjs +62 -0
- package/vendor/extension-sso/tools/workday_sso_cookie_header.mjs +59 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/LICENSE +202 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/README.md +16 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/package.json +47 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/autoAuth.mjs +47 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/browserHttpRead.mjs +76 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/browserLogin.mjs +24 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/chromeCookies.mjs +192 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/cookieJar.mjs +132 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/devops-data-paths.mjs +222 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/devops-diagnostics.mjs +440 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/enterprise-api-read.mjs +180 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-command-pack.mjs +1 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-plan.mjs +45 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-shell-policy.d.mts +26 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-shell-policy.mjs +170 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/htmlSessionExtract.mjs +64 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/LICENSE +202 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/README.md +29 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/package.json +43 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/devops-data-paths.mjs +222 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/devops-diagnostics.mjs +440 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/enterprise-api-read.mjs +180 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-command-pack.mjs +1 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-plan.mjs +45 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-shell-policy.d.mts +26 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-shell-policy.mjs +170 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { mkdir, writeFile, access } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getSlackConfig } from "./slackConfig.mjs";
|
|
4
|
+
import {
|
|
5
|
+
channelExportPaths,
|
|
6
|
+
listRootMessages,
|
|
7
|
+
threadTsToFilename
|
|
8
|
+
} from "./slackSyncPaths.mjs";
|
|
9
|
+
import { loadSyncState, saveSyncState, updateChannelSyncCursor } from "./slackSyncState.mjs";
|
|
10
|
+
import {
|
|
11
|
+
createSlackApiClient,
|
|
12
|
+
fetchAllHistory,
|
|
13
|
+
fetchAllReplies,
|
|
14
|
+
listChannelsFromBoot,
|
|
15
|
+
loadSlackSession,
|
|
16
|
+
slackUserBoot
|
|
17
|
+
} from "./slackSessionRead.mjs";
|
|
18
|
+
|
|
19
|
+
function compactMessage(message) {
|
|
20
|
+
return {
|
|
21
|
+
ts: message.ts,
|
|
22
|
+
user: message.user,
|
|
23
|
+
botId: message.bot_id,
|
|
24
|
+
subtype: message.subtype,
|
|
25
|
+
text: message.text,
|
|
26
|
+
threadTs: message.thread_ts,
|
|
27
|
+
replyCount: message.reply_count,
|
|
28
|
+
files: message.files,
|
|
29
|
+
attachments: message.attachments,
|
|
30
|
+
reactions: message.reactions
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function newestTs(messages) {
|
|
35
|
+
if (!messages.length) return null;
|
|
36
|
+
return messages.reduce((max, message) => (message.ts > max ? message.ts : max), messages[0].ts);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function matchesChannelName(channelName, pattern) {
|
|
40
|
+
const name = String(channelName || "").toLowerCase();
|
|
41
|
+
const p = String(pattern || "").toLowerCase();
|
|
42
|
+
if (!p) return false;
|
|
43
|
+
if (p.endsWith("*") && p.startsWith("*")) {
|
|
44
|
+
const inner = p.slice(1, -1);
|
|
45
|
+
return name.includes(inner);
|
|
46
|
+
}
|
|
47
|
+
if (p.endsWith("*")) return name.startsWith(p.slice(0, -1));
|
|
48
|
+
if (p.startsWith("*")) return name.endsWith(p.slice(1));
|
|
49
|
+
return name === p;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const SLACK_SYNC_PRESETS = {
|
|
53
|
+
/** All private channels plus BDS + issue-tracking public channels. */
|
|
54
|
+
priority: {
|
|
55
|
+
includePrivate: true,
|
|
56
|
+
channelNames: ["bds-public", "bds-notifications*", "jira-public", "*-issues"]
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function dedupeChannelsById(channels) {
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
return channels.filter((channel) => {
|
|
63
|
+
if (!channel?.id || seen.has(channel.id)) return false;
|
|
64
|
+
seen.add(channel.id);
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function filterPrivateChannels(channels) {
|
|
70
|
+
return channels.filter((c) => c.is_private && !c.is_im && !c.is_mpim);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function filterChannelsByKind(channels, kind) {
|
|
74
|
+
if (kind === "private") return filterPrivateChannels(channels);
|
|
75
|
+
if (kind === "public") return channels.filter((c) => !c.is_private && !c.is_im && !c.is_mpim);
|
|
76
|
+
if (kind === "dm") return channels.filter((c) => c.is_im);
|
|
77
|
+
if (kind === "mpim") return channels.filter((c) => c.is_mpim);
|
|
78
|
+
return channels;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function filterChannelsByNames(channels, channelNames) {
|
|
82
|
+
return channels.filter((channel) =>
|
|
83
|
+
channelNames.some((pattern) => matchesChannelName(channel.name, pattern))
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Pick channels for sync — kind/name filters, presets, and explicit channelIds. */
|
|
88
|
+
export function selectChannelsForSync(channels, options = {}) {
|
|
89
|
+
const preset = options.preset ? SLACK_SYNC_PRESETS[options.preset] : undefined;
|
|
90
|
+
const includePrivate = options.includePrivate === true || preset?.includePrivate === true;
|
|
91
|
+
const channelNames = options.channelNames ?? preset?.channelNames;
|
|
92
|
+
|
|
93
|
+
if (includePrivate && channelNames?.length) {
|
|
94
|
+
const selected = dedupeChannelsById([
|
|
95
|
+
...filterPrivateChannels(channels),
|
|
96
|
+
...filterChannelsByNames(channels, channelNames)
|
|
97
|
+
]);
|
|
98
|
+
const limit = options.channelLimit ?? selected.length;
|
|
99
|
+
return applyChannelIdFilter(selected, options).slice(0, limit);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let filtered = filterChannelsByKind(channels, options.channelKind);
|
|
103
|
+
if (channelNames?.length) {
|
|
104
|
+
filtered = filterChannelsByNames(filtered, channelNames);
|
|
105
|
+
}
|
|
106
|
+
filtered = applyChannelIdFilter(filtered, options);
|
|
107
|
+
const limit = options.channelLimit ?? filtered.length;
|
|
108
|
+
return filtered.slice(0, limit);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function applyChannelIdFilter(channels, options) {
|
|
112
|
+
if (!options.channelIds?.length) return channels;
|
|
113
|
+
const ids = new Set(options.channelIds);
|
|
114
|
+
return channels.filter((channel) => ids.has(channel.id));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function filterCompletedChannels(channels, exportDir) {
|
|
118
|
+
const pending = [];
|
|
119
|
+
for (const channel of channels) {
|
|
120
|
+
const paths = channelExportPaths(exportDir, channel);
|
|
121
|
+
try {
|
|
122
|
+
await access(paths.channelMetaPath);
|
|
123
|
+
} catch {
|
|
124
|
+
pending.push(channel);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return pending;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function threadFileExists(threadsDir, threadTs) {
|
|
131
|
+
try {
|
|
132
|
+
await access(join(threadsDir, threadTsToFilename(threadTs)));
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function writeThreadFile(threadsDir, threadTs, payload) {
|
|
140
|
+
await mkdir(threadsDir, { recursive: true });
|
|
141
|
+
const path = join(threadsDir, threadTsToFilename(threadTs));
|
|
142
|
+
await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`);
|
|
143
|
+
return path;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function syncSlackChannel(channel, context, options = {}) {
|
|
147
|
+
const { slackApi, exportDir, config, state } = context;
|
|
148
|
+
const paths = channelExportPaths(exportDir, channel);
|
|
149
|
+
await mkdir(paths.channelDir, { recursive: true });
|
|
150
|
+
|
|
151
|
+
const cursor = state.channels?.[channel.id];
|
|
152
|
+
const history = await fetchAllHistory(slackApi, channel.id, {
|
|
153
|
+
oldest: options.incremental && cursor?.newestTs ? cursor.newestTs : undefined
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const roots = listRootMessages(history);
|
|
157
|
+
let threadCount = 0;
|
|
158
|
+
let messageCount = 0;
|
|
159
|
+
const threadFiles = [];
|
|
160
|
+
|
|
161
|
+
for (const root of roots) {
|
|
162
|
+
if (options.skipExistingThreads && (await threadFileExists(paths.threadsDir, root.threadTs))) {
|
|
163
|
+
threadCount += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let threadMessages = [root];
|
|
168
|
+
if ((root.reply_count ?? 0) > 0) {
|
|
169
|
+
try {
|
|
170
|
+
threadMessages = await fetchAllReplies(slackApi, channel.id, root.threadTs);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (!String(error.message).includes("ratelimited")) throw error;
|
|
173
|
+
await new Promise((resolve) => setTimeout(resolve, 30_000));
|
|
174
|
+
threadMessages = await fetchAllReplies(slackApi, channel.id, root.threadTs);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const compacted = threadMessages.map(compactMessage);
|
|
178
|
+
messageCount += compacted.length;
|
|
179
|
+
threadCount += 1;
|
|
180
|
+
|
|
181
|
+
const filePath = await writeThreadFile(paths.threadsDir, root.threadTs, {
|
|
182
|
+
readOnly: true,
|
|
183
|
+
channelId: channel.id,
|
|
184
|
+
channelName: channel.name,
|
|
185
|
+
channelKind: channel.is_im ? "im" : channel.is_mpim ? "mpim" : channel.is_private ? "private" : "public",
|
|
186
|
+
threadTs: root.threadTs,
|
|
187
|
+
syncedAt: new Date().toISOString(),
|
|
188
|
+
messageCount: compacted.length,
|
|
189
|
+
root: compacted[0] ?? null,
|
|
190
|
+
messages: compacted
|
|
191
|
+
});
|
|
192
|
+
threadFiles.push(filePath);
|
|
193
|
+
options.onProgress?.({
|
|
194
|
+
phase: "thread",
|
|
195
|
+
channelId: channel.id,
|
|
196
|
+
channelName: channel.name,
|
|
197
|
+
threadTs: root.threadTs,
|
|
198
|
+
messageCount: compacted.length
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await writeFile(
|
|
203
|
+
paths.channelMetaPath,
|
|
204
|
+
`${JSON.stringify(
|
|
205
|
+
{
|
|
206
|
+
readOnly: true,
|
|
207
|
+
channelId: channel.id,
|
|
208
|
+
name: channel.name,
|
|
209
|
+
isPrivate: Boolean(channel.is_private),
|
|
210
|
+
isIm: Boolean(channel.is_im),
|
|
211
|
+
isMpim: Boolean(channel.is_mpim),
|
|
212
|
+
syncedAt: new Date().toISOString(),
|
|
213
|
+
threadCount,
|
|
214
|
+
messageCount,
|
|
215
|
+
incremental: Boolean(options.incremental && cursor?.newestTs),
|
|
216
|
+
threadsDir: "threads"
|
|
217
|
+
},
|
|
218
|
+
null,
|
|
219
|
+
2
|
|
220
|
+
)}\n`
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const allMessages = history;
|
|
224
|
+
updateChannelSyncCursor(state, channel.id, {
|
|
225
|
+
newestTs: newestTs(allMessages) ?? cursor?.newestTs ?? null,
|
|
226
|
+
threadCount: (cursor?.threadCount ?? 0) + threadCount,
|
|
227
|
+
messageCount: (cursor?.messageCount ?? 0) + messageCount
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return { channelId: channel.id, threadCount, messageCount, threadFiles };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Export all accessible Slack channels to disk:
|
|
235
|
+
* {exportDir}/channels/{channelId}__{kind}__{name}/channel.json
|
|
236
|
+
* {exportDir}/channels/.../threads/{threadTs}.json
|
|
237
|
+
*/
|
|
238
|
+
export async function syncAllSlackData(options = {}) {
|
|
239
|
+
const config = options.config ?? getSlackConfig();
|
|
240
|
+
const exportDir = options.exportDir ?? config.exportDir;
|
|
241
|
+
await mkdir(join(exportDir, "channels"), { recursive: true });
|
|
242
|
+
|
|
243
|
+
const session = await loadSlackSession(options);
|
|
244
|
+
const slackApi = createSlackApiClient(session, options);
|
|
245
|
+
const boot = await slackUserBoot(session, options);
|
|
246
|
+
const channels = listChannelsFromBoot(boot);
|
|
247
|
+
let selected = selectChannelsForSync(channels, options);
|
|
248
|
+
const skippedCompleted = options.skipCompleted
|
|
249
|
+
? selected.length - (await filterCompletedChannels(selected, exportDir)).length
|
|
250
|
+
: 0;
|
|
251
|
+
if (options.skipCompleted) {
|
|
252
|
+
selected = await filterCompletedChannels(selected, exportDir);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const state = await loadSyncState(config);
|
|
256
|
+
const context = { slackApi, exportDir, config, state, boot };
|
|
257
|
+
const results = [];
|
|
258
|
+
let totalThreads = 0;
|
|
259
|
+
let totalMessages = 0;
|
|
260
|
+
|
|
261
|
+
for (const channel of selected) {
|
|
262
|
+
options.onProgress?.({
|
|
263
|
+
phase: "channel_start",
|
|
264
|
+
channelId: channel.id,
|
|
265
|
+
channelName: channel.name,
|
|
266
|
+
index: results.length + 1,
|
|
267
|
+
total: selected.length
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
const result = await syncSlackChannel(channel, context, options);
|
|
271
|
+
totalThreads += result.threadCount;
|
|
272
|
+
totalMessages += result.messageCount;
|
|
273
|
+
results.push({ ...result, ok: true, channelName: channel.name });
|
|
274
|
+
} catch (error) {
|
|
275
|
+
results.push({
|
|
276
|
+
ok: false,
|
|
277
|
+
channelId: channel.id,
|
|
278
|
+
channelName: channel.name,
|
|
279
|
+
error: error instanceof Error ? error.message : String(error)
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
options.onProgress?.({
|
|
283
|
+
phase: "channel_done",
|
|
284
|
+
channelId: channel.id,
|
|
285
|
+
channelName: channel.name,
|
|
286
|
+
index: results.length,
|
|
287
|
+
total: selected.length
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
state.lastSyncAt = new Date().toISOString();
|
|
292
|
+
state.workspace = session.baseUrl;
|
|
293
|
+
state.user = boot.self?.name;
|
|
294
|
+
await saveSyncState(state, config);
|
|
295
|
+
|
|
296
|
+
const preset = options.preset ? SLACK_SYNC_PRESETS[options.preset] : undefined;
|
|
297
|
+
const resolvedChannelNames = options.channelNames ?? preset?.channelNames;
|
|
298
|
+
|
|
299
|
+
const manifest = {
|
|
300
|
+
readOnly: true,
|
|
301
|
+
workspace: session.baseUrl,
|
|
302
|
+
user: boot.self?.name,
|
|
303
|
+
syncedAt: state.lastSyncAt,
|
|
304
|
+
incremental: Boolean(options.incremental),
|
|
305
|
+
skipCompleted: Boolean(options.skipCompleted),
|
|
306
|
+
skipExistingThreads: Boolean(options.skipExistingThreads),
|
|
307
|
+
skippedCompleted,
|
|
308
|
+
channelKind: options.channelKind ?? (options.preset ? undefined : "all"),
|
|
309
|
+
preset: options.preset,
|
|
310
|
+
includePrivate: options.includePrivate ?? preset?.includePrivate,
|
|
311
|
+
channelNames: resolvedChannelNames,
|
|
312
|
+
selectedChannelCount: selected.length,
|
|
313
|
+
channelCount: results.filter((r) => r.ok).length,
|
|
314
|
+
failedChannels: results.filter((r) => !r.ok).length,
|
|
315
|
+
threadCount: totalThreads,
|
|
316
|
+
messageCount: totalMessages,
|
|
317
|
+
exportDir,
|
|
318
|
+
layout: {
|
|
319
|
+
channels: "channels/{channelId}__{kind}__{name}/channel.json",
|
|
320
|
+
threads: "channels/{channelId}__{kind}__{name}/threads/{threadTs}.json"
|
|
321
|
+
},
|
|
322
|
+
results: results.map((r) => ({
|
|
323
|
+
ok: r.ok,
|
|
324
|
+
channelId: r.channelId,
|
|
325
|
+
channelName: r.channelName,
|
|
326
|
+
threadCount: r.threadCount,
|
|
327
|
+
messageCount: r.messageCount,
|
|
328
|
+
error: r.error
|
|
329
|
+
}))
|
|
330
|
+
};
|
|
331
|
+
await writeFile(config.manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
332
|
+
|
|
333
|
+
return manifest;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function getSlackSyncStatus(config = getSlackConfig()) {
|
|
337
|
+
const state = await loadSyncState(config);
|
|
338
|
+
let manifest = null;
|
|
339
|
+
try {
|
|
340
|
+
const { readFile } = await import("node:fs/promises");
|
|
341
|
+
manifest = JSON.parse(await readFile(config.manifestPath, "utf8"));
|
|
342
|
+
} catch {
|
|
343
|
+
// no manifest yet
|
|
344
|
+
}
|
|
345
|
+
return { state, manifest, exportDir: config.exportDir };
|
|
346
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function sanitizePathSegment(value, fallback = "unnamed") {
|
|
4
|
+
const raw = String(value || fallback)
|
|
5
|
+
.trim()
|
|
6
|
+
.replace(/[\\/:*?"<>|]/g, "_")
|
|
7
|
+
.replace(/\s+/g, "-")
|
|
8
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
|
9
|
+
.replace(/_+/g, "_")
|
|
10
|
+
.replace(/^-|-$/g, "");
|
|
11
|
+
return (raw || fallback).slice(0, 80);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function channelDirName(channel) {
|
|
15
|
+
const label = channel.name || channel.id;
|
|
16
|
+
const kind = channel.is_im ? "dm" : channel.is_mpim ? "mpim" : channel.is_private ? "private" : "public";
|
|
17
|
+
return `${channel.id}__${kind}__${sanitizePathSegment(label, channel.id)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function threadTsToFilename(threadTs) {
|
|
21
|
+
return `${String(threadTs).replace(/\./g, "_")}.json`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function channelExportPaths(exportDir, channel) {
|
|
25
|
+
const dirName = channelDirName(channel);
|
|
26
|
+
const channelDir = join(exportDir, "channels", dirName);
|
|
27
|
+
return {
|
|
28
|
+
dirName,
|
|
29
|
+
channelDir,
|
|
30
|
+
channelMetaPath: join(channelDir, "channel.json"),
|
|
31
|
+
threadsDir: join(channelDir, "threads")
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function listRootMessages(messages) {
|
|
36
|
+
const roots = [];
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
for (const message of messages) {
|
|
39
|
+
if (message.subtype === "channel_join" || message.subtype === "channel_leave") continue;
|
|
40
|
+
const threadTs = message.thread_ts && message.thread_ts !== message.ts ? message.thread_ts : message.ts;
|
|
41
|
+
if (message.thread_ts && message.thread_ts !== message.ts) continue;
|
|
42
|
+
if (seen.has(threadTs)) continue;
|
|
43
|
+
seen.add(threadTs);
|
|
44
|
+
roots.push({ ...message, threadTs });
|
|
45
|
+
}
|
|
46
|
+
return roots;
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { getSlackConfig } from "./slackConfig.mjs";
|
|
4
|
+
|
|
5
|
+
export async function loadSyncState(config = getSlackConfig()) {
|
|
6
|
+
try {
|
|
7
|
+
const text = await readFile(config.syncStatePath, "utf8");
|
|
8
|
+
return JSON.parse(text);
|
|
9
|
+
} catch {
|
|
10
|
+
return {
|
|
11
|
+
version: 1,
|
|
12
|
+
lastSyncAt: null,
|
|
13
|
+
channels: {}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function saveSyncState(state, config = getSlackConfig()) {
|
|
19
|
+
await mkdir(dirname(config.syncStatePath), { recursive: true });
|
|
20
|
+
await writeFile(config.syncStatePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getChannelSyncCursor(state, channelId) {
|
|
24
|
+
return state.channels?.[channelId] ?? { newestTs: null, threadCount: 0, messageCount: 0 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function updateChannelSyncCursor(state, channelId, update) {
|
|
28
|
+
state.channels ??= {};
|
|
29
|
+
state.channels[channelId] = {
|
|
30
|
+
...getChannelSyncCursor(state, channelId),
|
|
31
|
+
...update,
|
|
32
|
+
syncedAt: new Date().toISOString()
|
|
33
|
+
};
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const description = "Read-only GET for Okta app Slack using browser SSO cookies.";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
|
|
5
|
+
import { oktaAppReadGet } from "@nuvlore/extension-sso/okta-app-access";
|
|
6
|
+
|
|
7
|
+
const inputSchema = {
|
|
8
|
+
restPath: z.string().trim().min(1).max(2048).optional(),
|
|
9
|
+
baseUrl: z.string().trim().url().max(2048).optional(),
|
|
10
|
+
query: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
11
|
+
allowCookieFallback: z.boolean().optional()
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const sharedTool = defineReadOnlyTool({
|
|
15
|
+
name: "okta_slack_okta_read",
|
|
16
|
+
description: "Read-only GET for Okta app \"Slack\" using browser SSO cookies.",
|
|
17
|
+
inputSchema,
|
|
18
|
+
meta: { permissions: { rule: "ask" } },
|
|
19
|
+
annotations: { openWorldHint: true },
|
|
20
|
+
async handler(input) {
|
|
21
|
+
try {
|
|
22
|
+
return ok(
|
|
23
|
+
JSON.stringify(
|
|
24
|
+
await oktaAppReadGet("Slack", { ...input, useDefault: input.useDefault ?? !input.restPath }),
|
|
25
|
+
null,
|
|
26
|
+
2
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return fail(error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export default {
|
|
36
|
+
description,
|
|
37
|
+
inputSchema: sharedTool.inputSchema,
|
|
38
|
+
meta: sharedTool.meta,
|
|
39
|
+
annotations: sharedTool.annotations,
|
|
40
|
+
handler: sharedTool.handler
|
|
41
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const description = "Probe read-only SSO readiness for Okta app Slack.";
|
|
2
|
+
|
|
3
|
+
import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
|
|
4
|
+
import { getOktaAppReadAccess } from "@nuvlore/extension-sso/okta-app-access";
|
|
5
|
+
|
|
6
|
+
const sharedTool = defineReadOnlyTool({
|
|
7
|
+
name: "okta_slack_okta_verify",
|
|
8
|
+
description: "Probe read-only SSO readiness for Okta app \"Slack\".",
|
|
9
|
+
inputSchema: { type: "object", properties: {} },
|
|
10
|
+
meta: { permissions: { rule: "allow" } },
|
|
11
|
+
async handler() {
|
|
12
|
+
try {
|
|
13
|
+
return ok(JSON.stringify(await getOktaAppReadAccess("Slack"), null, 2));
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return fail(error);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
description,
|
|
22
|
+
inputSchema: sharedTool.inputSchema,
|
|
23
|
+
meta: sharedTool.meta,
|
|
24
|
+
annotations: sharedTool.annotations,
|
|
25
|
+
handler: sharedTool.handler
|
|
26
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const description = "Report Slack search and Slack AI capabilities available with current browser SSO session.";
|
|
2
|
+
|
|
3
|
+
import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
|
|
4
|
+
import { getSlackAiCapabilities } from "../src/slackSearch.mjs";
|
|
5
|
+
|
|
6
|
+
const sharedTool = defineReadOnlyTool({
|
|
7
|
+
name: "slack_okta_ai_capabilities",
|
|
8
|
+
description: "Report Slack search and Slack AI capabilities available with current browser SSO session.",
|
|
9
|
+
inputSchema: { type: "object", properties: {} },
|
|
10
|
+
meta: { permissions: { rule: "allow" } },
|
|
11
|
+
async handler() {
|
|
12
|
+
try {
|
|
13
|
+
return ok(JSON.stringify(await getSlackAiCapabilities(), null, 2));
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return fail(error);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
description,
|
|
22
|
+
inputSchema: sharedTool.inputSchema,
|
|
23
|
+
meta: sharedTool.meta,
|
|
24
|
+
annotations: sharedTool.annotations,
|
|
25
|
+
handler: sharedTool.handler
|
|
26
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const description = "Send a message to Slackbot via browser SSO and wait for its AI reply. Only works in your Slackbot DM — no other channels.";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
|
|
5
|
+
import { askSlackbot, followUpSlackbot } from "../src/slackBotChat.mjs";
|
|
6
|
+
|
|
7
|
+
const inputSchema = {
|
|
8
|
+
question: z.string().trim().min(1).max(4000).describe("Message to send to @slackbot"),
|
|
9
|
+
threadTs: z
|
|
10
|
+
.string()
|
|
11
|
+
.trim()
|
|
12
|
+
.regex(/^\d+\.\d+$/)
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Optional thread timestamp to continue an existing Slackbot conversation"),
|
|
15
|
+
waitMs: z.number().int().min(5000).max(180000).optional().describe("How long to wait for Slackbot reply")
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
...defineReadOnlyTool({
|
|
20
|
+
name: "slack_okta_bot_ask",
|
|
21
|
+
description:
|
|
22
|
+
"Send a message to Slackbot via browser SSO and wait for its AI reply. Only works in your Slackbot DM — no other channels.",
|
|
23
|
+
inputSchema,
|
|
24
|
+
meta: { permissions: { rule: "ask" } },
|
|
25
|
+
annotations: { openWorldHint: true, destructiveHint: false, readOnlyHint: false },
|
|
26
|
+
async handler(input) {
|
|
27
|
+
try {
|
|
28
|
+
const result = input.threadTs
|
|
29
|
+
? await followUpSlackbot(input.threadTs, input.question, input)
|
|
30
|
+
: await askSlackbot(input.question, input);
|
|
31
|
+
return ok(JSON.stringify(result, null, 2));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return fail(error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
annotations: {
|
|
38
|
+
readOnlyHint: false,
|
|
39
|
+
destructiveHint: false,
|
|
40
|
+
openWorldHint: true
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const description = "Read Slackbot DM history and Q&A threads (read-only). Does not send new messages to Slackbot.";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
|
|
5
|
+
import { readSlackbotConversations } from "../src/slackBotRead.mjs";
|
|
6
|
+
|
|
7
|
+
const inputSchema = {
|
|
8
|
+
limit: z.number().int().min(1).max(200).optional()
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const sharedTool = defineReadOnlyTool({
|
|
12
|
+
name: "slack_okta_bot_history",
|
|
13
|
+
description:
|
|
14
|
+
"Read Slackbot DM history and Q&A threads (read-only). Does not send new messages to Slackbot.",
|
|
15
|
+
inputSchema,
|
|
16
|
+
meta: { permissions: { rule: "allow" } },
|
|
17
|
+
async handler(input) {
|
|
18
|
+
try {
|
|
19
|
+
return ok(JSON.stringify(await readSlackbotConversations(input), null, 2));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return fail(error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
description,
|
|
28
|
+
inputSchema: sharedTool.inputSchema,
|
|
29
|
+
meta: sharedTool.meta,
|
|
30
|
+
annotations: sharedTool.annotations,
|
|
31
|
+
handler: sharedTool.handler
|
|
32
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const description = "Create a Slack canvas for a channel using the project Slack/Okta tooling. Requires an env Slack token with canvases:write; falls back to Chrome session to surface auth blockers.";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
|
|
5
|
+
import { createSlackCanvas } from "../src/slackCanvasCreate.mjs";
|
|
6
|
+
|
|
7
|
+
const inputSchema = {
|
|
8
|
+
channelId: z.string().trim().min(1).describe("Slack channel conversation id, e.g. C0497KFSDNJ"),
|
|
9
|
+
title: z.string().trim().min(1).max(255).describe("Canvas title."),
|
|
10
|
+
content: z.string().trim().min(1).describe("Canvas markdown content."),
|
|
11
|
+
channelCanvas: z
|
|
12
|
+
.boolean()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Create as the channel canvas via conversations.canvases.create. Defaults to true.")
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
...defineReadOnlyTool({
|
|
19
|
+
name: "slack_okta_create_canvas",
|
|
20
|
+
description:
|
|
21
|
+
"Create a Slack canvas for a channel using the project Slack/Okta tooling. Requires an env Slack token with canvases:write; falls back to Chrome session to surface auth blockers.",
|
|
22
|
+
inputSchema,
|
|
23
|
+
meta: { permissions: { rule: "ask" } },
|
|
24
|
+
annotations: { openWorldHint: true, destructiveHint: false, readOnlyHint: false },
|
|
25
|
+
async handler(input) {
|
|
26
|
+
try {
|
|
27
|
+
return ok(JSON.stringify(await createSlackCanvas(input), null, 2));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return fail(error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}),
|
|
33
|
+
annotations: {
|
|
34
|
+
readOnlyHint: false,
|
|
35
|
+
destructiveHint: false,
|
|
36
|
+
openWorldHint: true
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const description = "Search Slack messages, files, and posts (read-only search.all).";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
|
|
5
|
+
import { searchSlackAll } from "../src/slackSearch.mjs";
|
|
6
|
+
|
|
7
|
+
const inputSchema = {
|
|
8
|
+
query: z.string().trim().min(1).max(2048),
|
|
9
|
+
count: z.number().int().min(1).max(100).optional()
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const sharedTool = defineReadOnlyTool({
|
|
13
|
+
name: "slack_okta_search_all",
|
|
14
|
+
description: "Search Slack messages, files, and posts (read-only search.all).",
|
|
15
|
+
inputSchema,
|
|
16
|
+
meta: { permissions: { rule: "ask" } },
|
|
17
|
+
annotations: { openWorldHint: true },
|
|
18
|
+
async handler(input) {
|
|
19
|
+
try {
|
|
20
|
+
return ok(JSON.stringify(await searchSlackAll(input.query, input), null, 2));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return fail(error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
description,
|
|
29
|
+
inputSchema: sharedTool.inputSchema,
|
|
30
|
+
meta: sharedTool.meta,
|
|
31
|
+
annotations: sharedTool.annotations,
|
|
32
|
+
handler: sharedTool.handler
|
|
33
|
+
};
|