@kbve/droid 0.0.6 → 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 (190) hide show
  1. package/droid.mjs +2312 -74
  2. package/index.d.ts +32 -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 +38 -0
  50. package/lib/gateway/SupabaseGateway.d.ts.map +1 -0
  51. package/lib/gateway/WorkerCommunication.d.ts +39 -0
  52. package/lib/gateway/WorkerCommunication.d.ts.map +1 -0
  53. package/lib/gateway/WorkerPool.d.ts +55 -0
  54. package/lib/gateway/WorkerPool.d.ts.map +1 -0
  55. package/lib/gateway/capabilities.d.ts +23 -0
  56. package/lib/gateway/capabilities.d.ts.map +1 -0
  57. package/lib/gateway/index.d.ts +9 -0
  58. package/lib/gateway/index.d.ts.map +1 -0
  59. package/lib/gateway/strategies/DirectStrategy.d.ts +52 -0
  60. package/lib/gateway/strategies/DirectStrategy.d.ts.map +1 -0
  61. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts +46 -0
  62. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts.map +1 -0
  63. package/lib/gateway/strategies/WebWorkerStrategy.d.ts +45 -0
  64. package/lib/gateway/strategies/WebWorkerStrategy.d.ts.map +1 -0
  65. package/lib/gateway/types.d.ts +81 -0
  66. package/lib/gateway/types.d.ts.map +1 -0
  67. package/lib/workers/canvas-ui-renderer.d.ts +25 -0
  68. package/lib/workers/canvas-ui-renderer.d.ts.map +1 -0
  69. package/lib/workers/canvas-worker.d.ts +14 -0
  70. package/lib/workers/canvas-worker.d.ts.map +1 -0
  71. package/lib/workers/canvas-worker.js +5 -302
  72. package/lib/workers/data.d.ts +40 -0
  73. package/lib/workers/data.d.ts.map +1 -0
  74. package/lib/workers/db-worker.d.ts +69 -0
  75. package/lib/workers/db-worker.d.ts.map +1 -0
  76. package/lib/workers/db-worker.js +13 -3636
  77. package/lib/workers/events.d.ts +11 -0
  78. package/lib/workers/events.d.ts.map +1 -0
  79. package/lib/workers/flexbuilder.d.ts +4 -0
  80. package/lib/workers/flexbuilder.d.ts.map +1 -0
  81. package/lib/workers/init.d.ts +11 -0
  82. package/lib/workers/init.d.ts.map +1 -0
  83. package/lib/workers/main.d.ts +105 -0
  84. package/lib/workers/main.d.ts.map +1 -0
  85. package/lib/workers/supabase-db-worker.d.ts +2 -0
  86. package/lib/workers/supabase-db-worker.d.ts.map +1 -0
  87. package/lib/workers/supabase-db-worker.js +23 -0
  88. package/lib/workers/supabase-shared-worker.d.ts +2 -0
  89. package/lib/workers/supabase-shared-worker.d.ts.map +1 -0
  90. package/lib/workers/supabase-shared-worker.js +25 -0
  91. package/lib/workers/tools.d.ts +4 -0
  92. package/lib/workers/tools.d.ts.map +1 -0
  93. package/lib/workers/validate.d.ts +15 -0
  94. package/lib/workers/validate.d.ts.map +1 -0
  95. package/lib/workers/ws-worker.d.ts +10 -0
  96. package/lib/workers/ws-worker.d.ts.map +1 -0
  97. package/lib/workers/ws-worker.js +5 -273
  98. package/main.js +5711 -0
  99. package/package.json +18 -6
  100. package/src/index.ts +41 -13
  101. package/src/lib/agents/api-types.ts +170 -0
  102. package/src/lib/agents/auth/init.ts +200 -0
  103. package/src/lib/agents/cache.ts +127 -0
  104. package/src/lib/agents/client/callGuildVault.ts +71 -0
  105. package/src/lib/agents/client/callJson.ts +64 -0
  106. package/src/lib/agents/config.ts +83 -0
  107. package/src/lib/agents/constants.ts +11 -0
  108. package/src/lib/agents/createAgents.ts +62 -0
  109. package/src/lib/agents/ctx.ts +63 -0
  110. package/src/lib/agents/discord/bot.ts +169 -0
  111. package/src/lib/agents/discord/guilds.ts +106 -0
  112. package/src/lib/agents/formDrafts.ts +55 -0
  113. package/src/lib/agents/generated/agents-schema.ts +155 -0
  114. package/src/lib/agents/generated/discordsh-agents-schema.ts +96 -0
  115. package/src/lib/agents/github/backfill.ts +126 -0
  116. package/src/lib/agents/github/events.ts +131 -0
  117. package/src/lib/agents/github/pat.ts +150 -0
  118. package/src/lib/agents/github/repoConfig.ts +240 -0
  119. package/src/lib/agents/github/tokens.ts +222 -0
  120. package/src/lib/agents/github/webhook.ts +349 -0
  121. package/src/lib/agents/index.ts +35 -0
  122. package/src/lib/agents/state/atoms.ts +122 -0
  123. package/src/lib/agents/types.ts +83 -0
  124. package/src/lib/api.ts +71 -0
  125. package/src/lib/droid.spec.ts +14 -19
  126. package/src/lib/droid.ts +48 -4
  127. package/src/lib/gateway/SupabaseGateway.ts +236 -0
  128. package/src/lib/gateway/WorkerCommunication.ts +132 -0
  129. package/src/lib/gateway/WorkerPool.ts +304 -0
  130. package/src/lib/gateway/capabilities.spec.ts +144 -0
  131. package/src/lib/gateway/capabilities.ts +80 -0
  132. package/src/lib/gateway/index.ts +15 -0
  133. package/src/lib/gateway/strategies/DirectStrategy.ts +443 -0
  134. package/src/lib/gateway/strategies/SharedWorkerStrategy.ts +278 -0
  135. package/src/lib/gateway/strategies/WebWorkerStrategy.ts +278 -0
  136. package/src/lib/gateway/types.ts +139 -0
  137. package/src/lib/mod/mod-manager.ts +13 -8
  138. package/src/lib/mod/module/bento/mod-bento.worker.ts +1 -1
  139. package/src/lib/mod/module/supabase/mod-supabase.spec.ts +21 -25
  140. package/src/lib/mod/module/supabase/mod-supabase.ts +73 -0
  141. package/src/lib/mod/module/supabase/mod-supabase.worker.ts +1 -56
  142. package/src/lib/state/auth.spec.ts +58 -0
  143. package/src/lib/state/auth.ts +148 -0
  144. package/src/lib/state/bento.spec.ts +76 -0
  145. package/src/lib/state/bento.ts +51 -0
  146. package/src/lib/state/index.ts +98 -0
  147. package/src/lib/state/overlay-manager.ts +125 -0
  148. package/src/lib/state/profile-sync.ts +214 -0
  149. package/src/lib/state/profile.ts +155 -0
  150. package/src/lib/state/router.ts +24 -0
  151. package/src/lib/state/staff.ts +128 -0
  152. package/src/lib/state/sync-bus.ts +109 -0
  153. package/src/lib/state/theme-sync.ts +75 -0
  154. package/src/lib/state/toasts.spec.ts +106 -0
  155. package/src/lib/state/toasts.ts +34 -0
  156. package/src/lib/state/ui.spec.ts +148 -0
  157. package/src/lib/state/ui.ts +45 -0
  158. package/src/lib/state/welcome-toast.spec.ts +101 -0
  159. package/src/lib/state/welcome-toast.ts +76 -0
  160. package/src/lib/sw-recovery.spec.ts +165 -0
  161. package/src/lib/sw-recovery.ts +141 -0
  162. package/src/lib/types/bento.spec.ts +116 -0
  163. package/src/lib/types/bento.ts +94 -55
  164. package/src/lib/types/event-schemas.spec.ts +120 -0
  165. package/src/lib/types/event-types.spec.ts +96 -0
  166. package/src/lib/types/event-types.ts +113 -3
  167. package/src/lib/types/modules.ts +12 -5
  168. package/src/lib/types/ui-event-types.ts +66 -0
  169. package/src/lib/workers/canvas-ui-renderer.ts +266 -0
  170. package/src/lib/workers/canvas-worker.ts +94 -9
  171. package/src/lib/workers/data.ts +64 -37
  172. package/src/lib/workers/db-worker.ts +105 -32
  173. package/src/lib/workers/events.spec.ts +208 -0
  174. package/src/lib/workers/events.ts +27 -14
  175. package/src/lib/workers/flexbuilder.ts +2 -2
  176. package/src/lib/workers/init.ts +25 -17
  177. package/src/lib/workers/main.ts +506 -327
  178. package/src/lib/workers/supabase-db-worker.ts +243 -0
  179. package/src/lib/workers/supabase-shared-worker.ts +607 -0
  180. package/src/lib/workers/validate.ts +64 -0
  181. package/src/lib/workers/ws-worker.spec.ts +41 -0
  182. package/src/lib/workers/ws-worker.ts +255 -31
  183. package/src/setup-vitest.ts +4 -2
  184. package/src/types.ts +30 -0
  185. package/src/vite-env.d.ts +1 -0
  186. package/workers/main.js +2 -1125
  187. package/comlink.js +0 -253
  188. package/index.js +0 -2972
  189. package/reference.js +0 -259
  190. package/src/types.d.ts +0 -31
@@ -0,0 +1,64 @@
1
+ import type { AgentsCtx, CallJson } from '../ctx';
2
+
3
+ export function makeCallJson(ctx: AgentsCtx): CallJson {
4
+ return async function callJson<T>(
5
+ url: string,
6
+ body: Record<string, unknown>,
7
+ opts: { retriedAfterResync?: boolean } = {},
8
+ ) {
9
+ const accessToken = ctx.store.$accessToken.get();
10
+ if (!accessToken) return { ok: false, error: 'Not authenticated' };
11
+ try {
12
+ const resp = await fetch(url, {
13
+ method: 'POST',
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ Authorization: `Bearer ${accessToken}`,
17
+ },
18
+ body: JSON.stringify(body),
19
+ });
20
+ const text = await resp.text();
21
+ let parsed: unknown;
22
+ try {
23
+ parsed = text.length > 0 ? JSON.parse(text) : {};
24
+ } catch {
25
+ return {
26
+ ok: false,
27
+ error: `HTTP ${resp.status}: ${text.slice(0, 200)}`,
28
+ };
29
+ }
30
+ if (!resp.ok) {
31
+ const b = parsed as {
32
+ error?: string;
33
+ sqlstate?: string | null;
34
+ hint?: string | null;
35
+ context?: string | null;
36
+ } | null;
37
+ const errMsg = b?.error ?? '';
38
+ const looksStale =
39
+ resp.status === 403 &&
40
+ /owned_guilds/i.test(errMsg) &&
41
+ !opts.retriedAfterResync;
42
+ if (looksStale) {
43
+ const resynced = await ctx.resyncOwnedGuilds();
44
+ if (resynced) {
45
+ return ctx.callJson<T>(url, body, {
46
+ retriedAfterResync: true,
47
+ });
48
+ }
49
+ }
50
+ const parts: string[] = [errMsg || `HTTP ${resp.status}`];
51
+ if (b?.sqlstate) parts.push(`sqlstate=${b.sqlstate}`);
52
+ if (b?.hint) parts.push(`hint=${b.hint}`);
53
+ if (b?.context) parts.push(`context=${b.context}`);
54
+ return { ok: false, error: parts.join(' · ') };
55
+ }
56
+ return { ok: true, data: parsed as T };
57
+ } catch (e) {
58
+ return {
59
+ ok: false,
60
+ error: e instanceof Error ? e.message : 'Network error',
61
+ };
62
+ }
63
+ };
64
+ }
@@ -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
+ }