@agent-native/core 0.111.3 → 0.112.0
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +18 -0
- package/corpus/core/package.json +3 -1
- package/corpus/core/src/cli/code-agent-executor.ts +17 -5
- package/corpus/core/src/cli/code-agent-runs.ts +43 -2
- package/corpus/core/src/cli/migration-codemod.ts +59 -9
- package/corpus/core/src/cli/multi-frontier-runs.ts +841 -0
- package/corpus/core/src/coding-tools/index.ts +27 -8
- package/corpus/core/src/embeddings/index.ts +233 -0
- package/corpus/core/src/index.ts +2 -0
- package/corpus/core/src/search/index.ts +413 -0
- package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +48 -5
- package/corpus/templates/brain/.agents/skills/brain/SKILL.md +46 -14
- package/corpus/templates/brain/README.md +34 -3
- package/corpus/templates/brain/actions/approve-proposal.ts +2 -0
- package/corpus/templates/brain/actions/claim-distillation.ts +24 -27
- package/corpus/templates/brain/actions/create-source.ts +1 -1
- package/corpus/templates/brain/actions/get-knowledge.ts +6 -2
- package/corpus/templates/brain/actions/get-pilot-report.ts +44 -8
- package/corpus/templates/brain/actions/import-capture.ts +21 -9
- package/corpus/templates/brain/actions/import-transcript.ts +42 -14
- package/corpus/templates/brain/actions/list-captures.ts +10 -1
- package/corpus/templates/brain/actions/list-distillation-queue.ts +39 -14
- package/corpus/templates/brain/actions/list-projects.ts +51 -0
- package/corpus/templates/brain/actions/list-proposals.ts +80 -4
- package/corpus/templates/brain/actions/list-sources.ts +17 -3
- package/corpus/templates/brain/actions/manage-project.ts +118 -0
- package/corpus/templates/brain/actions/mark-capture-distilled.ts +68 -20
- package/corpus/templates/brain/actions/navigate.ts +1 -0
- package/corpus/templates/brain/actions/reject-proposal.ts +6 -1
- package/corpus/templates/brain/actions/resanitize-captures.ts +159 -33
- package/corpus/templates/brain/actions/review-proposal.ts +2 -0
- package/corpus/templates/brain/actions/search-everything.ts +10 -1
- package/corpus/templates/brain/actions/search-knowledge.ts +1 -1
- package/corpus/templates/brain/actions/set-settings.ts +36 -1
- package/corpus/templates/brain/actions/update-proposal.ts +2 -0
- package/corpus/templates/brain/app/hooks/use-distillation-bridge.ts +18 -9
- package/corpus/templates/brain/app/hooks/use-navigation-state.ts +3 -0
- package/corpus/templates/brain/app/i18n-data.ts +731 -2
- package/corpus/templates/brain/app/lib/brain.ts +33 -1
- package/corpus/templates/brain/app/routes/ops.tsx +47 -0
- package/corpus/templates/brain/app/routes/review.tsx +41 -2
- package/corpus/templates/brain/app/routes/search.tsx +33 -2
- package/corpus/templates/brain/app/routes/settings.tsx +158 -0
- package/corpus/templates/brain/app/routes/sources.tsx +31 -0
- package/corpus/templates/brain/changelog/2026-07-19-added-private-source-safe-semantic-search.md +6 -0
- package/corpus/templates/brain/jobs/process-ingest-queue.ts +270 -36
- package/corpus/templates/brain/package.json +1 -0
- package/corpus/templates/brain/server/db/index.ts +18 -0
- package/corpus/templates/brain/server/db/schema.ts +197 -1
- package/corpus/templates/brain/server/jobs/distillation-queue.ts +12 -5
- package/corpus/templates/brain/server/lib/audiences.ts +708 -0
- package/corpus/templates/brain/server/lib/brain-health.ts +251 -38
- package/corpus/templates/brain/server/lib/brain.ts +877 -50
- package/corpus/templates/brain/server/lib/capture-sanitization.ts +217 -53
- package/corpus/templates/brain/server/lib/connectors.ts +556 -70
- package/corpus/templates/brain/server/lib/demo.ts +17 -4
- package/corpus/templates/brain/server/lib/hybrid-search.ts +363 -0
- package/corpus/templates/brain/server/lib/ingest-queue.ts +104 -0
- package/corpus/templates/brain/server/lib/privacy-readiness.ts +24 -0
- package/corpus/templates/brain/server/lib/search-index-contracts.ts +72 -0
- package/corpus/templates/brain/server/lib/search-index.ts +714 -0
- package/corpus/templates/brain/server/lib/search.ts +120 -4
- package/corpus/templates/brain/server/lib/sensitivity-policy.ts +174 -0
- package/corpus/templates/brain/server/lib/slack-events.ts +281 -0
- package/corpus/templates/brain/server/onboarding.ts +29 -0
- package/corpus/templates/brain/server/plugins/db.ts +204 -0
- package/corpus/templates/brain/server/register-secrets.ts +46 -0
- package/corpus/templates/brain/server/routes/api/_agent-native/brain/ingest.post.ts +36 -16
- package/corpus/templates/brain/server/routes/api/_agent-native/brain/slack-events.post.ts +28 -0
- package/corpus/templates/brain/shared/types.ts +10 -0
- package/dist/cli/code-agent-executor.d.ts +8 -9
- package/dist/cli/code-agent-executor.d.ts.map +1 -1
- package/dist/cli/code-agent-executor.js +9 -3
- package/dist/cli/code-agent-executor.js.map +1 -1
- package/dist/cli/code-agent-runs.d.ts +5 -0
- package/dist/cli/code-agent-runs.d.ts.map +1 -1
- package/dist/cli/code-agent-runs.js +33 -2
- package/dist/cli/code-agent-runs.js.map +1 -1
- package/dist/cli/migration-codemod.d.ts.map +1 -1
- package/dist/cli/migration-codemod.js +48 -10
- package/dist/cli/migration-codemod.js.map +1 -1
- package/dist/cli/multi-frontier-runs.d.ts +131 -0
- package/dist/cli/multi-frontier-runs.d.ts.map +1 -0
- package/dist/cli/multi-frontier-runs.js +529 -0
- package/dist/cli/multi-frontier-runs.js.map +1 -0
- package/dist/coding-tools/index.d.ts +1 -0
- package/dist/coding-tools/index.d.ts.map +1 -1
- package/dist/coding-tools/index.js +25 -7
- package/dist/coding-tools/index.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/embeddings/index.d.ts +24 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +172 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/search/index.d.ts +88 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +267 -0
- package/dist/search/index.js.map +1 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +3 -1
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { getCredentialContext } from "@agent-native/core/server";
|
|
2
2
|
import { accessFilter, assertAccess } from "@agent-native/core/sharing";
|
|
3
|
-
import { and, desc, eq } from "drizzle-orm";
|
|
3
|
+
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
BrainCaptureKind,
|
|
7
7
|
BrainSourceProvider,
|
|
8
8
|
} from "../../shared/types.js";
|
|
9
9
|
import { getDb, schema } from "../db/index.js";
|
|
10
|
+
import { listAccessibleAudienceIds } from "./audiences.js";
|
|
10
11
|
import {
|
|
12
|
+
BrainCaptureBlockedError,
|
|
11
13
|
createCapture,
|
|
12
14
|
nanoid,
|
|
13
15
|
nowIso,
|
|
14
16
|
parseJson,
|
|
17
|
+
readBrainSettings,
|
|
15
18
|
serializeCapture,
|
|
16
19
|
stableJson,
|
|
17
20
|
} from "./brain.js";
|
|
@@ -31,6 +34,19 @@ export interface ConnectorSyncResult {
|
|
|
31
34
|
stats?: Record<string, unknown>;
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
async function createConnectorCapture(
|
|
38
|
+
values: Parameters<typeof createCapture>[0],
|
|
39
|
+
) {
|
|
40
|
+
try {
|
|
41
|
+
return { capture: await createCapture(values), blocked: false };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof BrainCaptureBlockedError) {
|
|
44
|
+
return { capture: null, blocked: true };
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
export interface SlackPilotOptions {
|
|
35
51
|
readHistory?: boolean;
|
|
36
52
|
channelRefs?: string[];
|
|
@@ -145,6 +161,7 @@ interface SlackChannelCursor {
|
|
|
145
161
|
|
|
146
162
|
interface SlackSyncCursor {
|
|
147
163
|
channels?: Record<string, SlackChannelCursor>;
|
|
164
|
+
publicChannelOffset?: number;
|
|
148
165
|
retry?: RetryCursor;
|
|
149
166
|
lastRunAt?: string;
|
|
150
167
|
}
|
|
@@ -164,6 +181,7 @@ interface SlackChannel {
|
|
|
164
181
|
is_channel?: boolean;
|
|
165
182
|
is_group?: boolean;
|
|
166
183
|
is_archived?: boolean;
|
|
184
|
+
is_private?: boolean;
|
|
167
185
|
}
|
|
168
186
|
|
|
169
187
|
interface SlackMessage {
|
|
@@ -175,6 +193,7 @@ interface SlackMessage {
|
|
|
175
193
|
text?: string;
|
|
176
194
|
ts?: string;
|
|
177
195
|
thread_ts?: string;
|
|
196
|
+
reactions?: Array<{ name?: string; count?: number }>;
|
|
178
197
|
}
|
|
179
198
|
|
|
180
199
|
interface SlackHistoryResponse {
|
|
@@ -183,6 +202,10 @@ interface SlackHistoryResponse {
|
|
|
183
202
|
response_metadata?: { next_cursor?: string };
|
|
184
203
|
}
|
|
185
204
|
|
|
205
|
+
interface SlackRepliesResponse {
|
|
206
|
+
messages?: SlackMessage[];
|
|
207
|
+
}
|
|
208
|
+
|
|
186
209
|
interface SlackInfoResponse {
|
|
187
210
|
channel?: SlackChannel;
|
|
188
211
|
}
|
|
@@ -192,6 +215,19 @@ interface SlackListResponse {
|
|
|
192
215
|
response_metadata?: { next_cursor?: string };
|
|
193
216
|
}
|
|
194
217
|
|
|
218
|
+
interface SlackMembersResponse {
|
|
219
|
+
members?: string[];
|
|
220
|
+
response_metadata?: { next_cursor?: string };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface SlackUserInfoResponse {
|
|
224
|
+
user?: { profile?: { email?: string } };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
type SlackUserEmailCache = Map<string, string | null>;
|
|
228
|
+
|
|
229
|
+
const SLACK_USER_LOOKUP_CONCURRENCY = 4;
|
|
230
|
+
|
|
195
231
|
interface SlackPermalinkResponse {
|
|
196
232
|
permalink?: string;
|
|
197
233
|
}
|
|
@@ -702,10 +738,6 @@ function readableJson(value: unknown): string {
|
|
|
702
738
|
return JSON.stringify(value, null, 2);
|
|
703
739
|
}
|
|
704
740
|
|
|
705
|
-
function slackUserLabel(message: SlackMessage): string {
|
|
706
|
-
return message.username ?? message.user ?? message.bot_id ?? "unknown";
|
|
707
|
-
}
|
|
708
|
-
|
|
709
741
|
function slackDirectConversationFromRef(
|
|
710
742
|
channelRef: string,
|
|
711
743
|
): SlackChannel | null {
|
|
@@ -726,22 +758,89 @@ function isUsableSlackChannel(channel: SlackChannel): boolean {
|
|
|
726
758
|
);
|
|
727
759
|
}
|
|
728
760
|
|
|
729
|
-
function
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
761
|
+
function slackReactionCount(message: SlackMessage) {
|
|
762
|
+
return (message.reactions ?? []).reduce(
|
|
763
|
+
(total, reaction) => total + Math.max(0, Number(reaction.count) || 0),
|
|
764
|
+
0,
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function isSlackTextMessage(message: SlackMessage) {
|
|
769
|
+
return (
|
|
770
|
+
message.type === "message" &&
|
|
771
|
+
typeof message.text === "string" &&
|
|
772
|
+
message.text.trim().length > 0 &&
|
|
773
|
+
typeof message.ts === "string"
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export interface SlackThreadCapture {
|
|
778
|
+
externalId: string;
|
|
779
|
+
title: string;
|
|
780
|
+
content: string;
|
|
781
|
+
capturedAt: string;
|
|
782
|
+
metadata: Record<string, unknown>;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Builds the only Slack representation Brain persists. It deliberately omits
|
|
787
|
+
* Slack user ids, display names, and the raw Events/Web API payload.
|
|
788
|
+
*/
|
|
789
|
+
export function normalizeSlackThreadCapture(input: {
|
|
790
|
+
channel: SlackChannel;
|
|
791
|
+
messages: SlackMessage[];
|
|
792
|
+
permalink?: string | null;
|
|
793
|
+
syncRunId?: string;
|
|
794
|
+
}): SlackThreadCapture | null {
|
|
795
|
+
const messages = input.messages
|
|
796
|
+
.filter(isSlackTextMessage)
|
|
797
|
+
.sort((left, right) => Number(left.ts) - Number(right.ts));
|
|
798
|
+
const root = messages[0];
|
|
799
|
+
if (!root?.ts) return null;
|
|
800
|
+
|
|
801
|
+
let offset = 0;
|
|
802
|
+
const safeSegments = messages.map((message, index) => {
|
|
803
|
+
const capturedAt = isoFromSlackTs(message.ts) ?? message.ts!;
|
|
804
|
+
const text = `${capturedAt}\n${message.text!.trim()}`;
|
|
805
|
+
const startOffset = offset;
|
|
806
|
+
offset += text.length;
|
|
807
|
+
if (index < messages.length - 1) offset += 2;
|
|
808
|
+
return {
|
|
809
|
+
id: `slack:${input.channel.id}:${message.ts}`,
|
|
810
|
+
capturedAt,
|
|
811
|
+
sourceUrl: input.permalink ?? undefined,
|
|
812
|
+
text,
|
|
813
|
+
startOffset,
|
|
814
|
+
endOffset: startOffset + text.length,
|
|
815
|
+
reactionCount: slackReactionCount(message),
|
|
816
|
+
};
|
|
817
|
+
});
|
|
818
|
+
const content = safeSegments.map((segment) => segment.text).join("\n\n");
|
|
819
|
+
const threadTs = root.thread_ts ?? root.ts;
|
|
820
|
+
return {
|
|
821
|
+
externalId: `slack:${input.channel.id}:${threadTs}`,
|
|
822
|
+
title: `#${input.channel.name ?? input.channel.id} thread ${isoFromSlackTs(threadTs) ?? threadTs}`,
|
|
823
|
+
content,
|
|
824
|
+
capturedAt: isoFromSlackTs(threadTs) ?? nowIso(),
|
|
825
|
+
metadata: {
|
|
826
|
+
provider: "slack",
|
|
827
|
+
connector: "slack",
|
|
828
|
+
...(input.syncRunId ? { syncRunId: input.syncRunId } : {}),
|
|
829
|
+
channelId: input.channel.id,
|
|
830
|
+
channelName: input.channel.name,
|
|
831
|
+
threadTs,
|
|
832
|
+
messageCount: safeSegments.length,
|
|
833
|
+
reactionCount: safeSegments.reduce(
|
|
834
|
+
(total, segment) => total + segment.reactionCount,
|
|
835
|
+
0,
|
|
836
|
+
),
|
|
837
|
+
sourceUrl: input.permalink ?? null,
|
|
838
|
+
permalink: input.permalink ?? null,
|
|
839
|
+
// These offsets are against `content`, the safe persisted capture, never
|
|
840
|
+
// against a provider payload.
|
|
841
|
+
safeSegments,
|
|
842
|
+
},
|
|
843
|
+
};
|
|
745
844
|
}
|
|
746
845
|
|
|
747
846
|
async function slackApi<T>(
|
|
@@ -810,6 +909,60 @@ async function resolveSlackChannelByName(
|
|
|
810
909
|
return null;
|
|
811
910
|
}
|
|
812
911
|
|
|
912
|
+
function matchesPublicChannelExclusion(
|
|
913
|
+
channel: SlackChannel,
|
|
914
|
+
patterns: string[],
|
|
915
|
+
) {
|
|
916
|
+
return patterns.some((pattern) => {
|
|
917
|
+
try {
|
|
918
|
+
const expression = new RegExp(pattern, "i");
|
|
919
|
+
return expression.test(channel.id) || expression.test(channel.name ?? "");
|
|
920
|
+
} catch {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async function listPublicSlackChannels(
|
|
927
|
+
token: string,
|
|
928
|
+
exclusionPatterns: string[],
|
|
929
|
+
): Promise<{ channels: SlackChannel[]; excluded: number }> {
|
|
930
|
+
const channels: SlackChannel[] = [];
|
|
931
|
+
const seenCursors = new Set<string>();
|
|
932
|
+
let cursor: string | undefined;
|
|
933
|
+
let excluded = 0;
|
|
934
|
+
for (;;) {
|
|
935
|
+
const page = await slackApi<SlackListResponse>(
|
|
936
|
+
token,
|
|
937
|
+
"conversations.list",
|
|
938
|
+
{
|
|
939
|
+
types: "public_channel",
|
|
940
|
+
exclude_archived: true,
|
|
941
|
+
limit: 200,
|
|
942
|
+
cursor,
|
|
943
|
+
},
|
|
944
|
+
);
|
|
945
|
+
for (const channel of page.channels ?? []) {
|
|
946
|
+
if (
|
|
947
|
+
channel.is_archived ||
|
|
948
|
+
channel.is_private ||
|
|
949
|
+
channel.is_channel === false
|
|
950
|
+
) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (matchesPublicChannelExclusion(channel, exclusionPatterns)) {
|
|
954
|
+
excluded += 1;
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
channels.push(channel);
|
|
958
|
+
}
|
|
959
|
+
cursor = page.response_metadata?.next_cursor;
|
|
960
|
+
if (!cursor || seenCursors.has(cursor)) break;
|
|
961
|
+
seenCursors.add(cursor);
|
|
962
|
+
}
|
|
963
|
+
return { channels, excluded };
|
|
964
|
+
}
|
|
965
|
+
|
|
813
966
|
async function resolveSlackChannel(
|
|
814
967
|
token: string,
|
|
815
968
|
channelRef: string,
|
|
@@ -848,6 +1001,121 @@ async function slackPermalink(
|
|
|
848
1001
|
return data.permalink ?? null;
|
|
849
1002
|
}
|
|
850
1003
|
|
|
1004
|
+
async function slackPrivateChannelMemberEmails(
|
|
1005
|
+
token: string,
|
|
1006
|
+
channelId: string,
|
|
1007
|
+
userEmailCache: SlackUserEmailCache,
|
|
1008
|
+
): Promise<string[] | null> {
|
|
1009
|
+
const memberIds = new Set<string>();
|
|
1010
|
+
const seenCursors = new Set<string>();
|
|
1011
|
+
let cursor: string | undefined;
|
|
1012
|
+
while (true) {
|
|
1013
|
+
const response = await slackApi<SlackMembersResponse>(
|
|
1014
|
+
token,
|
|
1015
|
+
"conversations.members",
|
|
1016
|
+
{ channel: channelId, limit: 1_000, cursor },
|
|
1017
|
+
);
|
|
1018
|
+
for (const memberId of response.members ?? []) {
|
|
1019
|
+
if (memberId) memberIds.add(memberId);
|
|
1020
|
+
}
|
|
1021
|
+
const nextCursor = response.response_metadata?.next_cursor?.trim();
|
|
1022
|
+
if (!nextCursor) break;
|
|
1023
|
+
if (seenCursors.has(nextCursor)) return null;
|
|
1024
|
+
seenCursors.add(nextCursor);
|
|
1025
|
+
cursor = nextCursor;
|
|
1026
|
+
}
|
|
1027
|
+
const userIds = Array.from(memberIds);
|
|
1028
|
+
if (!userIds.length) return null;
|
|
1029
|
+
if (
|
|
1030
|
+
userIds.some(
|
|
1031
|
+
(userId) => userEmailCache.has(userId) && !userEmailCache.get(userId),
|
|
1032
|
+
)
|
|
1033
|
+
) {
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const uncachedUserIds = userIds.filter(
|
|
1038
|
+
(userId) => !userEmailCache.has(userId),
|
|
1039
|
+
);
|
|
1040
|
+
for (
|
|
1041
|
+
let offset = 0;
|
|
1042
|
+
offset < uncachedUserIds.length;
|
|
1043
|
+
offset += SLACK_USER_LOOKUP_CONCURRENCY
|
|
1044
|
+
) {
|
|
1045
|
+
const batch = uncachedUserIds.slice(
|
|
1046
|
+
offset,
|
|
1047
|
+
offset + SLACK_USER_LOOKUP_CONCURRENCY,
|
|
1048
|
+
);
|
|
1049
|
+
await Promise.all(
|
|
1050
|
+
batch.map(async (userId) => {
|
|
1051
|
+
const user = await slackApi<SlackUserInfoResponse>(
|
|
1052
|
+
token,
|
|
1053
|
+
"users.info",
|
|
1054
|
+
{ user: userId },
|
|
1055
|
+
);
|
|
1056
|
+
const email = user.user?.profile?.email?.trim().toLowerCase() ?? null;
|
|
1057
|
+
userEmailCache.set(userId, email || null);
|
|
1058
|
+
}),
|
|
1059
|
+
);
|
|
1060
|
+
if (batch.some((userId) => !userEmailCache.get(userId))) return null;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const emails: string[] = [];
|
|
1064
|
+
for (const userId of userIds) {
|
|
1065
|
+
const email = userEmailCache.get(userId);
|
|
1066
|
+
if (!email) return null;
|
|
1067
|
+
emails.push(email);
|
|
1068
|
+
}
|
|
1069
|
+
return Array.from(new Set(emails)).sort();
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function configuredSlackChannelIds(config: Record<string, unknown>) {
|
|
1073
|
+
return new Set(
|
|
1074
|
+
slackChannelRefsFromConfig(config).filter((value) =>
|
|
1075
|
+
/^[CG][A-Z0-9]+$/i.test(value),
|
|
1076
|
+
),
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function createSlackThreadCapture(input: {
|
|
1081
|
+
source: SourceRow;
|
|
1082
|
+
channel: SlackChannel;
|
|
1083
|
+
messages: SlackMessage[];
|
|
1084
|
+
permalink: string | null;
|
|
1085
|
+
syncRunId?: string;
|
|
1086
|
+
memberEmails?: string[] | null;
|
|
1087
|
+
}) {
|
|
1088
|
+
const normalized = normalizeSlackThreadCapture({
|
|
1089
|
+
channel: input.channel,
|
|
1090
|
+
messages: input.messages,
|
|
1091
|
+
permalink: input.permalink,
|
|
1092
|
+
syncRunId: input.syncRunId,
|
|
1093
|
+
});
|
|
1094
|
+
if (!normalized) return { capture: null, blocked: false };
|
|
1095
|
+
try {
|
|
1096
|
+
const capture = await createCapture({
|
|
1097
|
+
sourceId: input.source.id,
|
|
1098
|
+
externalId: normalized.externalId,
|
|
1099
|
+
title: normalized.title,
|
|
1100
|
+
kind: "message",
|
|
1101
|
+
content: normalized.content,
|
|
1102
|
+
capturedAt: normalized.capturedAt,
|
|
1103
|
+
metadata: normalized.metadata,
|
|
1104
|
+
audience: {
|
|
1105
|
+
kind: input.channel.is_private ? "slack-private-channel" : "org",
|
|
1106
|
+
memberEmails: input.memberEmails ?? undefined,
|
|
1107
|
+
upstreamRefHash: input.channel.id,
|
|
1108
|
+
},
|
|
1109
|
+
});
|
|
1110
|
+
return { capture, blocked: false };
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
if (error instanceof BrainCaptureBlockedError) {
|
|
1113
|
+
return { capture: null, blocked: true };
|
|
1114
|
+
}
|
|
1115
|
+
throw error;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
851
1119
|
export async function testSlackConnection(
|
|
852
1120
|
options: {
|
|
853
1121
|
channelRefs?: string[];
|
|
@@ -1659,10 +1927,32 @@ async function githubRefsFromLinkedSources(
|
|
|
1659
1927
|
Number(options.stats.linkedSourcesSkipped) + 1;
|
|
1660
1928
|
continue;
|
|
1661
1929
|
}
|
|
1930
|
+
const audienceIds = await listAccessibleAudienceIds([sourceId]);
|
|
1931
|
+
if (!audienceIds.length) continue;
|
|
1662
1932
|
const captures = await db
|
|
1663
|
-
.select(
|
|
1933
|
+
.select({
|
|
1934
|
+
id: schema.brainRawCaptures.id,
|
|
1935
|
+
title: schema.brainRawCaptures.title,
|
|
1936
|
+
content: schema.brainRawCaptures.content,
|
|
1937
|
+
metadataJson: schema.brainRawCaptures.metadataJson,
|
|
1938
|
+
})
|
|
1664
1939
|
.from(schema.brainRawCaptures)
|
|
1665
|
-
.
|
|
1940
|
+
.innerJoin(
|
|
1941
|
+
schema.brainCaptureAudiences,
|
|
1942
|
+
eq(schema.brainCaptureAudiences.captureId, schema.brainRawCaptures.id),
|
|
1943
|
+
)
|
|
1944
|
+
.innerJoin(
|
|
1945
|
+
schema.brainAudiences,
|
|
1946
|
+
eq(schema.brainAudiences.id, schema.brainCaptureAudiences.audienceId),
|
|
1947
|
+
)
|
|
1948
|
+
.where(
|
|
1949
|
+
and(
|
|
1950
|
+
eq(schema.brainRawCaptures.sourceId, sourceId),
|
|
1951
|
+
eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
|
|
1952
|
+
inArray(schema.brainCaptureAudiences.audienceId, audienceIds),
|
|
1953
|
+
eq(schema.brainAudiences.kind, "org"),
|
|
1954
|
+
),
|
|
1955
|
+
)
|
|
1666
1956
|
.orderBy(desc(schema.brainRawCaptures.capturedAt))
|
|
1667
1957
|
.limit(options.captureLimit);
|
|
1668
1958
|
|
|
@@ -1675,7 +1965,6 @@ async function githubRefsFromLinkedSources(
|
|
|
1675
1965
|
capture.metadataJson,
|
|
1676
1966
|
{},
|
|
1677
1967
|
);
|
|
1678
|
-
const raw = objectValue(metadata.raw);
|
|
1679
1968
|
const linkedFrom = {
|
|
1680
1969
|
sourceId,
|
|
1681
1970
|
captureId: capture.id,
|
|
@@ -1684,7 +1973,6 @@ async function githubRefsFromLinkedSources(
|
|
|
1684
1973
|
};
|
|
1685
1974
|
const candidates = [
|
|
1686
1975
|
capture.content,
|
|
1687
|
-
typeof raw.text === "string" ? raw.text : "",
|
|
1688
1976
|
typeof metadata.sourceUrl === "string" ? metadata.sourceUrl : "",
|
|
1689
1977
|
typeof metadata.permalink === "string" ? metadata.permalink : "",
|
|
1690
1978
|
].join("\n");
|
|
@@ -1741,10 +2029,11 @@ async function syncFromConfiguredItems(
|
|
|
1741
2029
|
const config = parseJson<Record<string, unknown>>(source.configJson, {});
|
|
1742
2030
|
const items = transcriptItems(config);
|
|
1743
2031
|
const captures = [];
|
|
2032
|
+
let sensitivityBlocked = 0;
|
|
1744
2033
|
|
|
1745
2034
|
try {
|
|
1746
2035
|
for (const item of items) {
|
|
1747
|
-
const
|
|
2036
|
+
const captureResult = await createConnectorCapture({
|
|
1748
2037
|
sourceId: source.id,
|
|
1749
2038
|
externalId: item.externalId,
|
|
1750
2039
|
title: item.title,
|
|
@@ -1757,9 +2046,14 @@ async function syncFromConfiguredItems(
|
|
|
1757
2046
|
syncRunId: runId,
|
|
1758
2047
|
},
|
|
1759
2048
|
});
|
|
1760
|
-
|
|
2049
|
+
if (captureResult.capture)
|
|
2050
|
+
captures.push(serializeCapture(captureResult.capture));
|
|
2051
|
+
if (captureResult.blocked) sensitivityBlocked += 1;
|
|
1761
2052
|
}
|
|
1762
|
-
await finishRun(runId, "success", {
|
|
2053
|
+
await finishRun(runId, "success", {
|
|
2054
|
+
capturesCreated: captures.length,
|
|
2055
|
+
sensitivityBlocked,
|
|
2056
|
+
});
|
|
1763
2057
|
await getDb()
|
|
1764
2058
|
.update(schema.brainSources)
|
|
1765
2059
|
.set({
|
|
@@ -1841,6 +2135,12 @@ async function syncSlack(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
|
1841
2135
|
],
|
|
1842
2136
|
"slack",
|
|
1843
2137
|
);
|
|
2138
|
+
const includePublicChannels = configuredBoolean(
|
|
2139
|
+
config,
|
|
2140
|
+
["includePublicChannels"],
|
|
2141
|
+
false,
|
|
2142
|
+
"slack",
|
|
2143
|
+
);
|
|
1844
2144
|
const limit = configuredNumber(config, ["historyLimit", "limit"], 15, {
|
|
1845
2145
|
min: 1,
|
|
1846
2146
|
max: 15,
|
|
@@ -1877,11 +2177,17 @@ async function syncSlack(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
|
1877
2177
|
const captures = [];
|
|
1878
2178
|
const stats: Record<string, unknown> = {
|
|
1879
2179
|
configuredChannels: channelRefs.length,
|
|
2180
|
+
includePublicChannels,
|
|
2181
|
+
discoveredPublicChannels: 0,
|
|
2182
|
+
excludedPublicChannels: 0,
|
|
2183
|
+
eligibleChannels: 0,
|
|
1880
2184
|
scannedChannels: 0,
|
|
1881
2185
|
rejectedChannels: 0,
|
|
1882
2186
|
missingChannels: 0,
|
|
1883
2187
|
messagesSeen: 0,
|
|
1884
2188
|
capturesCreated: 0,
|
|
2189
|
+
threadsFetched: 0,
|
|
2190
|
+
sensitivityBlocked: 0,
|
|
1885
2191
|
rateLimited: false,
|
|
1886
2192
|
};
|
|
1887
2193
|
|
|
@@ -1892,15 +2198,17 @@ async function syncSlack(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
|
1892
2198
|
"slack",
|
|
1893
2199
|
config,
|
|
1894
2200
|
);
|
|
1895
|
-
if (!channelRefs.length) {
|
|
2201
|
+
if (!channelRefs.length && !includePublicChannels) {
|
|
1896
2202
|
throw new Error(
|
|
1897
|
-
"Slack source must configure channelIds, channels, or
|
|
2203
|
+
"Slack source must configure channelIds, channels, allowedChannels, or includePublicChannels",
|
|
1898
2204
|
);
|
|
1899
2205
|
}
|
|
1900
2206
|
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2207
|
+
const settings = await readBrainSettings();
|
|
2208
|
+
const publicExclusionPatterns =
|
|
2209
|
+
settings.publicChannelExclusionPatterns ?? [];
|
|
2210
|
+
const channelsById = new Map<string, SlackChannel>();
|
|
2211
|
+
for (const channelRef of channelRefs) {
|
|
1904
2212
|
const channel = await resolveSlackChannel(token, channelRef);
|
|
1905
2213
|
if (!channel) {
|
|
1906
2214
|
stats.missingChannels = Number(stats.missingChannels) + 1;
|
|
@@ -1910,6 +2218,68 @@ async function syncSlack(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
|
1910
2218
|
stats.rejectedChannels = Number(stats.rejectedChannels) + 1;
|
|
1911
2219
|
continue;
|
|
1912
2220
|
}
|
|
2221
|
+
const isExplicitId = /^[CG][A-Z0-9]+$/i.test(channelRef);
|
|
2222
|
+
if (channel.is_private && !isExplicitId) {
|
|
2223
|
+
stats.rejectedChannels = Number(stats.rejectedChannels) + 1;
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
if (
|
|
2227
|
+
!channel.is_private &&
|
|
2228
|
+
matchesPublicChannelExclusion(channel, publicExclusionPatterns)
|
|
2229
|
+
) {
|
|
2230
|
+
stats.excludedPublicChannels = Number(stats.excludedPublicChannels) + 1;
|
|
2231
|
+
continue;
|
|
2232
|
+
}
|
|
2233
|
+
channelsById.set(channel.id, channel);
|
|
2234
|
+
}
|
|
2235
|
+
if (includePublicChannels) {
|
|
2236
|
+
const discovered = await listPublicSlackChannels(
|
|
2237
|
+
token,
|
|
2238
|
+
publicExclusionPatterns,
|
|
2239
|
+
);
|
|
2240
|
+
stats.discoveredPublicChannels = discovered.channels.length;
|
|
2241
|
+
stats.excludedPublicChannels =
|
|
2242
|
+
Number(stats.excludedPublicChannels) + discovered.excluded;
|
|
2243
|
+
for (const channel of discovered.channels) {
|
|
2244
|
+
channelsById.set(channel.id, channel);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
const channels = [...channelsById.values()];
|
|
2248
|
+
stats.eligibleChannels = channels.length;
|
|
2249
|
+
const userEmailCache: SlackUserEmailCache = new Map();
|
|
2250
|
+
|
|
2251
|
+
const channelsToScan = includePublicChannels
|
|
2252
|
+
? (() => {
|
|
2253
|
+
const ordered = [...channels].sort((left, right) =>
|
|
2254
|
+
left.id.localeCompare(right.id),
|
|
2255
|
+
);
|
|
2256
|
+
if (!ordered.length) return ordered;
|
|
2257
|
+
const offset =
|
|
2258
|
+
Math.max(0, cursor.publicChannelOffset ?? 0) % ordered.length;
|
|
2259
|
+
const rotated = [
|
|
2260
|
+
...ordered.slice(offset),
|
|
2261
|
+
...ordered.slice(0, offset),
|
|
2262
|
+
];
|
|
2263
|
+
nextCursor.publicChannelOffset =
|
|
2264
|
+
(offset + Math.min(maxChannels, rotated.length)) % ordered.length;
|
|
2265
|
+
return rotated.slice(0, maxChannels);
|
|
2266
|
+
})()
|
|
2267
|
+
: channels.slice(0, maxChannels);
|
|
2268
|
+
|
|
2269
|
+
let permalinkCalls = 0;
|
|
2270
|
+
|
|
2271
|
+
for (const channel of channelsToScan) {
|
|
2272
|
+
const privateMemberEmails = channel.is_private
|
|
2273
|
+
? await slackPrivateChannelMemberEmails(
|
|
2274
|
+
token,
|
|
2275
|
+
channel.id,
|
|
2276
|
+
userEmailCache,
|
|
2277
|
+
)
|
|
2278
|
+
: null;
|
|
2279
|
+
if (channel.is_private && !privateMemberEmails?.length) {
|
|
2280
|
+
stats.rejectedChannels = Number(stats.rejectedChannels) + 1;
|
|
2281
|
+
continue;
|
|
2282
|
+
}
|
|
1913
2283
|
|
|
1914
2284
|
stats.scannedChannels = Number(stats.scannedChannels) + 1;
|
|
1915
2285
|
const channelCursor = nextCursor.channels?.[channel.id] ?? {};
|
|
@@ -1939,13 +2309,7 @@ async function syncSlack(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
|
1939
2309
|
"conversations.history",
|
|
1940
2310
|
params,
|
|
1941
2311
|
);
|
|
1942
|
-
const messages = (data.messages ?? []).filter(
|
|
1943
|
-
(message) =>
|
|
1944
|
-
message.type === "message" &&
|
|
1945
|
-
typeof message.text === "string" &&
|
|
1946
|
-
message.text.trim() &&
|
|
1947
|
-
typeof message.ts === "string",
|
|
1948
|
-
);
|
|
2312
|
+
const messages = (data.messages ?? []).filter(isSlackTextMessage);
|
|
1949
2313
|
stats.messagesSeen = Number(stats.messagesSeen) + messages.length;
|
|
1950
2314
|
|
|
1951
2315
|
const newest = newestSlackTs(messages);
|
|
@@ -1953,36 +2317,35 @@ async function syncSlack(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
|
1953
2317
|
channelCursor.pendingLatestTs = newest;
|
|
1954
2318
|
}
|
|
1955
2319
|
|
|
2320
|
+
const fetchedThreadTs = new Set<string>();
|
|
1956
2321
|
for (const message of messages) {
|
|
2322
|
+
const threadTs = message.thread_ts ?? message.ts;
|
|
2323
|
+
if (!threadTs || fetchedThreadTs.has(threadTs)) continue;
|
|
2324
|
+
fetchedThreadTs.add(threadTs);
|
|
1957
2325
|
let permalink: string | null = null;
|
|
1958
2326
|
if (permalinkCalls < permalinkLimit) {
|
|
1959
|
-
permalink = await slackPermalink(token, channel.id,
|
|
2327
|
+
permalink = await slackPermalink(token, channel.id, threadTs);
|
|
1960
2328
|
permalinkCalls += 1;
|
|
1961
2329
|
}
|
|
1962
|
-
const
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
ts: message.ts,
|
|
1976
|
-
threadTs: message.thread_ts,
|
|
1977
|
-
user: message.user,
|
|
1978
|
-
username: message.username,
|
|
1979
|
-
botId: message.bot_id,
|
|
1980
|
-
sourceUrl: permalink,
|
|
1981
|
-
permalink,
|
|
1982
|
-
raw: message,
|
|
1983
|
-
},
|
|
2330
|
+
const thread = await slackApi<SlackRepliesResponse>(
|
|
2331
|
+
token,
|
|
2332
|
+
"conversations.replies",
|
|
2333
|
+
{ channel: channel.id, ts: threadTs, limit: 1_000 },
|
|
2334
|
+
);
|
|
2335
|
+
stats.threadsFetched = Number(stats.threadsFetched) + 1;
|
|
2336
|
+
const captureResult = await createSlackThreadCapture({
|
|
2337
|
+
source,
|
|
2338
|
+
channel,
|
|
2339
|
+
messages: thread.messages?.length ? thread.messages : [message],
|
|
2340
|
+
permalink,
|
|
2341
|
+
syncRunId: runId,
|
|
2342
|
+
memberEmails: privateMemberEmails,
|
|
1984
2343
|
});
|
|
1985
|
-
|
|
2344
|
+
if (captureResult.capture)
|
|
2345
|
+
captures.push(serializeCapture(captureResult.capture));
|
|
2346
|
+
if (captureResult.blocked) {
|
|
2347
|
+
stats.sensitivityBlocked = Number(stats.sensitivityBlocked) + 1;
|
|
2348
|
+
}
|
|
1986
2349
|
}
|
|
1987
2350
|
|
|
1988
2351
|
const nextPage = data.response_metadata?.next_cursor;
|
|
@@ -2076,6 +2439,117 @@ async function syncSlack(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
|
2076
2439
|
}
|
|
2077
2440
|
}
|
|
2078
2441
|
|
|
2442
|
+
export async function refreshSlackThreadCapture(
|
|
2443
|
+
source: SourceRow,
|
|
2444
|
+
payloadJson: string,
|
|
2445
|
+
) {
|
|
2446
|
+
const payload = parseJson<Record<string, unknown>>(payloadJson, {});
|
|
2447
|
+
const channelId =
|
|
2448
|
+
typeof payload.channelId === "string" ? payload.channelId.trim() : "";
|
|
2449
|
+
const threadTs =
|
|
2450
|
+
typeof payload.threadTs === "string" ? payload.threadTs.trim() : "";
|
|
2451
|
+
if (!/^[CG][A-Z0-9]+$/i.test(channelId) || !slackTsFromDateish(threadTs)) {
|
|
2452
|
+
throw new Error("Slack thread refresh requires a channelId and threadTs");
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
const config = parseJson<Record<string, unknown>>(source.configJson, {});
|
|
2456
|
+
const includePublicChannels = configuredBoolean(
|
|
2457
|
+
config,
|
|
2458
|
+
["includePublicChannels"],
|
|
2459
|
+
false,
|
|
2460
|
+
"slack",
|
|
2461
|
+
);
|
|
2462
|
+
const explicitlyConfigured = configuredSlackChannelIds(config).has(channelId);
|
|
2463
|
+
if (
|
|
2464
|
+
!explicitlyConfigured &&
|
|
2465
|
+
!(includePublicChannels && channelId.startsWith("C"))
|
|
2466
|
+
) {
|
|
2467
|
+
throw new Error(
|
|
2468
|
+
`Slack thread refresh rejected for channel ${channelId}: it is not in this source's public or explicit channel scope`,
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
const runId = await createRun(source);
|
|
2472
|
+
try {
|
|
2473
|
+
const token = await requireConnectorCredential(
|
|
2474
|
+
"SLACK_BOT_TOKEN",
|
|
2475
|
+
"Slack",
|
|
2476
|
+
"slack",
|
|
2477
|
+
config,
|
|
2478
|
+
);
|
|
2479
|
+
const channel = await resolveSlackChannel(token, channelId);
|
|
2480
|
+
if (!channel || !isUsableSlackChannel(channel)) {
|
|
2481
|
+
throw new Error(`Slack channel ${channelId} is unavailable for refresh`);
|
|
2482
|
+
}
|
|
2483
|
+
if (
|
|
2484
|
+
!channel.is_private &&
|
|
2485
|
+
matchesPublicChannelExclusion(
|
|
2486
|
+
channel,
|
|
2487
|
+
(await readBrainSettings()).publicChannelExclusionPatterns ?? [],
|
|
2488
|
+
)
|
|
2489
|
+
) {
|
|
2490
|
+
throw new Error(
|
|
2491
|
+
`Slack thread refresh rejected for channel ${channelId}: it matches a Brain public-channel exclusion`,
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
const memberEmails = channel.is_private
|
|
2495
|
+
? await slackPrivateChannelMemberEmails(token, channel.id, new Map())
|
|
2496
|
+
: null;
|
|
2497
|
+
if (channel.is_private && !memberEmails?.length) {
|
|
2498
|
+
throw new Error(
|
|
2499
|
+
`Slack private channel ${channel.id} has no resolvable member emails; refusing to refresh without an ACL`,
|
|
2500
|
+
);
|
|
2501
|
+
}
|
|
2502
|
+
const [thread, permalink] = await Promise.all([
|
|
2503
|
+
slackApi<SlackRepliesResponse>(token, "conversations.replies", {
|
|
2504
|
+
channel: channel.id,
|
|
2505
|
+
ts: threadTs,
|
|
2506
|
+
limit: 1_000,
|
|
2507
|
+
}),
|
|
2508
|
+
slackPermalink(token, channel.id, threadTs),
|
|
2509
|
+
]);
|
|
2510
|
+
const captureResult = await createSlackThreadCapture({
|
|
2511
|
+
source,
|
|
2512
|
+
channel,
|
|
2513
|
+
messages: thread.messages ?? [],
|
|
2514
|
+
permalink,
|
|
2515
|
+
syncRunId: runId,
|
|
2516
|
+
memberEmails,
|
|
2517
|
+
});
|
|
2518
|
+
const stats = {
|
|
2519
|
+
channelId: channel.id,
|
|
2520
|
+
threadTs,
|
|
2521
|
+
threadsFetched: 1,
|
|
2522
|
+
capturesCreated: captureResult.capture ? 1 : 0,
|
|
2523
|
+
sensitivityBlocked: captureResult.blocked ? 1 : 0,
|
|
2524
|
+
};
|
|
2525
|
+
await finishRun(runId, "success", stats);
|
|
2526
|
+
await getDb()
|
|
2527
|
+
.update(schema.brainSources)
|
|
2528
|
+
.set({
|
|
2529
|
+
lastSyncedAt: nowIso(),
|
|
2530
|
+
lastError: null,
|
|
2531
|
+
status: "active",
|
|
2532
|
+
updatedAt: nowIso(),
|
|
2533
|
+
})
|
|
2534
|
+
.where(
|
|
2535
|
+
and(
|
|
2536
|
+
accessFilter(schema.brainSources, schema.brainSourceShares),
|
|
2537
|
+
eq(schema.brainSources.id, source.id),
|
|
2538
|
+
),
|
|
2539
|
+
);
|
|
2540
|
+
return {
|
|
2541
|
+
runId,
|
|
2542
|
+
capture: captureResult.capture
|
|
2543
|
+
? serializeCapture(captureResult.capture)
|
|
2544
|
+
: null,
|
|
2545
|
+
};
|
|
2546
|
+
} catch (error) {
|
|
2547
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2548
|
+
await finishRun(runId, "error", { capturesCreated: 0 }, message);
|
|
2549
|
+
throw error;
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2079
2553
|
async function syncGranola(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
2080
2554
|
const config = parseJson<Record<string, unknown>>(source.configJson, {});
|
|
2081
2555
|
if (isFixtureConfig(config) || transcriptItems(config).length > 0) {
|
|
@@ -2146,7 +2620,7 @@ async function syncGranola(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
|
2146
2620
|
...listed,
|
|
2147
2621
|
...note,
|
|
2148
2622
|
});
|
|
2149
|
-
const
|
|
2623
|
+
const captureResult = await createConnectorCapture({
|
|
2150
2624
|
sourceId: source.id,
|
|
2151
2625
|
externalId: normalized.externalId,
|
|
2152
2626
|
title: normalized.title,
|
|
@@ -2159,7 +2633,11 @@ async function syncGranola(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
|
2159
2633
|
syncRunId: runId,
|
|
2160
2634
|
},
|
|
2161
2635
|
});
|
|
2162
|
-
|
|
2636
|
+
if (captureResult.capture)
|
|
2637
|
+
captures.push(serializeCapture(captureResult.capture));
|
|
2638
|
+
if (captureResult.blocked) {
|
|
2639
|
+
stats.sensitivityBlocked = Number(stats.sensitivityBlocked ?? 0) + 1;
|
|
2640
|
+
}
|
|
2163
2641
|
|
|
2164
2642
|
const candidateUpdatedAt = note.updated_at ?? listed.updated_at;
|
|
2165
2643
|
if (
|
|
@@ -2449,7 +2927,7 @@ async function syncGitHub(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
|
2449
2927
|
ref.linkedFrom,
|
|
2450
2928
|
);
|
|
2451
2929
|
importedExternalIds.add(normalized.externalId);
|
|
2452
|
-
const
|
|
2930
|
+
const captureResult = await createConnectorCapture({
|
|
2453
2931
|
sourceId: source.id,
|
|
2454
2932
|
externalId: normalized.externalId,
|
|
2455
2933
|
title: normalized.title,
|
|
@@ -2461,7 +2939,11 @@ async function syncGitHub(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
|
2461
2939
|
syncRunId: runId,
|
|
2462
2940
|
},
|
|
2463
2941
|
});
|
|
2464
|
-
|
|
2942
|
+
if (captureResult.capture)
|
|
2943
|
+
captures.push(serializeCapture(captureResult.capture));
|
|
2944
|
+
if (captureResult.blocked) {
|
|
2945
|
+
stats.sensitivityBlocked = Number(stats.sensitivityBlocked ?? 0) + 1;
|
|
2946
|
+
}
|
|
2465
2947
|
stats.itemsSeen = Number(stats.itemsSeen) + 1;
|
|
2466
2948
|
stats.linkedRefsImported = Number(stats.linkedRefsImported) + 1;
|
|
2467
2949
|
}
|
|
@@ -2511,7 +2993,7 @@ async function syncGitHub(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
|
2511
2993
|
|
|
2512
2994
|
const normalized = normalizeGitHubIssue(repo, item, context);
|
|
2513
2995
|
importedExternalIds.add(normalized.externalId);
|
|
2514
|
-
const
|
|
2996
|
+
const captureResult = await createConnectorCapture({
|
|
2515
2997
|
sourceId: source.id,
|
|
2516
2998
|
externalId: normalized.externalId,
|
|
2517
2999
|
title: normalized.title,
|
|
@@ -2523,7 +3005,11 @@ async function syncGitHub(source: SourceRow): Promise<ConnectorSyncResult> {
|
|
|
2523
3005
|
syncRunId: runId,
|
|
2524
3006
|
},
|
|
2525
3007
|
});
|
|
2526
|
-
|
|
3008
|
+
if (captureResult.capture)
|
|
3009
|
+
captures.push(serializeCapture(captureResult.capture));
|
|
3010
|
+
if (captureResult.blocked) {
|
|
3011
|
+
stats.sensitivityBlocked = Number(stats.sensitivityBlocked ?? 0) + 1;
|
|
3012
|
+
}
|
|
2527
3013
|
|
|
2528
3014
|
const candidateUpdatedAt = item.updated_at;
|
|
2529
3015
|
if (
|