@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.
Files changed (120) hide show
  1. package/droid.mjs +2311 -203
  2. package/index.d.ts +3 -0
  3. package/lib/agents/api-types.d.ts +121 -0
  4. package/lib/agents/api-types.d.ts.map +1 -0
  5. package/lib/agents/auth/init.d.ts +8 -0
  6. package/lib/agents/auth/init.d.ts.map +1 -0
  7. package/lib/agents/cache.d.ts +9 -0
  8. package/lib/agents/cache.d.ts.map +1 -0
  9. package/lib/agents/client/callGuildVault.d.ts +3 -0
  10. package/lib/agents/client/callGuildVault.d.ts.map +1 -0
  11. package/lib/agents/client/callJson.d.ts +3 -0
  12. package/lib/agents/client/callJson.d.ts.map +1 -0
  13. package/lib/agents/config.d.ts +36 -0
  14. package/lib/agents/config.d.ts.map +1 -0
  15. package/lib/agents/constants.d.ts +11 -0
  16. package/lib/agents/constants.d.ts.map +1 -0
  17. package/lib/agents/createAgents.d.ts +4 -0
  18. package/lib/agents/createAgents.d.ts.map +1 -0
  19. package/lib/agents/ctx.d.ts +46 -0
  20. package/lib/agents/ctx.d.ts.map +1 -0
  21. package/lib/agents/discord/bot.d.ts +21 -0
  22. package/lib/agents/discord/bot.d.ts.map +1 -0
  23. package/lib/agents/discord/guilds.d.ts +8 -0
  24. package/lib/agents/discord/guilds.d.ts.map +1 -0
  25. package/lib/agents/formDrafts.d.ts +5 -0
  26. package/lib/agents/formDrafts.d.ts.map +1 -0
  27. package/lib/agents/generated/agents-schema.d.ts +73 -0
  28. package/lib/agents/generated/agents-schema.d.ts.map +1 -0
  29. package/lib/agents/generated/discordsh-agents-schema.d.ts +71 -0
  30. package/lib/agents/generated/discordsh-agents-schema.d.ts.map +1 -0
  31. package/lib/agents/github/backfill.d.ts +23 -0
  32. package/lib/agents/github/backfill.d.ts.map +1 -0
  33. package/lib/agents/github/events.d.ts +10 -0
  34. package/lib/agents/github/events.d.ts.map +1 -0
  35. package/lib/agents/github/pat.d.ts +13 -0
  36. package/lib/agents/github/pat.d.ts.map +1 -0
  37. package/lib/agents/github/repoConfig.d.ts +25 -0
  38. package/lib/agents/github/repoConfig.d.ts.map +1 -0
  39. package/lib/agents/github/tokens.d.ts +22 -0
  40. package/lib/agents/github/tokens.d.ts.map +1 -0
  41. package/lib/agents/github/webhook.d.ts +37 -0
  42. package/lib/agents/github/webhook.d.ts.map +1 -0
  43. package/lib/agents/index.d.ts +8 -0
  44. package/lib/agents/index.d.ts.map +1 -0
  45. package/lib/agents/state/atoms.d.ts +122 -0
  46. package/lib/agents/state/atoms.d.ts.map +1 -0
  47. package/lib/agents/types.d.ts +65 -0
  48. package/lib/agents/types.d.ts.map +1 -0
  49. package/lib/gateway/SupabaseGateway.d.ts.map +1 -1
  50. package/lib/gateway/types.d.ts +1 -1
  51. package/lib/gateway/types.d.ts.map +1 -1
  52. package/lib/workers/canvas-worker.js +6 -449
  53. package/lib/workers/data.d.ts.map +1 -1
  54. package/lib/workers/db-worker.d.ts +2 -2
  55. package/lib/workers/db-worker.d.ts.map +1 -1
  56. package/lib/workers/db-worker.js +14 -4170
  57. package/lib/workers/main.d.ts +0 -2
  58. package/lib/workers/main.d.ts.map +1 -1
  59. package/lib/workers/supabase-db-worker.js +11 -9325
  60. package/lib/workers/supabase-shared-worker.js +13 -12623
  61. package/lib/workers/ws-worker.d.ts +1 -2
  62. package/lib/workers/ws-worker.d.ts.map +1 -1
  63. package/lib/workers/ws-worker.js +6 -333
  64. package/main.js +5622 -4786
  65. package/package.json +2 -1
  66. package/src/index.ts +3 -0
  67. package/src/lib/agents/api-types.ts +170 -0
  68. package/src/lib/agents/auth/init.ts +200 -0
  69. package/src/lib/agents/cache.ts +127 -0
  70. package/src/lib/agents/client/callGuildVault.ts +71 -0
  71. package/src/lib/agents/client/callJson.ts +64 -0
  72. package/src/lib/agents/config.ts +83 -0
  73. package/src/lib/agents/constants.ts +11 -0
  74. package/src/lib/agents/createAgents.ts +62 -0
  75. package/src/lib/agents/ctx.ts +63 -0
  76. package/src/lib/agents/discord/bot.ts +169 -0
  77. package/src/lib/agents/discord/guilds.ts +106 -0
  78. package/src/lib/agents/formDrafts.ts +55 -0
  79. package/src/lib/agents/generated/agents-schema.ts +155 -0
  80. package/src/lib/agents/generated/discordsh-agents-schema.ts +96 -0
  81. package/src/lib/agents/github/backfill.ts +126 -0
  82. package/src/lib/agents/github/events.ts +131 -0
  83. package/src/lib/agents/github/pat.ts +150 -0
  84. package/src/lib/agents/github/repoConfig.ts +240 -0
  85. package/src/lib/agents/github/tokens.ts +222 -0
  86. package/src/lib/agents/github/webhook.ts +349 -0
  87. package/src/lib/agents/index.ts +35 -0
  88. package/src/lib/agents/state/atoms.ts +122 -0
  89. package/src/lib/agents/types.ts +83 -0
  90. package/src/lib/api.ts +71 -0
  91. package/src/lib/gateway/SupabaseGateway.ts +55 -2
  92. package/src/lib/gateway/WorkerCommunication.ts +1 -1
  93. package/src/lib/gateway/capabilities.spec.ts +144 -0
  94. package/src/lib/gateway/types.ts +97 -70
  95. package/src/lib/mod/module/supabase/mod-supabase.ts +10 -5
  96. package/src/lib/state/auth.ts +121 -3
  97. package/src/lib/state/bento.spec.ts +76 -0
  98. package/src/lib/state/bento.ts +51 -0
  99. package/src/lib/state/index.ts +64 -0
  100. package/src/lib/state/profile-sync.ts +214 -0
  101. package/src/lib/state/profile.ts +155 -0
  102. package/src/lib/state/staff.ts +128 -0
  103. package/src/lib/state/sync-bus.ts +109 -0
  104. package/src/lib/state/welcome-toast.spec.ts +101 -0
  105. package/src/lib/sw-recovery.spec.ts +165 -0
  106. package/src/lib/sw-recovery.ts +141 -0
  107. package/src/lib/types/event-schemas.spec.ts +120 -0
  108. package/src/lib/types/event-types.spec.ts +5 -2
  109. package/src/lib/types/event-types.ts +74 -0
  110. package/src/lib/workers/data.ts +5 -1
  111. package/src/lib/workers/db-worker.ts +34 -8
  112. package/src/lib/workers/main.ts +3 -18
  113. package/src/lib/workers/supabase-db-worker.ts +25 -3
  114. package/src/lib/workers/supabase-shared-worker.ts +43 -8
  115. package/src/lib/workers/ws-worker.spec.ts +41 -0
  116. package/src/lib/workers/ws-worker.ts +121 -28
  117. package/workers/main.js +2 -12
  118. package/comlink.js +0 -247
  119. package/index.js +0 -9263
  120. 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>;