@kbve/droid 0.1.1 → 0.1.2
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/droid.mjs +2311 -203
- package/index.d.ts +3 -0
- package/lib/agents/api-types.d.ts +121 -0
- package/lib/agents/api-types.d.ts.map +1 -0
- package/lib/agents/auth/init.d.ts +8 -0
- package/lib/agents/auth/init.d.ts.map +1 -0
- package/lib/agents/cache.d.ts +9 -0
- package/lib/agents/cache.d.ts.map +1 -0
- package/lib/agents/client/callGuildVault.d.ts +3 -0
- package/lib/agents/client/callGuildVault.d.ts.map +1 -0
- package/lib/agents/client/callJson.d.ts +3 -0
- package/lib/agents/client/callJson.d.ts.map +1 -0
- package/lib/agents/config.d.ts +36 -0
- package/lib/agents/config.d.ts.map +1 -0
- package/lib/agents/constants.d.ts +11 -0
- package/lib/agents/constants.d.ts.map +1 -0
- package/lib/agents/createAgents.d.ts +4 -0
- package/lib/agents/createAgents.d.ts.map +1 -0
- package/lib/agents/ctx.d.ts +46 -0
- package/lib/agents/ctx.d.ts.map +1 -0
- package/lib/agents/discord/bot.d.ts +21 -0
- package/lib/agents/discord/bot.d.ts.map +1 -0
- package/lib/agents/discord/guilds.d.ts +8 -0
- package/lib/agents/discord/guilds.d.ts.map +1 -0
- package/lib/agents/formDrafts.d.ts +5 -0
- package/lib/agents/formDrafts.d.ts.map +1 -0
- package/lib/agents/generated/agents-schema.d.ts +73 -0
- package/lib/agents/generated/agents-schema.d.ts.map +1 -0
- package/lib/agents/generated/discordsh-agents-schema.d.ts +71 -0
- package/lib/agents/generated/discordsh-agents-schema.d.ts.map +1 -0
- package/lib/agents/github/backfill.d.ts +23 -0
- package/lib/agents/github/backfill.d.ts.map +1 -0
- package/lib/agents/github/events.d.ts +10 -0
- package/lib/agents/github/events.d.ts.map +1 -0
- package/lib/agents/github/pat.d.ts +13 -0
- package/lib/agents/github/pat.d.ts.map +1 -0
- package/lib/agents/github/repoConfig.d.ts +25 -0
- package/lib/agents/github/repoConfig.d.ts.map +1 -0
- package/lib/agents/github/tokens.d.ts +22 -0
- package/lib/agents/github/tokens.d.ts.map +1 -0
- package/lib/agents/github/webhook.d.ts +37 -0
- package/lib/agents/github/webhook.d.ts.map +1 -0
- package/lib/agents/index.d.ts +8 -0
- package/lib/agents/index.d.ts.map +1 -0
- package/lib/agents/state/atoms.d.ts +122 -0
- package/lib/agents/state/atoms.d.ts.map +1 -0
- package/lib/agents/types.d.ts +65 -0
- package/lib/agents/types.d.ts.map +1 -0
- package/lib/gateway/SupabaseGateway.d.ts.map +1 -1
- package/lib/gateway/types.d.ts +1 -1
- package/lib/gateway/types.d.ts.map +1 -1
- package/lib/workers/canvas-worker.js +6 -449
- package/lib/workers/data.d.ts.map +1 -1
- package/lib/workers/db-worker.d.ts +2 -2
- package/lib/workers/db-worker.d.ts.map +1 -1
- package/lib/workers/db-worker.js +14 -4170
- package/lib/workers/main.d.ts +0 -2
- package/lib/workers/main.d.ts.map +1 -1
- package/lib/workers/supabase-db-worker.js +11 -9325
- package/lib/workers/supabase-shared-worker.js +13 -12623
- package/lib/workers/ws-worker.d.ts +1 -2
- package/lib/workers/ws-worker.d.ts.map +1 -1
- package/lib/workers/ws-worker.js +6 -333
- package/main.js +5622 -4786
- package/package.json +2 -1
- package/src/index.ts +3 -0
- package/src/lib/agents/api-types.ts +170 -0
- package/src/lib/agents/auth/init.ts +200 -0
- package/src/lib/agents/cache.ts +127 -0
- package/src/lib/agents/client/callGuildVault.ts +71 -0
- package/src/lib/agents/client/callJson.ts +64 -0
- package/src/lib/agents/config.ts +83 -0
- package/src/lib/agents/constants.ts +11 -0
- package/src/lib/agents/createAgents.ts +62 -0
- package/src/lib/agents/ctx.ts +63 -0
- package/src/lib/agents/discord/bot.ts +169 -0
- package/src/lib/agents/discord/guilds.ts +106 -0
- package/src/lib/agents/formDrafts.ts +55 -0
- package/src/lib/agents/generated/agents-schema.ts +155 -0
- package/src/lib/agents/generated/discordsh-agents-schema.ts +96 -0
- package/src/lib/agents/github/backfill.ts +126 -0
- package/src/lib/agents/github/events.ts +131 -0
- package/src/lib/agents/github/pat.ts +150 -0
- package/src/lib/agents/github/repoConfig.ts +240 -0
- package/src/lib/agents/github/tokens.ts +222 -0
- package/src/lib/agents/github/webhook.ts +349 -0
- package/src/lib/agents/index.ts +35 -0
- package/src/lib/agents/state/atoms.ts +122 -0
- package/src/lib/agents/types.ts +83 -0
- package/src/lib/api.ts +71 -0
- package/src/lib/gateway/SupabaseGateway.ts +55 -2
- package/src/lib/gateway/WorkerCommunication.ts +1 -1
- package/src/lib/gateway/capabilities.spec.ts +144 -0
- package/src/lib/gateway/types.ts +97 -70
- package/src/lib/mod/module/supabase/mod-supabase.ts +10 -5
- package/src/lib/state/auth.ts +121 -3
- package/src/lib/state/bento.spec.ts +76 -0
- package/src/lib/state/bento.ts +51 -0
- package/src/lib/state/index.ts +64 -0
- package/src/lib/state/profile-sync.ts +214 -0
- package/src/lib/state/profile.ts +155 -0
- package/src/lib/state/staff.ts +128 -0
- package/src/lib/state/sync-bus.ts +109 -0
- package/src/lib/state/welcome-toast.spec.ts +101 -0
- package/src/lib/sw-recovery.spec.ts +165 -0
- package/src/lib/sw-recovery.ts +141 -0
- package/src/lib/types/event-schemas.spec.ts +120 -0
- package/src/lib/types/event-types.spec.ts +5 -2
- package/src/lib/types/event-types.ts +74 -0
- package/src/lib/workers/data.ts +5 -1
- package/src/lib/workers/db-worker.ts +34 -8
- package/src/lib/workers/main.ts +3 -18
- package/src/lib/workers/supabase-db-worker.ts +25 -3
- package/src/lib/workers/supabase-shared-worker.ts +43 -8
- package/src/lib/workers/ws-worker.spec.ts +41 -0
- package/src/lib/workers/ws-worker.ts +121 -28
- package/workers/main.js +2 -12
- package/comlink.js +0 -247
- package/index.js +0 -9263
- package/reference.js +0 -719
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { addToast } from '../state/toasts';
|
|
2
|
+
import type { ToastSeverity } from '../types/ui-event-types';
|
|
3
|
+
|
|
4
|
+
export interface AgentsNotice {
|
|
5
|
+
message: string;
|
|
6
|
+
severity: ToastSeverity;
|
|
7
|
+
duration?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AgentsSession {
|
|
11
|
+
accessToken: string | null;
|
|
12
|
+
providerToken: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AgentsConfig {
|
|
16
|
+
supabaseUrl: string;
|
|
17
|
+
discordBotClientId?: string;
|
|
18
|
+
discordClientId?: string;
|
|
19
|
+
|
|
20
|
+
loadSession(): Promise<AgentsSession>;
|
|
21
|
+
refreshSession(): Promise<string | null>;
|
|
22
|
+
|
|
23
|
+
getDiscordProviderToken(): string | null;
|
|
24
|
+
clearDiscordProviderToken(): void;
|
|
25
|
+
|
|
26
|
+
signInWithDiscord(): Promise<void>;
|
|
27
|
+
|
|
28
|
+
notify?(notice: AgentsNotice): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ResolvedAgentsConfig extends Required<
|
|
32
|
+
Omit<AgentsConfig, 'notify'>
|
|
33
|
+
> {
|
|
34
|
+
notify(notice: AgentsNotice): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function defaultNotify(notice: AgentsNotice): void {
|
|
38
|
+
addToast({
|
|
39
|
+
id: `agents-${notice.severity}-${Date.now()}`,
|
|
40
|
+
message: notice.message,
|
|
41
|
+
severity: notice.severity,
|
|
42
|
+
duration: notice.duration ?? 3500,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveAgentsConfig(
|
|
47
|
+
config: AgentsConfig,
|
|
48
|
+
): ResolvedAgentsConfig {
|
|
49
|
+
return {
|
|
50
|
+
supabaseUrl: config.supabaseUrl.replace(/\/$/, ''),
|
|
51
|
+
discordBotClientId: config.discordBotClientId ?? '',
|
|
52
|
+
discordClientId: config.discordClientId ?? '',
|
|
53
|
+
loadSession: config.loadSession,
|
|
54
|
+
refreshSession: config.refreshSession,
|
|
55
|
+
getDiscordProviderToken: config.getDiscordProviderToken,
|
|
56
|
+
clearDiscordProviderToken: config.clearDiscordProviderToken,
|
|
57
|
+
signInWithDiscord: config.signInWithDiscord,
|
|
58
|
+
notify: config.notify ?? defaultNotify,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AgentsEndpoints {
|
|
63
|
+
discordApiBase: string;
|
|
64
|
+
guildVault: string;
|
|
65
|
+
discordBootstrap: string;
|
|
66
|
+
discordBot: string;
|
|
67
|
+
ghAdmin: string;
|
|
68
|
+
ghBackfill: string;
|
|
69
|
+
ghWebhookBase: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildEndpoints(supabaseUrl: string): AgentsEndpoints {
|
|
73
|
+
const base = supabaseUrl.replace(/\/$/, '');
|
|
74
|
+
return {
|
|
75
|
+
discordApiBase: 'https://discord.com/api/v10',
|
|
76
|
+
guildVault: `${base}/functions/v1/guild-vault`,
|
|
77
|
+
discordBootstrap: `${base}/functions/v1/discord-bootstrap`,
|
|
78
|
+
discordBot: `${base}/functions/v1/discord-bot`,
|
|
79
|
+
ghAdmin: `${base}/functions/v1/gh-admin`,
|
|
80
|
+
ghBackfill: `${base}/functions/v1/gh-backfill`,
|
|
81
|
+
ghWebhookBase: `${base}/functions/v1/gh-webhook`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const GUILDS_CACHE_KEY = 'kbve:agents:owned_guilds_cache:v1';
|
|
2
|
+
export const TOKENS_CACHE_KEY = 'kbve:agents:tokens_cache:v1';
|
|
3
|
+
|
|
4
|
+
export const GUILDS_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
5
|
+
export const GUILDS_LIVE_TTL_MS = 60 * 1000;
|
|
6
|
+
export const TOKENS_CACHE_TTL_MS = 60 * 1000;
|
|
7
|
+
export const CHANNELS_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
8
|
+
export const BOT_MEMBERSHIP_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
|
+
export const INIT_DEDUP_MS = 30 * 1000;
|
|
10
|
+
export const BOOTSTRAP_COOLDOWN_MS = 5 * 60 * 1000;
|
|
11
|
+
export const FORCED_REFRESH_COOLDOWN_MS = 5 * 1000;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildEndpoints,
|
|
3
|
+
resolveAgentsConfig,
|
|
4
|
+
type AgentsConfig,
|
|
5
|
+
} from './config';
|
|
6
|
+
import { createAgentsStore } from './state/atoms';
|
|
7
|
+
import { createRuntime, type AgentsCtx } from './ctx';
|
|
8
|
+
import { makeCallJson } from './client/callJson';
|
|
9
|
+
import { makeCallGuildVault } from './client/callGuildVault';
|
|
10
|
+
import { makeAuth } from './auth/init';
|
|
11
|
+
import { makeGuilds } from './discord/guilds';
|
|
12
|
+
import { makeDiscordBot } from './discord/bot';
|
|
13
|
+
import { makeTokens } from './github/tokens';
|
|
14
|
+
import { makeRepoConfig } from './github/repoConfig';
|
|
15
|
+
import { makePat } from './github/pat';
|
|
16
|
+
import { makeWebhook } from './github/webhook';
|
|
17
|
+
import { makeBackfill } from './github/backfill';
|
|
18
|
+
import { makeEvents } from './github/events';
|
|
19
|
+
import type { AgentsApi } from './api-types';
|
|
20
|
+
|
|
21
|
+
export function createAgents(config: AgentsConfig): AgentsApi {
|
|
22
|
+
const resolved = resolveAgentsConfig(config);
|
|
23
|
+
const store = createAgentsStore();
|
|
24
|
+
const runtime = createRuntime();
|
|
25
|
+
|
|
26
|
+
const ctx = {
|
|
27
|
+
config: resolved,
|
|
28
|
+
endpoints: buildEndpoints(resolved.supabaseUrl),
|
|
29
|
+
store,
|
|
30
|
+
runtime,
|
|
31
|
+
callJson: (() => {
|
|
32
|
+
throw new Error('agents: callJson used before init');
|
|
33
|
+
}) as AgentsCtx['callJson'],
|
|
34
|
+
callGuildVault: (() => {
|
|
35
|
+
throw new Error('agents: callGuildVault used before init');
|
|
36
|
+
}) as AgentsCtx['callGuildVault'],
|
|
37
|
+
resyncOwnedGuilds: async () => false,
|
|
38
|
+
} as AgentsCtx;
|
|
39
|
+
|
|
40
|
+
const api = { ...store } as AgentsApi;
|
|
41
|
+
|
|
42
|
+
ctx.callJson = makeCallJson(ctx);
|
|
43
|
+
ctx.callGuildVault = makeCallGuildVault(ctx);
|
|
44
|
+
|
|
45
|
+
const auth = makeAuth(ctx, api);
|
|
46
|
+
ctx.resyncOwnedGuilds = auth.resyncOwnedGuilds;
|
|
47
|
+
|
|
48
|
+
Object.assign(
|
|
49
|
+
api,
|
|
50
|
+
auth,
|
|
51
|
+
makeGuilds(ctx, api),
|
|
52
|
+
makeTokens(ctx, api),
|
|
53
|
+
makeRepoConfig(ctx, api),
|
|
54
|
+
makePat(ctx, api),
|
|
55
|
+
makeDiscordBot(ctx),
|
|
56
|
+
makeWebhook(ctx, api),
|
|
57
|
+
makeBackfill(ctx),
|
|
58
|
+
makeEvents(ctx, api),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return api;
|
|
62
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AgentsEndpoints, ResolvedAgentsConfig } from './config';
|
|
2
|
+
import type { AgentsStore } from './state/atoms';
|
|
3
|
+
|
|
4
|
+
export type CallJsonResult<T> =
|
|
5
|
+
| { ok: true; data: T }
|
|
6
|
+
| { ok: false; error: string };
|
|
7
|
+
|
|
8
|
+
export type CallVaultResult =
|
|
9
|
+
| { ok: true; body: unknown }
|
|
10
|
+
| { ok: false; error: string };
|
|
11
|
+
|
|
12
|
+
export type CallJson = <T>(
|
|
13
|
+
url: string,
|
|
14
|
+
body: Record<string, unknown>,
|
|
15
|
+
opts?: { retriedAfterResync?: boolean },
|
|
16
|
+
) => Promise<CallJsonResult<T>>;
|
|
17
|
+
|
|
18
|
+
export type CallGuildVault = (
|
|
19
|
+
command: string,
|
|
20
|
+
accessToken: string,
|
|
21
|
+
extra: Record<string, unknown>,
|
|
22
|
+
opts?: { retriedAfterResync?: boolean },
|
|
23
|
+
) => Promise<CallVaultResult>;
|
|
24
|
+
|
|
25
|
+
export interface AgentsRuntime {
|
|
26
|
+
initPromise: Promise<void> | null;
|
|
27
|
+
lastInitAt: number;
|
|
28
|
+
lastGuildFetchAt: number;
|
|
29
|
+
guildsAbort: AbortController | null;
|
|
30
|
+
tokensInflight: Map<string, Promise<void>>;
|
|
31
|
+
tokensAbort: Map<string, AbortController>;
|
|
32
|
+
bootstrapInflight: Promise<boolean> | null;
|
|
33
|
+
lastBootstrapAt: number;
|
|
34
|
+
lastBootstrapOk: boolean;
|
|
35
|
+
refreshInflight: Promise<boolean> | null;
|
|
36
|
+
lastForcedRefreshAt: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createRuntime(): AgentsRuntime {
|
|
40
|
+
return {
|
|
41
|
+
initPromise: null,
|
|
42
|
+
lastInitAt: 0,
|
|
43
|
+
lastGuildFetchAt: 0,
|
|
44
|
+
guildsAbort: null,
|
|
45
|
+
tokensInflight: new Map(),
|
|
46
|
+
tokensAbort: new Map(),
|
|
47
|
+
bootstrapInflight: null,
|
|
48
|
+
lastBootstrapAt: 0,
|
|
49
|
+
lastBootstrapOk: false,
|
|
50
|
+
refreshInflight: null,
|
|
51
|
+
lastForcedRefreshAt: 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AgentsCtx {
|
|
56
|
+
config: ResolvedAgentsConfig;
|
|
57
|
+
endpoints: AgentsEndpoints;
|
|
58
|
+
store: AgentsStore;
|
|
59
|
+
runtime: AgentsRuntime;
|
|
60
|
+
callJson: CallJson;
|
|
61
|
+
callGuildVault: CallGuildVault;
|
|
62
|
+
resyncOwnedGuilds: () => Promise<boolean>;
|
|
63
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BOT_MEMBERSHIP_CACHE_TTL_MS,
|
|
3
|
+
CHANNELS_CACHE_TTL_MS,
|
|
4
|
+
} from '../constants';
|
|
5
|
+
import type { AgentsCtx } from '../ctx';
|
|
6
|
+
import type { DiscordForumChannel, GuildChannels, Result } from '../types';
|
|
7
|
+
|
|
8
|
+
export function makeDiscordBot(ctx: AgentsCtx) {
|
|
9
|
+
const { store, config, endpoints } = ctx;
|
|
10
|
+
|
|
11
|
+
async function ensureGuildChannelsLoaded(
|
|
12
|
+
guildId: string,
|
|
13
|
+
force = false,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const cur = store.$guildChannels.get()[guildId];
|
|
16
|
+
const fresh =
|
|
17
|
+
!!cur && Date.now() - cur.cached_at < CHANNELS_CACHE_TTL_MS;
|
|
18
|
+
if (fresh && !force) return;
|
|
19
|
+
if (store.$guildChannelsLoading.get()[guildId]) return;
|
|
20
|
+
store.$guildChannelsLoading.set({
|
|
21
|
+
...store.$guildChannelsLoading.get(),
|
|
22
|
+
[guildId]: true,
|
|
23
|
+
});
|
|
24
|
+
store.$guildChannelsError.set({
|
|
25
|
+
...store.$guildChannelsError.get(),
|
|
26
|
+
[guildId]: null,
|
|
27
|
+
});
|
|
28
|
+
const r = await ctx.callJson<GuildChannels>(endpoints.discordBot, {
|
|
29
|
+
command: 'bot.list_channels',
|
|
30
|
+
server_id: guildId,
|
|
31
|
+
});
|
|
32
|
+
const loading = { ...store.$guildChannelsLoading.get() };
|
|
33
|
+
delete loading[guildId];
|
|
34
|
+
store.$guildChannelsLoading.set(loading);
|
|
35
|
+
if (!r.ok) {
|
|
36
|
+
store.$guildChannelsError.set({
|
|
37
|
+
...store.$guildChannelsError.get(),
|
|
38
|
+
[guildId]: r.error,
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
store.$guildChannels.set({
|
|
43
|
+
...store.$guildChannels.get(),
|
|
44
|
+
[guildId]: {
|
|
45
|
+
forums: r.data.forums ?? [],
|
|
46
|
+
texts: r.data.texts ?? [],
|
|
47
|
+
cached_at: Date.now(),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function ensureBotMembershipLoaded(
|
|
53
|
+
guildId: string,
|
|
54
|
+
force = false,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
const cur = store.$botMembership.get()[guildId];
|
|
57
|
+
const fresh =
|
|
58
|
+
!!cur && Date.now() - cur.cached_at < BOT_MEMBERSHIP_CACHE_TTL_MS;
|
|
59
|
+
if (fresh && !force) return;
|
|
60
|
+
if (store.$botMembershipLoading.get()[guildId]) return;
|
|
61
|
+
store.$botMembershipLoading.set({
|
|
62
|
+
...store.$botMembershipLoading.get(),
|
|
63
|
+
[guildId]: true,
|
|
64
|
+
});
|
|
65
|
+
store.$botMembershipError.set({
|
|
66
|
+
...store.$botMembershipError.get(),
|
|
67
|
+
[guildId]: null,
|
|
68
|
+
});
|
|
69
|
+
const r = await ctx.callJson<{ is_member: boolean }>(
|
|
70
|
+
endpoints.discordBot,
|
|
71
|
+
{ command: 'bot.is_member', server_id: guildId },
|
|
72
|
+
);
|
|
73
|
+
const loading = { ...store.$botMembershipLoading.get() };
|
|
74
|
+
delete loading[guildId];
|
|
75
|
+
store.$botMembershipLoading.set(loading);
|
|
76
|
+
if (!r.ok) {
|
|
77
|
+
store.$botMembershipError.set({
|
|
78
|
+
...store.$botMembershipError.get(),
|
|
79
|
+
[guildId]: r.error,
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
store.$botMembership.set({
|
|
84
|
+
...store.$botMembership.get(),
|
|
85
|
+
[guildId]: {
|
|
86
|
+
isMember: !!r.data.is_member,
|
|
87
|
+
cached_at: Date.now(),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function invalidateBotMembership(guildId: string): void {
|
|
93
|
+
const m = { ...store.$botMembership.get() };
|
|
94
|
+
delete m[guildId];
|
|
95
|
+
store.$botMembership.set(m);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function isBotMember(
|
|
99
|
+
guildId: string,
|
|
100
|
+
): Promise<
|
|
101
|
+
| { ok: true; isMember: boolean; joinedAt: string | null }
|
|
102
|
+
| { ok: false; error: string }
|
|
103
|
+
> {
|
|
104
|
+
const r = await ctx.callJson<{
|
|
105
|
+
is_member: boolean;
|
|
106
|
+
joined_at: string | null;
|
|
107
|
+
}>(endpoints.discordBot, {
|
|
108
|
+
command: 'bot.is_member',
|
|
109
|
+
server_id: guildId,
|
|
110
|
+
});
|
|
111
|
+
if (!r.ok) return r;
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
isMember: !!r.data.is_member,
|
|
115
|
+
joinedAt: r.data.joined_at ?? null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function listForumChannels(
|
|
120
|
+
guildId: string,
|
|
121
|
+
): Promise<Result<{ channels: DiscordForumChannel[] }>> {
|
|
122
|
+
const r = await ctx.callJson<{ channels: DiscordForumChannel[] }>(
|
|
123
|
+
endpoints.discordBot,
|
|
124
|
+
{ command: 'bot.list_forum_channels', server_id: guildId },
|
|
125
|
+
);
|
|
126
|
+
if (!r.ok) return r;
|
|
127
|
+
return { ok: true, channels: r.data.channels ?? [] };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildInstallUrl(
|
|
131
|
+
clientId: string,
|
|
132
|
+
guildId?: string,
|
|
133
|
+
): string | null {
|
|
134
|
+
if (!/^[0-9]{17,20}$/.test(clientId)) return null;
|
|
135
|
+
const perms = '326417847872';
|
|
136
|
+
const scope = 'bot applications.commands';
|
|
137
|
+
const guildParam = guildId
|
|
138
|
+
? `&guild_id=${guildId}&disable_guild_select=true`
|
|
139
|
+
: '';
|
|
140
|
+
return `https://discord.com/oauth2/authorize?client_id=${clientId}&permissions=${perms}&scope=${encodeURIComponent(scope)}${guildParam}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function botInstallUrl(guildId?: string): string | null {
|
|
144
|
+
const clientId = config.discordBotClientId || config.discordClientId;
|
|
145
|
+
if (!clientId) return null;
|
|
146
|
+
return buildInstallUrl(clientId, guildId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function fetchBotInstallUrl(guildId: string): Promise<string | null> {
|
|
150
|
+
const fromEnv = botInstallUrl(guildId);
|
|
151
|
+
if (fromEnv) return fromEnv;
|
|
152
|
+
const r = await ctx.callJson<{ client_id: string }>(
|
|
153
|
+
endpoints.discordBot,
|
|
154
|
+
{ command: 'bot.install_info', server_id: guildId },
|
|
155
|
+
);
|
|
156
|
+
if (!r.ok || !r.data.client_id) return null;
|
|
157
|
+
return buildInstallUrl(r.data.client_id, guildId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
ensureGuildChannelsLoaded,
|
|
162
|
+
ensureBotMembershipLoaded,
|
|
163
|
+
invalidateBotMembership,
|
|
164
|
+
isBotMember,
|
|
165
|
+
listForumChannels,
|
|
166
|
+
botInstallUrl,
|
|
167
|
+
fetchBotInstallUrl,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { GUILDS_LIVE_TTL_MS } from '../constants';
|
|
2
|
+
import { invalidateCachedTokens, saveCachedGuilds } from '../cache';
|
|
3
|
+
import type { AgentsCtx } from '../ctx';
|
|
4
|
+
import type { AgentsApi } from '../api-types';
|
|
5
|
+
import type { DiscordGuild } from '../types';
|
|
6
|
+
|
|
7
|
+
export function makeGuilds(ctx: AgentsCtx, api: AgentsApi) {
|
|
8
|
+
const { store, config, endpoints, runtime } = ctx;
|
|
9
|
+
|
|
10
|
+
function selectGuild(guildId: string): void {
|
|
11
|
+
const current = store.$selectedGuildId.get();
|
|
12
|
+
if (current === guildId) return;
|
|
13
|
+
store.$selectedGuildId.set(guildId);
|
|
14
|
+
void api.loadTokens(guildId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function loadOwnedGuilds(force = false): Promise<void> {
|
|
18
|
+
const providerToken = store.$providerToken.get();
|
|
19
|
+
if (!providerToken) return;
|
|
20
|
+
|
|
21
|
+
if (
|
|
22
|
+
!force &&
|
|
23
|
+
runtime.lastGuildFetchAt > 0 &&
|
|
24
|
+
Date.now() - runtime.lastGuildFetchAt < GUILDS_LIVE_TTL_MS &&
|
|
25
|
+
store.$guilds.get().length > 0
|
|
26
|
+
) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (runtime.guildsAbort) runtime.guildsAbort.abort();
|
|
31
|
+
const abort = new AbortController();
|
|
32
|
+
runtime.guildsAbort = abort;
|
|
33
|
+
|
|
34
|
+
store.$guildsLoading.set(true);
|
|
35
|
+
store.$guildsError.set(null);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const resp = await fetch(
|
|
39
|
+
`${endpoints.discordApiBase}/users/@me/guilds`,
|
|
40
|
+
{
|
|
41
|
+
headers: { Authorization: `Bearer ${providerToken}` },
|
|
42
|
+
signal: abort.signal,
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (resp.status === 401) {
|
|
47
|
+
store.$providerToken.set(null);
|
|
48
|
+
store.$guildsStale.set(true);
|
|
49
|
+
try {
|
|
50
|
+
config.clearDiscordProviderToken();
|
|
51
|
+
} catch {
|
|
52
|
+
/* non-fatal */
|
|
53
|
+
}
|
|
54
|
+
store.$guildsError.set(
|
|
55
|
+
'Discord session expired — guild list may be stale. Sign in with Discord to refresh.',
|
|
56
|
+
);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!resp.ok) {
|
|
61
|
+
const body = await resp.text();
|
|
62
|
+
store.$guildsError.set(
|
|
63
|
+
`Discord API ${resp.status}: ${body.slice(0, 200)}`,
|
|
64
|
+
);
|
|
65
|
+
store.$guildsStale.set(true);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const allGuilds = (await resp.json()) as DiscordGuild[];
|
|
70
|
+
const owned = allGuilds.filter((g) => g.owner === true);
|
|
71
|
+
store.$guilds.set(owned);
|
|
72
|
+
store.$guildsStale.set(false);
|
|
73
|
+
runtime.lastGuildFetchAt = Date.now();
|
|
74
|
+
|
|
75
|
+
const userId = store.$userId.get();
|
|
76
|
+
if (userId) saveCachedGuilds(userId, owned);
|
|
77
|
+
|
|
78
|
+
if (owned.length === 1) {
|
|
79
|
+
selectGuild(owned[0].id);
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
store.$guildsError.set(
|
|
86
|
+
e instanceof Error
|
|
87
|
+
? e.message
|
|
88
|
+
: 'Failed to load Discord guilds',
|
|
89
|
+
);
|
|
90
|
+
store.$guildsStale.set(true);
|
|
91
|
+
} finally {
|
|
92
|
+
store.$guildsLoading.set(false);
|
|
93
|
+
if (runtime.guildsAbort === abort) runtime.guildsAbort = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function refreshSelectedGuild(): Promise<void> {
|
|
98
|
+
const guildId = store.$selectedGuildId.get();
|
|
99
|
+
if (!guildId) return;
|
|
100
|
+
const userId = store.$userId.get();
|
|
101
|
+
if (userId) invalidateCachedTokens(userId, guildId);
|
|
102
|
+
await api.loadTokens(guildId, true);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { selectGuild, loadOwnedGuilds, refreshSelectedGuild };
|
|
106
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { BotConfigFormDraft, DiscordshConfig } from './types';
|
|
2
|
+
|
|
3
|
+
export function emptyBotConfigFormDraft(): BotConfigFormDraft {
|
|
4
|
+
return {
|
|
5
|
+
default_repo: '',
|
|
6
|
+
claim_channel_id: '',
|
|
7
|
+
forum_channel_id: '',
|
|
8
|
+
noticeboard_channel_id: '',
|
|
9
|
+
taskboard_channel_id: '',
|
|
10
|
+
max_assignees: '2',
|
|
11
|
+
mirror_pr_events: true,
|
|
12
|
+
active: true,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function botConfigToFormDraft(c: DiscordshConfig): BotConfigFormDraft {
|
|
17
|
+
const base = emptyBotConfigFormDraft();
|
|
18
|
+
return {
|
|
19
|
+
default_repo: c.default_repo ?? base.default_repo,
|
|
20
|
+
claim_channel_id: c.claim_channel_id ?? base.claim_channel_id,
|
|
21
|
+
forum_channel_id: c.forum_channel_id ?? base.forum_channel_id,
|
|
22
|
+
noticeboard_channel_id:
|
|
23
|
+
c.noticeboard_channel_id ?? base.noticeboard_channel_id,
|
|
24
|
+
taskboard_channel_id:
|
|
25
|
+
c.taskboard_channel_id ?? base.taskboard_channel_id,
|
|
26
|
+
max_assignees:
|
|
27
|
+
typeof c.max_assignees === 'number'
|
|
28
|
+
? String(c.max_assignees)
|
|
29
|
+
: base.max_assignees,
|
|
30
|
+
mirror_pr_events:
|
|
31
|
+
typeof c.mirror_pr_events === 'boolean'
|
|
32
|
+
? c.mirror_pr_events
|
|
33
|
+
: base.mirror_pr_events,
|
|
34
|
+
active: typeof c.active === 'boolean' ? c.active : base.active,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function botConfigFromFormDraft(f: BotConfigFormDraft): DiscordshConfig {
|
|
39
|
+
const cfg: DiscordshConfig = {
|
|
40
|
+
mirror_pr_events: f.mirror_pr_events,
|
|
41
|
+
active: f.active,
|
|
42
|
+
};
|
|
43
|
+
if (f.default_repo.trim()) cfg.default_repo = f.default_repo.trim();
|
|
44
|
+
if (f.claim_channel_id.trim())
|
|
45
|
+
cfg.claim_channel_id = f.claim_channel_id.trim();
|
|
46
|
+
if (f.forum_channel_id.trim())
|
|
47
|
+
cfg.forum_channel_id = f.forum_channel_id.trim();
|
|
48
|
+
if (f.noticeboard_channel_id.trim())
|
|
49
|
+
cfg.noticeboard_channel_id = f.noticeboard_channel_id.trim();
|
|
50
|
+
if (f.taskboard_channel_id.trim())
|
|
51
|
+
cfg.taskboard_channel_id = f.taskboard_channel_id.trim();
|
|
52
|
+
const max = parseInt(f.max_assignees, 10);
|
|
53
|
+
if (!Number.isNaN(max) && max > 0) cfg.max_assignees = max;
|
|
54
|
+
return cfg;
|
|
55
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-GENERATED by proto-to-zod — DO NOT EDIT
|
|
3
|
+
*
|
|
4
|
+
* Source: ../descriptors/agents.binpb
|
|
5
|
+
* Config: ../agents-zod-config.json
|
|
6
|
+
* Generated: 2026-07-20T00:37:21.617Z
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
|
|
11
|
+
export const TokenServices = [
|
|
12
|
+
'github_webhook',
|
|
13
|
+
'github',
|
|
14
|
+
'github_repos',
|
|
15
|
+
'discordsh_config',
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type TokenServiceValue = (typeof TokenServices)[number];
|
|
19
|
+
|
|
20
|
+
export const TokenServiceSchema = z.enum(TokenServices);
|
|
21
|
+
|
|
22
|
+
// PeekTokenResponse
|
|
23
|
+
export const PeekTokenResponseSchema = z.object({
|
|
24
|
+
value: z.string().optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type PeekTokenResponse = z.infer<typeof PeekTokenResponseSchema>;
|
|
28
|
+
|
|
29
|
+
// PeekTokenRequest
|
|
30
|
+
export const PeekTokenRequestSchema = z.object({
|
|
31
|
+
server_id: z
|
|
32
|
+
.string()
|
|
33
|
+
.regex(
|
|
34
|
+
/^\d{17,20}$/,
|
|
35
|
+
'Must be a valid Discord snowflake (17-20 digits)',
|
|
36
|
+
),
|
|
37
|
+
service: z
|
|
38
|
+
.string()
|
|
39
|
+
.regex(
|
|
40
|
+
/^[a-z0-9_]{2,32}$/,
|
|
41
|
+
'Service must be 2-32 chars: lowercase a-z, 0-9, underscore',
|
|
42
|
+
),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type PeekTokenRequest = z.infer<typeof PeekTokenRequestSchema>;
|
|
46
|
+
|
|
47
|
+
// ToggleTokenRequest
|
|
48
|
+
export const ToggleTokenRequestSchema = z.object({
|
|
49
|
+
server_id: z
|
|
50
|
+
.string()
|
|
51
|
+
.regex(
|
|
52
|
+
/^\d{17,20}$/,
|
|
53
|
+
'Must be a valid Discord snowflake (17-20 digits)',
|
|
54
|
+
),
|
|
55
|
+
token_id: z
|
|
56
|
+
.string()
|
|
57
|
+
.regex(
|
|
58
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
59
|
+
'token_id must be a UUID',
|
|
60
|
+
),
|
|
61
|
+
is_active: z.boolean(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export type ToggleTokenRequest = z.infer<typeof ToggleTokenRequestSchema>;
|
|
65
|
+
|
|
66
|
+
// DeleteTokenRequest
|
|
67
|
+
export const DeleteTokenRequestSchema = z.object({
|
|
68
|
+
server_id: z
|
|
69
|
+
.string()
|
|
70
|
+
.regex(
|
|
71
|
+
/^\d{17,20}$/,
|
|
72
|
+
'Must be a valid Discord snowflake (17-20 digits)',
|
|
73
|
+
),
|
|
74
|
+
token_id: z
|
|
75
|
+
.string()
|
|
76
|
+
.regex(
|
|
77
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
78
|
+
'token_id must be a UUID',
|
|
79
|
+
),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export type DeleteTokenRequest = z.infer<typeof DeleteTokenRequestSchema>;
|
|
83
|
+
|
|
84
|
+
// AgentTokenRow
|
|
85
|
+
export const AgentTokenRowSchema = z.object({
|
|
86
|
+
token_id: z.string(),
|
|
87
|
+
token_name: z.string(),
|
|
88
|
+
service: z.string(),
|
|
89
|
+
description: z.string().nullish(),
|
|
90
|
+
is_active: z.boolean(),
|
|
91
|
+
created_at: z.string(),
|
|
92
|
+
updated_at: z.string().nullish(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export type AgentTokenRow = z.infer<typeof AgentTokenRowSchema>;
|
|
96
|
+
|
|
97
|
+
// ListTokensResponse
|
|
98
|
+
export const ListTokensResponseSchema = z.object({
|
|
99
|
+
tokens: z.array(AgentTokenRowSchema).optional(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export type ListTokensResponse = z.infer<typeof ListTokensResponseSchema>;
|
|
103
|
+
|
|
104
|
+
// ListTokensRequest
|
|
105
|
+
export const ListTokensRequestSchema = z.object({
|
|
106
|
+
server_id: z
|
|
107
|
+
.string()
|
|
108
|
+
.regex(
|
|
109
|
+
/^\d{17,20}$/,
|
|
110
|
+
'Must be a valid Discord snowflake (17-20 digits)',
|
|
111
|
+
),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export type ListTokensRequest = z.infer<typeof ListTokensRequestSchema>;
|
|
115
|
+
|
|
116
|
+
// SetTokenRequest
|
|
117
|
+
export const SetTokenRequestSchema = z.object({
|
|
118
|
+
server_id: z
|
|
119
|
+
.string()
|
|
120
|
+
.regex(
|
|
121
|
+
/^\d{17,20}$/,
|
|
122
|
+
'Must be a valid Discord snowflake (17-20 digits)',
|
|
123
|
+
),
|
|
124
|
+
service: z
|
|
125
|
+
.string()
|
|
126
|
+
.regex(
|
|
127
|
+
/^[a-z0-9_]{2,32}$/,
|
|
128
|
+
'Service must be 2-32 chars: lowercase a-z, 0-9, underscore',
|
|
129
|
+
),
|
|
130
|
+
token_name: z
|
|
131
|
+
.string()
|
|
132
|
+
.regex(
|
|
133
|
+
/^[a-z0-9_-]{3,64}$/,
|
|
134
|
+
'Token name must be 3-64 lowercase chars: a-z, 0-9, underscore, dash',
|
|
135
|
+
),
|
|
136
|
+
token_value: z
|
|
137
|
+
.string()
|
|
138
|
+
.min(10, 'Token value must be 10-8000 characters')
|
|
139
|
+
.max(8000, 'Token value must be 10-8000 characters'),
|
|
140
|
+
description: z.string().optional(),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export type SetTokenRequest = z.infer<typeof SetTokenRequestSchema>;
|
|
144
|
+
|
|
145
|
+
// AgentError
|
|
146
|
+
export const AgentErrorSchema = z.object({
|
|
147
|
+
success: z.boolean(),
|
|
148
|
+
message: z.string(),
|
|
149
|
+
code: z.number(),
|
|
150
|
+
sqlstate: z.string().optional(),
|
|
151
|
+
hint: z.string().optional(),
|
|
152
|
+
context: z.string().optional(),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
export type AgentError = z.infer<typeof AgentErrorSchema>;
|