sparkecoder 0.1.139 → 0.1.141
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/index.d.ts +3 -3
- package/dist/agent/index.js +661 -10
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +756 -294
- package/dist/cli.js.map +1 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/{index-BAsQWqZj.d.ts → index-Cl_eUatM.d.ts} +96 -96
- package/dist/index.d.ts +5 -5
- package/dist/index.js +752 -292
- package/dist/index.js.map +1 -1
- package/dist/{schema-Dz-wABVY.d.ts → schema-BSz4MzhJ.d.ts} +3 -3
- package/dist/{search-CVVfuBPZ.d.ts → search-DOzC4ojH.d.ts} +4 -4
- package/dist/server/index.js +752 -292
- package/dist/server/index.js.map +1 -1
- package/dist/tools/index.d.ts +3 -3
- package/dist/tools/index.js.map +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/web/.next/server/app/(main)/settings/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_settings_page_tsx_eb320e07._.js +1 -1
- package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/web/.next/static/chunks/001c7ddc8dad6764.js +3 -0
- package/web/.next/standalone/web/.next/static/static/chunks/001c7ddc8dad6764.js +3 -0
- package/web/.next/standalone/web/runtime-config.json +2 -2
- package/web/.next/standalone/web/src/app/(main)/settings/page.tsx +14 -3
- package/web/.next/static/chunks/001c7ddc8dad6764.js +3 -0
- package/web/.next/standalone/web/.next/static/chunks/20ca4e35e9bb3e94.js +0 -3
- package/web/.next/standalone/web/.next/static/static/chunks/20ca4e35e9bb3e94.js +0 -3
- package/web/.next/static/chunks/20ca4e35e9bb3e94.js +0 -3
- /package/web/.next/standalone/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
- /package/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
package/dist/agent/index.js
CHANGED
|
@@ -1798,6 +1798,17 @@ function hookExit() {
|
|
|
1798
1798
|
process.once("SIGINT", flush2);
|
|
1799
1799
|
process.once("SIGTERM", flush2);
|
|
1800
1800
|
}
|
|
1801
|
+
function getCachedUserName(userId) {
|
|
1802
|
+
load();
|
|
1803
|
+
return userMap.get(userId);
|
|
1804
|
+
}
|
|
1805
|
+
function setCachedUserName(userId, entry2) {
|
|
1806
|
+
load();
|
|
1807
|
+
hookExit();
|
|
1808
|
+
userMap.set(userId, entry2);
|
|
1809
|
+
evictIfOversized(userMap, MAX_USERS);
|
|
1810
|
+
scheduleSave();
|
|
1811
|
+
}
|
|
1801
1812
|
function setCachedThreadOwnership(key2, entry2) {
|
|
1802
1813
|
load();
|
|
1803
1814
|
hookExit();
|
|
@@ -1805,7 +1816,7 @@ function setCachedThreadOwnership(key2, entry2) {
|
|
|
1805
1816
|
evictIfOversized(threadMap, MAX_THREADS);
|
|
1806
1817
|
scheduleSave();
|
|
1807
1818
|
}
|
|
1808
|
-
var FILENAME, FILE_VERSION, SAVE_DEBOUNCE_MS, MAX_THREADS, loaded, userMap, threadMap, dirty, saveTimer, exitHooked;
|
|
1819
|
+
var FILENAME, FILE_VERSION, SAVE_DEBOUNCE_MS, MAX_USERS, MAX_THREADS, loaded, userMap, threadMap, dirty, saveTimer, exitHooked;
|
|
1809
1820
|
var init_persistence = __esm({
|
|
1810
1821
|
"src/integrations/slack/persistence.ts"() {
|
|
1811
1822
|
"use strict";
|
|
@@ -1813,6 +1824,7 @@ var init_persistence = __esm({
|
|
|
1813
1824
|
FILENAME = "slack-cache.json";
|
|
1814
1825
|
FILE_VERSION = 1;
|
|
1815
1826
|
SAVE_DEBOUNCE_MS = 500;
|
|
1827
|
+
MAX_USERS = 5e3;
|
|
1816
1828
|
MAX_THREADS = 5e3;
|
|
1817
1829
|
loaded = false;
|
|
1818
1830
|
userMap = /* @__PURE__ */ new Map();
|
|
@@ -1936,6 +1948,58 @@ function getSlackAdapter() {
|
|
|
1936
1948
|
function isSlackConfigured() {
|
|
1937
1949
|
return readSlackConfig() !== void 0;
|
|
1938
1950
|
}
|
|
1951
|
+
function getSlackBotToken() {
|
|
1952
|
+
return readSlackConfig()?.botToken ?? null;
|
|
1953
|
+
}
|
|
1954
|
+
async function fetchSlackUserInfo(userId) {
|
|
1955
|
+
const token = getSlackBotToken();
|
|
1956
|
+
if (!token) return null;
|
|
1957
|
+
try {
|
|
1958
|
+
const res = await fetch(`https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`, {
|
|
1959
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1960
|
+
});
|
|
1961
|
+
const data = await res.json().catch(() => ({}));
|
|
1962
|
+
if (!data?.ok) {
|
|
1963
|
+
console.warn(`[slack] users.info(${userId}) failed: ${data?.error || `HTTP ${res.status}`}`);
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
const profile = data.user?.profile || {};
|
|
1967
|
+
const name = profile.display_name_normalized || profile.display_name || profile.real_name_normalized || profile.real_name || data.user?.real_name || data.user?.name || null;
|
|
1968
|
+
const realName = profile.real_name_normalized || profile.real_name || data.user?.real_name || null;
|
|
1969
|
+
const email = profile.email || null;
|
|
1970
|
+
return {
|
|
1971
|
+
name: name ? String(name) : null,
|
|
1972
|
+
realName: realName ? String(realName) : null,
|
|
1973
|
+
email: email ? String(email) : null
|
|
1974
|
+
};
|
|
1975
|
+
} catch (err) {
|
|
1976
|
+
console.warn(`[slack] users.info(${userId}) error:`, err?.message || err);
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
async function resolveSlackUserInfo(userId) {
|
|
1981
|
+
if (!userId) return null;
|
|
1982
|
+
const now = Date.now();
|
|
1983
|
+
const hit = getCachedUserName(userId);
|
|
1984
|
+
if (hit && hit.expiresAt > now) {
|
|
1985
|
+
return { name: hit.name, realName: hit.realName ?? null, email: hit.email ?? null };
|
|
1986
|
+
}
|
|
1987
|
+
const inflight = userInflight.get(userId);
|
|
1988
|
+
if (inflight) return inflight;
|
|
1989
|
+
const p = (async () => {
|
|
1990
|
+
const info = await fetchSlackUserInfo(userId);
|
|
1991
|
+
setCachedUserName(userId, {
|
|
1992
|
+
name: info?.name ?? null,
|
|
1993
|
+
realName: info?.realName ?? null,
|
|
1994
|
+
email: info?.email ?? null,
|
|
1995
|
+
expiresAt: now + (info?.name ? USER_TTL_MS : USER_FAIL_TTL_MS)
|
|
1996
|
+
});
|
|
1997
|
+
userInflight.delete(userId);
|
|
1998
|
+
return info;
|
|
1999
|
+
})();
|
|
2000
|
+
userInflight.set(userId, p);
|
|
2001
|
+
return p;
|
|
2002
|
+
}
|
|
1939
2003
|
function threadCacheKey(channel, threadTs) {
|
|
1940
2004
|
return `${channel}\u241F${threadTs}`;
|
|
1941
2005
|
}
|
|
@@ -1946,7 +2010,7 @@ function noteBotPostedInThread(channel, threadTs) {
|
|
|
1946
2010
|
expiresAt: Date.now() + THREAD_OWNED_TTL_MS
|
|
1947
2011
|
});
|
|
1948
2012
|
}
|
|
1949
|
-
var RESULT_REACTIONS, SLACK_FETCH_ATTEMPTS, SLACK_BACKOFF_BASE_MS, SLACK_BACKOFF_CAP_MS, REACTION_SOFT_ERRORS, USER_TTL_MS, USER_FAIL_TTL_MS, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS;
|
|
2013
|
+
var RESULT_REACTIONS, SLACK_FETCH_ATTEMPTS, SLACK_BACKOFF_BASE_MS, SLACK_BACKOFF_CAP_MS, REACTION_SOFT_ERRORS, USER_TTL_MS, USER_FAIL_TTL_MS, userInflight, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS;
|
|
1950
2014
|
var init_client2 = __esm({
|
|
1951
2015
|
"src/integrations/slack/client.ts"() {
|
|
1952
2016
|
"use strict";
|
|
@@ -1971,6 +2035,7 @@ var init_client2 = __esm({
|
|
|
1971
2035
|
]);
|
|
1972
2036
|
USER_TTL_MS = 60 * 60 * 1e3;
|
|
1973
2037
|
USER_FAIL_TTL_MS = 5 * 60 * 1e3;
|
|
2038
|
+
userInflight = /* @__PURE__ */ new Map();
|
|
1974
2039
|
THREAD_OWNED_TTL_MS = 60 * 60 * 1e3;
|
|
1975
2040
|
THREAD_NEG_TTL_MS = 5 * 60 * 1e3;
|
|
1976
2041
|
}
|
|
@@ -2278,7 +2343,7 @@ async function reconcileOnce(now = Date.now()) {
|
|
|
2278
2343
|
entry2.updatedAt = Date.now();
|
|
2279
2344
|
const nudged = {
|
|
2280
2345
|
...entry2.event,
|
|
2281
|
-
content: `[REPLAY attempt ${entry2.attempts}/${MAX_ATTEMPTS} \u2014 you received this but have not yet replied to it or marked it handled. Respond now on the originating channel; if it genuinely needs no reply,
|
|
2346
|
+
content: `[REPLAY attempt ${entry2.attempts}/${MAX_ATTEMPTS} \u2014 you received this but have not yet replied to it or marked it handled. Respond now on the originating channel; if it genuinely needs no substantive reply, post a brief acknowledgement.]
|
|
2282
2347
|
${entry2.event.content}`,
|
|
2283
2348
|
wake: "now"
|
|
2284
2349
|
};
|
|
@@ -2392,6 +2457,428 @@ var init_inbox_acks = __esm({
|
|
|
2392
2457
|
}
|
|
2393
2458
|
});
|
|
2394
2459
|
|
|
2460
|
+
// src/integrations/slack/files.ts
|
|
2461
|
+
var files_exports = {};
|
|
2462
|
+
__export(files_exports, {
|
|
2463
|
+
INGEST_TIMEOUT_MS: () => INGEST_TIMEOUT_MS,
|
|
2464
|
+
MAX_BYTES: () => MAX_BYTES,
|
|
2465
|
+
formatFileBlock: () => formatFileBlock,
|
|
2466
|
+
ingestSlackFiles: () => ingestSlackFiles
|
|
2467
|
+
});
|
|
2468
|
+
function inferFileName(file) {
|
|
2469
|
+
return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
|
|
2470
|
+
}
|
|
2471
|
+
function inferContentType(file) {
|
|
2472
|
+
if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
|
|
2473
|
+
return "application/octet-stream";
|
|
2474
|
+
}
|
|
2475
|
+
function formatBytes(n) {
|
|
2476
|
+
if (!Number.isFinite(n) || n <= 0) return "?";
|
|
2477
|
+
if (n < 1024) return `${n} B`;
|
|
2478
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
2479
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
2480
|
+
}
|
|
2481
|
+
function sleep(ms) {
|
|
2482
|
+
return new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
2483
|
+
}
|
|
2484
|
+
function imageMagic(bytes) {
|
|
2485
|
+
if (bytes.length < 12) return null;
|
|
2486
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
|
|
2487
|
+
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return "image/png";
|
|
2488
|
+
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return "image/gif";
|
|
2489
|
+
if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "image/webp";
|
|
2490
|
+
return null;
|
|
2491
|
+
}
|
|
2492
|
+
function requiresRasterMagic(contentType) {
|
|
2493
|
+
const normalized = contentType.toLowerCase().split(";", 1)[0].trim();
|
|
2494
|
+
return normalized === "image/jpeg" || normalized === "image/jpg" || normalized === "image/png" || normalized === "image/gif" || normalized === "image/webp";
|
|
2495
|
+
}
|
|
2496
|
+
function validateDownloadedBytes(bytes, declaredContentType) {
|
|
2497
|
+
if (!requiresRasterMagic(declaredContentType)) return null;
|
|
2498
|
+
const actual = imageMagic(bytes);
|
|
2499
|
+
if (!actual) return "invalid_image_bytes";
|
|
2500
|
+
return null;
|
|
2501
|
+
}
|
|
2502
|
+
async function fetchSlackPrivateFile(sourceUrl, botToken) {
|
|
2503
|
+
let lastError = "unknown";
|
|
2504
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
2505
|
+
try {
|
|
2506
|
+
const res = await fetch(sourceUrl, {
|
|
2507
|
+
headers: { Authorization: `Bearer ${botToken}` }
|
|
2508
|
+
});
|
|
2509
|
+
if (res.status === 429 || res.status >= 500) {
|
|
2510
|
+
lastError = `slack_fetch_${res.status}`;
|
|
2511
|
+
const retryAfter = Number(res.headers.get("retry-after"));
|
|
2512
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1e3, 2e3) : 250 * (attempt + 1);
|
|
2513
|
+
if (attempt < 2) {
|
|
2514
|
+
await sleep(waitMs);
|
|
2515
|
+
continue;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
if (!res.ok) {
|
|
2519
|
+
return { ok: false, error: `slack_fetch_${res.status}` };
|
|
2520
|
+
}
|
|
2521
|
+
const contentLength = Number(res.headers.get("content-length"));
|
|
2522
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_BYTES) {
|
|
2523
|
+
return { ok: false, error: "size_exceeded" };
|
|
2524
|
+
}
|
|
2525
|
+
const ab = await res.arrayBuffer();
|
|
2526
|
+
if (ab.byteLength > MAX_BYTES) {
|
|
2527
|
+
return { ok: false, error: "size_exceeded" };
|
|
2528
|
+
}
|
|
2529
|
+
return { ok: true, bytes: Buffer.from(ab) };
|
|
2530
|
+
} catch (err) {
|
|
2531
|
+
lastError = `slack_fetch_error:${err?.message || "unknown"}`;
|
|
2532
|
+
if (attempt < 2) {
|
|
2533
|
+
await sleep(250 * (attempt + 1));
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
return { ok: false, error: lastError };
|
|
2539
|
+
}
|
|
2540
|
+
function withTimeout(p, ms, label) {
|
|
2541
|
+
return new Promise((resolve11, reject) => {
|
|
2542
|
+
const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
|
|
2543
|
+
p.then(
|
|
2544
|
+
(v) => {
|
|
2545
|
+
clearTimeout(t);
|
|
2546
|
+
resolve11(v);
|
|
2547
|
+
},
|
|
2548
|
+
(e) => {
|
|
2549
|
+
clearTimeout(t);
|
|
2550
|
+
reject(e);
|
|
2551
|
+
}
|
|
2552
|
+
);
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
async function ingestOne(file, sessionId, botToken) {
|
|
2556
|
+
const fileName = inferFileName(file);
|
|
2557
|
+
const contentType = inferContentType(file);
|
|
2558
|
+
const declaredSize = typeof file.size === "number" ? file.size : 0;
|
|
2559
|
+
const base = {
|
|
2560
|
+
slackFileId: file.id,
|
|
2561
|
+
fileName,
|
|
2562
|
+
contentType,
|
|
2563
|
+
sizeBytes: declaredSize
|
|
2564
|
+
};
|
|
2565
|
+
const sourceUrl = file.url_private_download || file.url_private;
|
|
2566
|
+
if (!sourceUrl || typeof sourceUrl !== "string") {
|
|
2567
|
+
return { ...base, shortUrl: null, error: "no_source_url" };
|
|
2568
|
+
}
|
|
2569
|
+
if (declaredSize > MAX_BYTES) {
|
|
2570
|
+
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
2571
|
+
}
|
|
2572
|
+
let bytes;
|
|
2573
|
+
const fetched = await fetchSlackPrivateFile(sourceUrl, botToken);
|
|
2574
|
+
if (!fetched.ok) {
|
|
2575
|
+
return { ...base, shortUrl: null, error: fetched.error };
|
|
2576
|
+
}
|
|
2577
|
+
bytes = fetched.bytes;
|
|
2578
|
+
const byteError = validateDownloadedBytes(bytes, contentType);
|
|
2579
|
+
if (byteError) {
|
|
2580
|
+
console.warn(
|
|
2581
|
+
`[slack-files] refusing to upload ${fileName}: Slack metadata says ${contentType}, but downloaded bytes are not a supported image (${bytes.slice(0, 32).toString("utf8").replace(/\s+/g, " ").slice(0, 32)})`
|
|
2582
|
+
);
|
|
2583
|
+
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: byteError };
|
|
2584
|
+
}
|
|
2585
|
+
const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
2586
|
+
let upload;
|
|
2587
|
+
try {
|
|
2588
|
+
upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
|
|
2589
|
+
} catch (err) {
|
|
2590
|
+
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
|
|
2591
|
+
}
|
|
2592
|
+
try {
|
|
2593
|
+
const putRes = await fetch(upload.uploadUrl, {
|
|
2594
|
+
method: "PUT",
|
|
2595
|
+
headers: { "Content-Type": contentType },
|
|
2596
|
+
body: bytes
|
|
2597
|
+
});
|
|
2598
|
+
if (!putRes.ok) {
|
|
2599
|
+
return {
|
|
2600
|
+
...base,
|
|
2601
|
+
sizeBytes: bytes.length,
|
|
2602
|
+
shortUrl: null,
|
|
2603
|
+
error: `gcs_put_${putRes.status}`
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
} catch (err) {
|
|
2607
|
+
return {
|
|
2608
|
+
...base,
|
|
2609
|
+
sizeBytes: bytes.length,
|
|
2610
|
+
shortUrl: null,
|
|
2611
|
+
error: `gcs_put_error:${err?.message || "unknown"}`
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
try {
|
|
2615
|
+
await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
|
|
2616
|
+
} catch (err) {
|
|
2617
|
+
console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
|
|
2618
|
+
}
|
|
2619
|
+
const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
|
|
2620
|
+
// server somehow forgot to return it (older remote-server versions).
|
|
2621
|
+
inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
|
|
2622
|
+
return {
|
|
2623
|
+
...base,
|
|
2624
|
+
sizeBytes: bytes.length,
|
|
2625
|
+
shortUrl
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
|
|
2629
|
+
try {
|
|
2630
|
+
const u = new URL(uploadUrl);
|
|
2631
|
+
if (u.hostname.endsWith(".googleapis.com")) return null;
|
|
2632
|
+
return `${u.origin}/f/${fileId}`;
|
|
2633
|
+
} catch {
|
|
2634
|
+
return null;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
async function ingestSlackFiles(files, sessionId, options = {}) {
|
|
2638
|
+
if (!Array.isArray(files) || files.length === 0) return [];
|
|
2639
|
+
const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
2640
|
+
if (!isRemoteConfigured2()) {
|
|
2641
|
+
console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
|
|
2642
|
+
return files.map((f) => ({
|
|
2643
|
+
slackFileId: f.id,
|
|
2644
|
+
fileName: inferFileName(f),
|
|
2645
|
+
contentType: inferContentType(f),
|
|
2646
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
2647
|
+
shortUrl: null,
|
|
2648
|
+
error: "storage_unconfigured"
|
|
2649
|
+
}));
|
|
2650
|
+
}
|
|
2651
|
+
const botToken = getSlackBotToken();
|
|
2652
|
+
if (!botToken) {
|
|
2653
|
+
console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
|
|
2654
|
+
return files.map((f) => ({
|
|
2655
|
+
slackFileId: f.id,
|
|
2656
|
+
fileName: inferFileName(f),
|
|
2657
|
+
contentType: inferContentType(f),
|
|
2658
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
2659
|
+
shortUrl: null,
|
|
2660
|
+
error: "no_bot_token"
|
|
2661
|
+
}));
|
|
2662
|
+
}
|
|
2663
|
+
const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
|
|
2664
|
+
const startedAt = Date.now();
|
|
2665
|
+
const pipeline = Promise.allSettled(
|
|
2666
|
+
files.map(
|
|
2667
|
+
(f) => withTimeout(ingestOne(f, sessionId, botToken), timeoutMs, `ingest:${f.id}`).catch((err) => {
|
|
2668
|
+
if (String(err?.message || err).includes("_timeout")) {
|
|
2669
|
+
return {
|
|
2670
|
+
slackFileId: f.id,
|
|
2671
|
+
fileName: inferFileName(f),
|
|
2672
|
+
contentType: inferContentType(f),
|
|
2673
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
2674
|
+
shortUrl: null,
|
|
2675
|
+
error: "timeout"
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
throw err;
|
|
2679
|
+
})
|
|
2680
|
+
)
|
|
2681
|
+
);
|
|
2682
|
+
const settled = await pipeline;
|
|
2683
|
+
const results = settled.map((s, i) => {
|
|
2684
|
+
if (s.status === "fulfilled") return s.value;
|
|
2685
|
+
const f = files[i];
|
|
2686
|
+
return {
|
|
2687
|
+
slackFileId: f.id,
|
|
2688
|
+
fileName: inferFileName(f),
|
|
2689
|
+
contentType: inferContentType(f),
|
|
2690
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
2691
|
+
shortUrl: null,
|
|
2692
|
+
error: `unexpected:${s.reason?.message || String(s.reason)}`
|
|
2693
|
+
};
|
|
2694
|
+
});
|
|
2695
|
+
const okCount = results.filter((r) => r.shortUrl).length;
|
|
2696
|
+
console.log(
|
|
2697
|
+
`[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
|
|
2698
|
+
);
|
|
2699
|
+
return results;
|
|
2700
|
+
}
|
|
2701
|
+
function formatFileBlock(files) {
|
|
2702
|
+
if (!files || files.length === 0) return "";
|
|
2703
|
+
const lines = ["[files]"];
|
|
2704
|
+
for (const f of files) {
|
|
2705
|
+
const sizeLabel = formatBytes(f.sizeBytes);
|
|
2706
|
+
if (f.shortUrl) {
|
|
2707
|
+
lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
|
|
2708
|
+
} else {
|
|
2709
|
+
lines.push(
|
|
2710
|
+
` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
return lines.join("\n");
|
|
2715
|
+
}
|
|
2716
|
+
var MAX_BYTES, INGEST_TIMEOUT_MS;
|
|
2717
|
+
var init_files = __esm({
|
|
2718
|
+
"src/integrations/slack/files.ts"() {
|
|
2719
|
+
"use strict";
|
|
2720
|
+
init_client2();
|
|
2721
|
+
MAX_BYTES = 100 * 1024 * 1024;
|
|
2722
|
+
INGEST_TIMEOUT_MS = 2500;
|
|
2723
|
+
}
|
|
2724
|
+
});
|
|
2725
|
+
|
|
2726
|
+
// src/integrations/slack/read.ts
|
|
2727
|
+
var read_exports = {};
|
|
2728
|
+
__export(read_exports, {
|
|
2729
|
+
findChannelByName: () => findChannelByName,
|
|
2730
|
+
findUsers: () => findUsers,
|
|
2731
|
+
getChannelHistory: () => getChannelHistory,
|
|
2732
|
+
getPermalink: () => getPermalink,
|
|
2733
|
+
getThreadReplies: () => getThreadReplies,
|
|
2734
|
+
ingestMessageFiles: () => ingestMessageFiles
|
|
2735
|
+
});
|
|
2736
|
+
async function slackGet(method, params) {
|
|
2737
|
+
const token = getSlackBotToken();
|
|
2738
|
+
if (!token) return { ok: false, error: "slack_not_configured" };
|
|
2739
|
+
try {
|
|
2740
|
+
const qs = new URLSearchParams(params).toString();
|
|
2741
|
+
const res = await fetch(`https://slack.com/api/${method}?${qs}`, {
|
|
2742
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
2743
|
+
});
|
|
2744
|
+
const json = await res.json().catch(() => ({}));
|
|
2745
|
+
if (!json?.ok) return { ok: false, error: json?.error || `HTTP ${res.status}` };
|
|
2746
|
+
return { ok: true, json };
|
|
2747
|
+
} catch (err) {
|
|
2748
|
+
return { ok: false, error: err?.message || "unknown" };
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
function clampLimit(n, def, max) {
|
|
2752
|
+
const v = typeof n === "number" && Number.isFinite(n) ? n : def;
|
|
2753
|
+
return String(Math.min(Math.max(Math.floor(v), 1), max));
|
|
2754
|
+
}
|
|
2755
|
+
function liteMessage(m) {
|
|
2756
|
+
return {
|
|
2757
|
+
ts: String(m?.ts ?? ""),
|
|
2758
|
+
threadTs: typeof m?.thread_ts === "string" ? m.thread_ts : void 0,
|
|
2759
|
+
user: typeof m?.user === "string" ? m.user : void 0,
|
|
2760
|
+
botId: typeof m?.bot_id === "string" ? m.bot_id : void 0,
|
|
2761
|
+
text: typeof m?.text === "string" ? m.text.slice(0, 4e3) : "",
|
|
2762
|
+
files: Array.isArray(m?.files) ? m.files.map((f) => ({ id: String(f?.id ?? ""), name: f?.name || f?.title, mimetype: f?.mimetype, size: typeof f?.size === "number" ? f.size : void 0 })) : void 0,
|
|
2763
|
+
replyCount: typeof m?.reply_count === "number" ? m.reply_count : void 0
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
async function enrichUserNames(messages) {
|
|
2767
|
+
const ids = [...new Set(messages.map((m) => m.user).filter((u) => !!u))];
|
|
2768
|
+
const names = /* @__PURE__ */ new Map();
|
|
2769
|
+
await Promise.all(
|
|
2770
|
+
ids.map(async (id) => {
|
|
2771
|
+
const info = await resolveSlackUserInfo(id).catch(() => null);
|
|
2772
|
+
if (info?.name) names.set(id, info.name);
|
|
2773
|
+
})
|
|
2774
|
+
);
|
|
2775
|
+
return messages.map((m) => m.user && names.has(m.user) ? { ...m, userName: names.get(m.user) } : m);
|
|
2776
|
+
}
|
|
2777
|
+
async function getChannelHistory(channel, limit) {
|
|
2778
|
+
const r = await slackGet("conversations.history", { channel, limit: clampLimit(limit, 20, 100) });
|
|
2779
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
2780
|
+
const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
|
|
2781
|
+
return { ok: true, data: await enrichUserNames(messages) };
|
|
2782
|
+
}
|
|
2783
|
+
async function getThreadReplies(channel, threadTs, limit) {
|
|
2784
|
+
const r = await slackGet("conversations.replies", { channel, ts: threadTs, limit: clampLimit(limit, 50, 200) });
|
|
2785
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
2786
|
+
const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
|
|
2787
|
+
return { ok: true, data: await enrichUserNames(messages) };
|
|
2788
|
+
}
|
|
2789
|
+
async function getPermalink(channel, messageTs) {
|
|
2790
|
+
const r = await slackGet("chat.getPermalink", { channel, message_ts: messageTs });
|
|
2791
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
2792
|
+
return { ok: true, data: { permalink: String(r.json.permalink ?? "") } };
|
|
2793
|
+
}
|
|
2794
|
+
async function findChannelByName(name) {
|
|
2795
|
+
const clean = name.replace(/^#/, "").trim().toLowerCase();
|
|
2796
|
+
let cursor;
|
|
2797
|
+
for (let page = 0; page < 10; page++) {
|
|
2798
|
+
const params = {
|
|
2799
|
+
types: "public_channel,private_channel",
|
|
2800
|
+
exclude_archived: "true",
|
|
2801
|
+
limit: "999"
|
|
2802
|
+
};
|
|
2803
|
+
if (cursor) params.cursor = cursor;
|
|
2804
|
+
const r = await slackGet("conversations.list", params);
|
|
2805
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
2806
|
+
const match = (r.json.channels || []).find((c) => String(c?.name ?? "").toLowerCase() === clean);
|
|
2807
|
+
if (match) return { ok: true, data: { id: String(match.id), name: String(match.name) } };
|
|
2808
|
+
cursor = r.json.response_metadata?.next_cursor || "";
|
|
2809
|
+
if (!cursor) break;
|
|
2810
|
+
}
|
|
2811
|
+
return { ok: false, error: "channel_not_found" };
|
|
2812
|
+
}
|
|
2813
|
+
async function findUsers(query, max = 10) {
|
|
2814
|
+
const q = query.trim().toLowerCase();
|
|
2815
|
+
if (!q) return { ok: false, error: "empty_query" };
|
|
2816
|
+
if (q.includes("@")) {
|
|
2817
|
+
const r = await slackGet("users.lookupByEmail", { email: query.trim() });
|
|
2818
|
+
if (r.ok && r.json.user) {
|
|
2819
|
+
const u = r.json.user;
|
|
2820
|
+
return { ok: true, data: [{ id: String(u.id), name: u.profile?.display_name || u.real_name || u.name, realName: u.real_name, email: u.profile?.email }] };
|
|
2821
|
+
}
|
|
2822
|
+
if (r.error && !["users_not_found", "user_not_found", "not_found"].includes(r.error)) {
|
|
2823
|
+
return { ok: false, error: r.error };
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
const matches = [];
|
|
2827
|
+
let cursor;
|
|
2828
|
+
for (let page = 0; page < 10 && matches.length < max; page++) {
|
|
2829
|
+
const params = { limit: "1000" };
|
|
2830
|
+
if (cursor) params.cursor = cursor;
|
|
2831
|
+
const r = await slackGet("users.list", params);
|
|
2832
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
2833
|
+
for (const u of r.json.members || []) {
|
|
2834
|
+
if (u?.deleted || u?.is_bot) continue;
|
|
2835
|
+
const dn = String(u?.profile?.display_name_normalized || u?.profile?.real_name || u?.name || "");
|
|
2836
|
+
const email = String(u?.profile?.email || "");
|
|
2837
|
+
if (dn.toLowerCase().includes(q) || email.toLowerCase().includes(q)) {
|
|
2838
|
+
matches.push({ id: String(u.id), name: dn || String(u.name), realName: u?.profile?.real_name, email: email || void 0 });
|
|
2839
|
+
if (matches.length >= max) break;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
cursor = r.json.response_metadata?.next_cursor || "";
|
|
2843
|
+
if (!cursor) break;
|
|
2844
|
+
}
|
|
2845
|
+
return matches.length > 0 ? { ok: true, data: matches } : { ok: false, error: "no_match" };
|
|
2846
|
+
}
|
|
2847
|
+
async function getSingleMessage(channel, ts, threadTs) {
|
|
2848
|
+
if (threadTs) {
|
|
2849
|
+
const rr2 = await slackGet("conversations.replies", { channel, ts: threadTs, limit: "200" });
|
|
2850
|
+
if (rr2.ok && Array.isArray(rr2.json.messages)) {
|
|
2851
|
+
const match = rr2.json.messages.find((m) => m?.ts === ts);
|
|
2852
|
+
if (match) return match;
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
const r = await slackGet("conversations.history", { channel, latest: ts, oldest: ts, inclusive: "true", limit: "1" });
|
|
2856
|
+
if (r.ok && Array.isArray(r.json.messages) && r.json.messages[0]) return r.json.messages[0];
|
|
2857
|
+
const rr = await slackGet("conversations.replies", { channel, ts, limit: "1" });
|
|
2858
|
+
if (rr.ok && Array.isArray(rr.json.messages)) {
|
|
2859
|
+
return rr.json.messages.find((m) => m?.ts === ts) || rr.json.messages[0] || null;
|
|
2860
|
+
}
|
|
2861
|
+
return null;
|
|
2862
|
+
}
|
|
2863
|
+
async function ingestMessageFiles(channel, ts, orchestratorSessionId, threadTs) {
|
|
2864
|
+
const msg = await getSingleMessage(channel, ts, threadTs);
|
|
2865
|
+
if (!msg) return { ok: false, error: "message_not_found" };
|
|
2866
|
+
const files = Array.isArray(msg.files) ? msg.files : [];
|
|
2867
|
+
if (files.length === 0) return { ok: true, data: [] };
|
|
2868
|
+
const { ingestSlackFiles: ingestSlackFiles2 } = await Promise.resolve().then(() => (init_files(), files_exports));
|
|
2869
|
+
const ingested = await ingestSlackFiles2(files, orchestratorSessionId);
|
|
2870
|
+
return {
|
|
2871
|
+
ok: true,
|
|
2872
|
+
data: ingested.map((f) => ({ name: f.fileName, url: f.shortUrl, error: f.error }))
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2875
|
+
var init_read = __esm({
|
|
2876
|
+
"src/integrations/slack/read.ts"() {
|
|
2877
|
+
"use strict";
|
|
2878
|
+
init_client2();
|
|
2879
|
+
}
|
|
2880
|
+
});
|
|
2881
|
+
|
|
2395
2882
|
// src/browser/stream-proxy.ts
|
|
2396
2883
|
var stream_proxy_exports = {};
|
|
2397
2884
|
__export(stream_proxy_exports, {
|
|
@@ -7734,6 +8221,99 @@ function capImageCount(messages, max = MAX_IMAGES_IN_CONTEXT) {
|
|
|
7734
8221
|
return mutated ? out : messages;
|
|
7735
8222
|
}
|
|
7736
8223
|
|
|
8224
|
+
// src/utils/sanitize-images.ts
|
|
8225
|
+
var SUPPORTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
8226
|
+
var INVALID_IMAGE_PLACEHOLDER = "[invalid image omitted \u2014 the data was not a valid jpeg/png/gif/webp]";
|
|
8227
|
+
function hasImageMagic(bytes) {
|
|
8228
|
+
if (bytes.length < 12) return false;
|
|
8229
|
+
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return true;
|
|
8230
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return true;
|
|
8231
|
+
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return true;
|
|
8232
|
+
if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return true;
|
|
8233
|
+
return false;
|
|
8234
|
+
}
|
|
8235
|
+
function extractBase64(part) {
|
|
8236
|
+
const raw = typeof part?.data === "string" ? part.data : typeof part?.image === "string" ? part.image : null;
|
|
8237
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
8238
|
+
if (/^https?:\/\//i.test(raw)) return null;
|
|
8239
|
+
const dataUrl = raw.match(/^data:[^;,]+;base64,([\s\S]*)$/);
|
|
8240
|
+
return dataUrl ? dataUrl[1] : raw;
|
|
8241
|
+
}
|
|
8242
|
+
function isInvalidImagePart(part) {
|
|
8243
|
+
if (!part || typeof part !== "object") return false;
|
|
8244
|
+
const t = part.type;
|
|
8245
|
+
if (t !== "image" && t !== "image-data" && t !== "media") return false;
|
|
8246
|
+
const mt = part.mediaType;
|
|
8247
|
+
const b64 = extractBase64(part);
|
|
8248
|
+
if (b64 === null) {
|
|
8249
|
+
return typeof mt === "string" && mt.startsWith("image/") && !SUPPORTED_IMAGE_TYPES.includes(mt);
|
|
8250
|
+
}
|
|
8251
|
+
let bytes;
|
|
8252
|
+
try {
|
|
8253
|
+
bytes = Buffer.from(b64, "base64");
|
|
8254
|
+
} catch {
|
|
8255
|
+
return true;
|
|
8256
|
+
}
|
|
8257
|
+
if (bytes.length === 0) return true;
|
|
8258
|
+
return !hasImageMagic(bytes);
|
|
8259
|
+
}
|
|
8260
|
+
function placeholder() {
|
|
8261
|
+
return { type: "text", text: INVALID_IMAGE_PLACEHOLDER };
|
|
8262
|
+
}
|
|
8263
|
+
function sanitizeInvalidImages(messages) {
|
|
8264
|
+
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
8265
|
+
let mutated = false;
|
|
8266
|
+
let dropped = 0;
|
|
8267
|
+
const out = messages.slice();
|
|
8268
|
+
for (let i = 0; i < out.length; i++) {
|
|
8269
|
+
const msg = out[i];
|
|
8270
|
+
if (!Array.isArray(msg.content)) continue;
|
|
8271
|
+
let contentCloned = false;
|
|
8272
|
+
const ensureCloned = () => {
|
|
8273
|
+
if (contentCloned) return;
|
|
8274
|
+
out[i] = { ...msg, content: [...msg.content] };
|
|
8275
|
+
contentCloned = true;
|
|
8276
|
+
};
|
|
8277
|
+
const content = () => out[i].content;
|
|
8278
|
+
for (let j = 0; j < content().length; j++) {
|
|
8279
|
+
const part = content()[j];
|
|
8280
|
+
if (isInvalidImagePart(part)) {
|
|
8281
|
+
ensureCloned();
|
|
8282
|
+
out[i].content[j] = placeholder();
|
|
8283
|
+
mutated = true;
|
|
8284
|
+
dropped++;
|
|
8285
|
+
continue;
|
|
8286
|
+
}
|
|
8287
|
+
if (part && typeof part === "object" && part.type === "tool-result" && part.output && part.output.type === "content" && Array.isArray(part.output.value)) {
|
|
8288
|
+
const innerValue = part.output.value;
|
|
8289
|
+
let innerMutated = false;
|
|
8290
|
+
const newValue = innerValue.slice();
|
|
8291
|
+
for (let k = 0; k < newValue.length; k++) {
|
|
8292
|
+
if (isInvalidImagePart(newValue[k])) {
|
|
8293
|
+
newValue[k] = placeholder();
|
|
8294
|
+
innerMutated = true;
|
|
8295
|
+
dropped++;
|
|
8296
|
+
}
|
|
8297
|
+
}
|
|
8298
|
+
if (innerMutated) {
|
|
8299
|
+
ensureCloned();
|
|
8300
|
+
out[i].content[j] = {
|
|
8301
|
+
...part,
|
|
8302
|
+
output: { ...part.output, value: newValue }
|
|
8303
|
+
};
|
|
8304
|
+
mutated = true;
|
|
8305
|
+
}
|
|
8306
|
+
}
|
|
8307
|
+
}
|
|
8308
|
+
}
|
|
8309
|
+
if (mutated) {
|
|
8310
|
+
console.warn(
|
|
8311
|
+
`[sanitize-images] Replaced ${dropped} invalid image part(s) with a text placeholder (non-image bytes / unsupported type) to avoid provider 400 "invalid image" errors.`
|
|
8312
|
+
);
|
|
8313
|
+
}
|
|
8314
|
+
return mutated ? out : messages;
|
|
8315
|
+
}
|
|
8316
|
+
|
|
7737
8317
|
// src/agent/model-limits.ts
|
|
7738
8318
|
var MODEL_LIMITS = {
|
|
7739
8319
|
"claude-opus-4-8": { contextWindow: 2e5, rollingTarget: 15e4 },
|
|
@@ -7813,6 +8393,7 @@ ${summaryContent}`
|
|
|
7813
8393
|
messages = repairToolPairing(messages);
|
|
7814
8394
|
messages = ensureToolResultsFollowCalls(messages);
|
|
7815
8395
|
messages = ensureEndsWithUserOrTool(messages);
|
|
8396
|
+
messages = sanitizeInvalidImages(messages);
|
|
7816
8397
|
messages = capImageCount(messages);
|
|
7817
8398
|
messages = this.enforceHardCap(messages);
|
|
7818
8399
|
return messages;
|
|
@@ -7824,28 +8405,35 @@ ${summaryContent}`
|
|
|
7824
8405
|
* repairs tool pairing so dropping can't orphan a tool result.
|
|
7825
8406
|
*/
|
|
7826
8407
|
enforceHardCap(messages) {
|
|
7827
|
-
const {
|
|
7828
|
-
const
|
|
8408
|
+
const { rollingTarget } = getModelLimits(this.modelId);
|
|
8409
|
+
const MAX_MESSAGES = 120;
|
|
8410
|
+
const tokenCeiling = Math.max(2e4, Math.floor(rollingTarget * 1.5));
|
|
7829
8411
|
const tokens = messages.map((m) => this.messageTokens(m));
|
|
7830
8412
|
let total = tokens.reduce((a, b) => a + b, 0);
|
|
7831
|
-
if (total <= ceiling) return messages;
|
|
7832
8413
|
const hasLeadingSummary = messages.length > 0 && messages[0].role === "system";
|
|
7833
8414
|
const firstBody = hasLeadingSummary ? 1 : 0;
|
|
7834
8415
|
const lastIndex = messages.length - 1;
|
|
8416
|
+
const bodyCount = messages.length - firstBody;
|
|
8417
|
+
if (bodyCount <= MAX_MESSAGES && total <= tokenCeiling) return messages;
|
|
7835
8418
|
let start = firstBody;
|
|
7836
|
-
|
|
8419
|
+
const countDropTarget = messages.length - MAX_MESSAGES;
|
|
8420
|
+
while (start < countDropTarget && start < lastIndex) {
|
|
8421
|
+
total -= tokens[start];
|
|
8422
|
+
start += 1;
|
|
8423
|
+
}
|
|
8424
|
+
while (start < lastIndex && total > tokenCeiling) {
|
|
7837
8425
|
total -= tokens[start];
|
|
7838
8426
|
start += 1;
|
|
7839
8427
|
}
|
|
7840
8428
|
let out = hasLeadingSummary ? [messages[0], ...messages.slice(start)] : messages.slice(start);
|
|
7841
|
-
if (total >
|
|
8429
|
+
if (total > tokenCeiling) {
|
|
7842
8430
|
out = out.map((m) => hardTruncateMessageText(m));
|
|
7843
8431
|
}
|
|
7844
8432
|
out = repairToolPairing(out);
|
|
7845
8433
|
out = ensureToolResultsFollowCalls(out);
|
|
7846
8434
|
out = ensureEndsWithUserOrTool(out);
|
|
7847
8435
|
console.warn(
|
|
7848
|
-
`[Context] hard cap engaged for ${this.modelId}: trimmed ${messages.length}\u2192${out.length} msgs (ceiling ${
|
|
8436
|
+
`[Context] hard cap engaged for ${this.modelId}: trimmed ${messages.length}\u2192${out.length} msgs (ceiling ${tokenCeiling} est-tokens / ${MAX_MESSAGES} msgs).`
|
|
7849
8437
|
);
|
|
7850
8438
|
return out;
|
|
7851
8439
|
}
|
|
@@ -8427,7 +9015,16 @@ async function postMessage(opts) {
|
|
|
8427
9015
|
let ref;
|
|
8428
9016
|
switch (opts.channel) {
|
|
8429
9017
|
case "slack": {
|
|
8430
|
-
|
|
9018
|
+
let slackChannel2 = opts.to.trim();
|
|
9019
|
+
if (slackChannel2.startsWith("#")) {
|
|
9020
|
+
const { findChannelByName: findChannelByName2 } = await Promise.resolve().then(() => (init_read(), read_exports));
|
|
9021
|
+
const found = await findChannelByName2(slackChannel2);
|
|
9022
|
+
if (!found.ok || !found.data?.id) {
|
|
9023
|
+
return { ok: false, error: `slack channel lookup failed: ${found.error || "channel_not_found"}` };
|
|
9024
|
+
}
|
|
9025
|
+
slackChannel2 = found.data.id;
|
|
9026
|
+
}
|
|
9027
|
+
const slackRef = { channel: "slack", slackChannel: slackChannel2, threadTs: opts.threadTs };
|
|
8431
9028
|
ref = slackRef;
|
|
8432
9029
|
break;
|
|
8433
9030
|
}
|
|
@@ -8449,6 +9046,10 @@ function describeConfiguredChannels() {
|
|
|
8449
9046
|
}));
|
|
8450
9047
|
}
|
|
8451
9048
|
|
|
9049
|
+
// src/tools/orchestrator-actions.ts
|
|
9050
|
+
init_read();
|
|
9051
|
+
init_client2();
|
|
9052
|
+
|
|
8452
9053
|
// src/orchestrator/schedules-store.ts
|
|
8453
9054
|
init_db();
|
|
8454
9055
|
import { nanoid as nanoid5 } from "nanoid";
|
|
@@ -8757,6 +9358,55 @@ function buildMessengerTool() {
|
|
|
8757
9358
|
}
|
|
8758
9359
|
});
|
|
8759
9360
|
}
|
|
9361
|
+
var slackInputSchema = z14.object({
|
|
9362
|
+
action: z14.enum(["history", "replies", "permalink", "find_channel", "find_user", "user_info", "fetch_files"]),
|
|
9363
|
+
channel: z14.string().optional().describe("channel id (C0123/G0123/D0123). Required for history/replies/permalink/fetch_files."),
|
|
9364
|
+
threadTs: z14.string().optional().describe("replies/fetch_files: parent message ts of the thread."),
|
|
9365
|
+
messageTs: z14.string().optional().describe("permalink/fetch_files: ts of the target message; for thread-reply files also pass threadTs."),
|
|
9366
|
+
query: z14.string().optional().describe("find_channel: channel name (no #). find_user: name or email."),
|
|
9367
|
+
user: z14.string().optional().describe("user_info: user id (U0123)."),
|
|
9368
|
+
limit: z14.number().optional().describe("history/replies: max messages (history \u2264100, replies \u2264200).")
|
|
9369
|
+
});
|
|
9370
|
+
function buildSlackTool(opts) {
|
|
9371
|
+
return tool13({
|
|
9372
|
+
description: "Read from Slack like a human would. Actions: history (channel[, limit] \u2192 recent messages, newest first, with sender names + file metadata), replies (channel, threadTs[, limit] \u2192 full thread), permalink (channel, messageTs \u2192 shareable link), find_channel (query=name \u2192 channel id), find_user (query=name|email \u2192 matching members), user_info (user=U0123 \u2192 name/email), fetch_files (channel, messageTs[, threadTs] \u2192 re-uploads that message's attachments and returns fetchable URLs you can curl). Use this to scroll back, read context, and open files others posted before replying.",
|
|
9373
|
+
inputSchema: slackInputSchema,
|
|
9374
|
+
execute: async (input) => {
|
|
9375
|
+
if (!isSlackConfigured()) return { ok: false, error: "slack not configured" };
|
|
9376
|
+
switch (input.action) {
|
|
9377
|
+
case "history": {
|
|
9378
|
+
if (!input.channel) return { ok: false, error: "channel required" };
|
|
9379
|
+
return getChannelHistory(input.channel, input.limit);
|
|
9380
|
+
}
|
|
9381
|
+
case "replies": {
|
|
9382
|
+
if (!input.channel || !input.threadTs) return { ok: false, error: "channel and threadTs required" };
|
|
9383
|
+
return getThreadReplies(input.channel, input.threadTs, input.limit);
|
|
9384
|
+
}
|
|
9385
|
+
case "permalink": {
|
|
9386
|
+
if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
|
|
9387
|
+
return getPermalink(input.channel, input.messageTs);
|
|
9388
|
+
}
|
|
9389
|
+
case "find_channel": {
|
|
9390
|
+
if (!input.query) return { ok: false, error: "query (channel name) required" };
|
|
9391
|
+
return findChannelByName(input.query);
|
|
9392
|
+
}
|
|
9393
|
+
case "find_user": {
|
|
9394
|
+
if (!input.query) return { ok: false, error: "query (name or email) required" };
|
|
9395
|
+
return findUsers(input.query);
|
|
9396
|
+
}
|
|
9397
|
+
case "user_info": {
|
|
9398
|
+
if (!input.user) return { ok: false, error: "user required" };
|
|
9399
|
+
const info = await resolveSlackUserInfo(input.user);
|
|
9400
|
+
return info ? { ok: true, data: { id: input.user, ...info } } : { ok: false, error: "not_found" };
|
|
9401
|
+
}
|
|
9402
|
+
case "fetch_files": {
|
|
9403
|
+
if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
|
|
9404
|
+
return ingestMessageFiles(input.channel, input.messageTs, opts.orchestratorSessionId, input.threadTs);
|
|
9405
|
+
}
|
|
9406
|
+
}
|
|
9407
|
+
}
|
|
9408
|
+
});
|
|
9409
|
+
}
|
|
8760
9410
|
var scheduleInputSchema = z14.object({
|
|
8761
9411
|
action: z14.enum(["create", "list", "update", "delete", "pause", "resume"]),
|
|
8762
9412
|
// create / update
|
|
@@ -8857,6 +9507,7 @@ function createOrchestratorActionTools(opts) {
|
|
|
8857
9507
|
return {
|
|
8858
9508
|
agent: buildAgentTool(opts),
|
|
8859
9509
|
messenger: buildMessengerTool(),
|
|
9510
|
+
slack: buildSlackTool(opts),
|
|
8860
9511
|
schedule: buildScheduleTool(opts),
|
|
8861
9512
|
webhook: buildWebhookTool(opts)
|
|
8862
9513
|
};
|