@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,349 @@
1
+ import type { AgentsCtx } from '../ctx';
2
+ import type { AgentsApi } from '../api-types';
3
+ import {
4
+ GithubRepoHookSchema,
5
+ WebhookDeliverySchema,
6
+ } from '../generated/discordsh-agents-schema';
7
+ import type {
8
+ GithubRepoHook,
9
+ Result,
10
+ WebhookDelivery,
11
+ WebhookInstallResult,
12
+ } from '../types';
13
+
14
+ export function makeWebhook(ctx: AgentsCtx, api: AgentsApi) {
15
+ const { store, endpoints } = ctx;
16
+
17
+ function setWebhookDraft(guildId: string, secret: string | null): void {
18
+ const m = { ...store.$webhookDrafts.get() };
19
+ if (secret === null) delete m[guildId];
20
+ else m[guildId] = secret;
21
+ store.$webhookDrafts.set(m);
22
+ }
23
+
24
+ function clearWebhookError(guildId: string): void {
25
+ const m = { ...store.$webhookErrors.get() };
26
+ delete m[guildId];
27
+ store.$webhookErrors.set(m);
28
+ }
29
+
30
+ async function saveWebhookDraft(
31
+ guildId: string,
32
+ tokenName: string,
33
+ description: string,
34
+ ): Promise<Result<{ tokenId: string }>> {
35
+ const secret = store.$webhookDrafts.get()[guildId];
36
+ if (!secret) return { ok: false, error: 'No secret to save' };
37
+ store.$webhookSavingFor.set({
38
+ ...store.$webhookSavingFor.get(),
39
+ [guildId]: true,
40
+ });
41
+ store.$webhookErrors.set({
42
+ ...store.$webhookErrors.get(),
43
+ [guildId]: null,
44
+ });
45
+ const r = await api.addToken({
46
+ tokenName,
47
+ service: 'github_webhook',
48
+ tokenValue: secret,
49
+ description,
50
+ });
51
+ const savingDone = { ...store.$webhookSavingFor.get() };
52
+ delete savingDone[guildId];
53
+ store.$webhookSavingFor.set(savingDone);
54
+ if (r.ok) {
55
+ setWebhookDraft(guildId, null);
56
+ } else {
57
+ store.$webhookErrors.set({
58
+ ...store.$webhookErrors.get(),
59
+ [guildId]: r.error,
60
+ });
61
+ }
62
+ return r;
63
+ }
64
+
65
+ async function installRepoWebhook(
66
+ guildId: string,
67
+ owner: string,
68
+ repo: string,
69
+ ): Promise<WebhookInstallResult | { ok: false; error: string }> {
70
+ const r = await ctx.callJson<{
71
+ installed: boolean;
72
+ already_present: boolean;
73
+ hook_id: number | null;
74
+ }>(endpoints.ghAdmin, {
75
+ command: 'webhooks.install',
76
+ server_id: guildId,
77
+ owner,
78
+ repo,
79
+ });
80
+ if (!r.ok) return r;
81
+ return {
82
+ ok: true,
83
+ installed: !!r.data.installed,
84
+ alreadyPresent: !!r.data.already_present,
85
+ hookId: r.data.hook_id ?? null,
86
+ };
87
+ }
88
+
89
+ async function pingRepoWebhook(
90
+ guildId: string,
91
+ owner: string,
92
+ repo: string,
93
+ ): Promise<
94
+ | { ok: true; hookId: number; url: string }
95
+ | { ok: false; error: string; retryAfterMs?: number }
96
+ > {
97
+ const r = await ctx.callJson<{
98
+ pinged: boolean;
99
+ hook_id: number;
100
+ url: string;
101
+ }>(endpoints.ghAdmin, {
102
+ command: 'webhooks.ping',
103
+ server_id: guildId,
104
+ owner,
105
+ repo,
106
+ });
107
+ if (!r.ok) return { ok: false, error: r.error };
108
+ return { ok: true, hookId: r.data.hook_id, url: r.data.url };
109
+ }
110
+
111
+ async function listRepoWebhooks(
112
+ guildId: string,
113
+ owner: string,
114
+ repo: string,
115
+ ): Promise<Result<{ expectedUrl: string; hooks: GithubRepoHook[] }>> {
116
+ const r = await ctx.callJson<{
117
+ expected_url: string;
118
+ hooks: GithubRepoHook[];
119
+ }>(endpoints.ghAdmin, {
120
+ command: 'webhooks.list',
121
+ server_id: guildId,
122
+ owner,
123
+ repo,
124
+ });
125
+ if (!r.ok) return r;
126
+ const parsed = GithubRepoHookSchema.array().safeParse(
127
+ r.data.hooks ?? [],
128
+ );
129
+ if (!parsed.success) {
130
+ return {
131
+ ok: false,
132
+ error: 'Webhook list failed schema validation',
133
+ };
134
+ }
135
+ return {
136
+ ok: true,
137
+ expectedUrl: r.data.expected_url,
138
+ hooks: parsed.data,
139
+ };
140
+ }
141
+
142
+ function setWebhookInstallSelected(guildId: string, repo: string): void {
143
+ store.$webhookInstallSelected.set({
144
+ ...store.$webhookInstallSelected.get(),
145
+ [guildId]: repo,
146
+ });
147
+ }
148
+
149
+ function clearWebhookInstallResult(guildId: string): void {
150
+ const m = { ...store.$webhookInstallResults.get() };
151
+ delete m[guildId];
152
+ store.$webhookInstallResults.set(m);
153
+ }
154
+
155
+ async function installWebhookForGuild(guildId: string): Promise<void> {
156
+ const repoFull = store.$webhookInstallSelected.get()[guildId] ?? '';
157
+ const [owner, repo] = repoFull.split('/');
158
+ if (!owner || !repo) return;
159
+ store.$webhookInstallBusyFor.set({
160
+ ...store.$webhookInstallBusyFor.get(),
161
+ [guildId]: true,
162
+ });
163
+ const resultsClear = { ...store.$webhookInstallResults.get() };
164
+ delete resultsClear[guildId];
165
+ store.$webhookInstallResults.set(resultsClear);
166
+ const r = await installRepoWebhook(guildId, owner, repo);
167
+ const busyDone = { ...store.$webhookInstallBusyFor.get() };
168
+ delete busyDone[guildId];
169
+ store.$webhookInstallBusyFor.set(busyDone);
170
+ store.$webhookInstallResults.set({
171
+ ...store.$webhookInstallResults.get(),
172
+ [guildId]: r,
173
+ });
174
+ }
175
+
176
+ async function pingWebhookForGuild(guildId: string): Promise<void> {
177
+ const repoFull = store.$webhookInstallSelected.get()[guildId] ?? '';
178
+ const [owner, repo] = repoFull.split('/');
179
+ if (!owner || !repo) return;
180
+ store.$webhookPingBusyFor.set({
181
+ ...store.$webhookPingBusyFor.get(),
182
+ [guildId]: true,
183
+ });
184
+ const r = await pingRepoWebhook(guildId, owner, repo);
185
+ const busyDone = { ...store.$webhookPingBusyFor.get() };
186
+ delete busyDone[guildId];
187
+ store.$webhookPingBusyFor.set(busyDone);
188
+ const result = r.ok
189
+ ? { ok: true as const, hookId: r.hookId, at: Date.now() }
190
+ : { ok: false as const, error: r.error };
191
+ store.$webhookPingResults.set({
192
+ ...store.$webhookPingResults.get(),
193
+ [guildId]: result,
194
+ });
195
+ }
196
+
197
+ async function verifyWebhookInstall(
198
+ guildId: string,
199
+ repoFull: string,
200
+ ): Promise<void> {
201
+ const [owner, repo] = repoFull.split('/');
202
+ if (!owner || !repo) return;
203
+ const cur = store.$webhookInstallResults.get()[guildId];
204
+ if (cur && cur.ok && (cur.installed || cur.alreadyPresent)) return;
205
+ const r = await listRepoWebhooks(guildId, owner, repo);
206
+ if (!r.ok) return;
207
+ const kbveHook = r.hooks.find((h) => h.is_kbve);
208
+ if (!kbveHook) return;
209
+ store.$webhookInstallResults.set({
210
+ ...store.$webhookInstallResults.get(),
211
+ [guildId]: {
212
+ ok: true as const,
213
+ installed: false,
214
+ alreadyPresent: true,
215
+ hookId: kbveHook.id,
216
+ },
217
+ });
218
+ }
219
+
220
+ async function rotateWebhookForGuild(
221
+ guildId: string,
222
+ owner: string,
223
+ repo: string,
224
+ ): Promise<void> {
225
+ store.$webhookRotateBusyFor.set({
226
+ ...store.$webhookRotateBusyFor.get(),
227
+ [guildId]: true,
228
+ });
229
+ const r = await ctx.callJson<{ rotated: boolean; hook_id: number }>(
230
+ endpoints.ghAdmin,
231
+ { command: 'webhooks.rotate', server_id: guildId, owner, repo },
232
+ );
233
+ const busy = { ...store.$webhookRotateBusyFor.get() };
234
+ delete busy[guildId];
235
+ store.$webhookRotateBusyFor.set(busy);
236
+ const result = r.ok
237
+ ? { ok: true as const, hookId: r.data.hook_id, at: Date.now() }
238
+ : { ok: false as const, error: r.error };
239
+ store.$webhookRotateResults.set({
240
+ ...store.$webhookRotateResults.get(),
241
+ [guildId]: result,
242
+ });
243
+ if (r.ok) {
244
+ await api.refreshSelectedGuild();
245
+ }
246
+ }
247
+
248
+ async function deleteWebhookForGuild(
249
+ guildId: string,
250
+ owner: string,
251
+ repo: string,
252
+ ): Promise<Result> {
253
+ store.$webhookDeleteBusyFor.set({
254
+ ...store.$webhookDeleteBusyFor.get(),
255
+ [guildId]: true,
256
+ });
257
+ const r = await ctx.callJson<{
258
+ deleted: boolean;
259
+ already_absent?: boolean;
260
+ hook_id?: number;
261
+ }>(endpoints.ghAdmin, {
262
+ command: 'webhooks.delete',
263
+ server_id: guildId,
264
+ owner,
265
+ repo,
266
+ });
267
+ const busy = { ...store.$webhookDeleteBusyFor.get() };
268
+ delete busy[guildId];
269
+ store.$webhookDeleteBusyFor.set(busy);
270
+ if (!r.ok) return { ok: false, error: r.error };
271
+ const results = { ...store.$webhookInstallResults.get() };
272
+ delete results[guildId];
273
+ store.$webhookInstallResults.set(results);
274
+ return { ok: true };
275
+ }
276
+
277
+ async function loadWebhookDeliveries(
278
+ guildId: string,
279
+ owner: string,
280
+ repo: string,
281
+ limit = 10,
282
+ ): Promise<void> {
283
+ const key = `${guildId}:${owner}/${repo}`;
284
+ store.$webhookDeliveriesLoading.set({
285
+ ...store.$webhookDeliveriesLoading.get(),
286
+ [key]: true,
287
+ });
288
+ store.$webhookDeliveriesError.set({
289
+ ...store.$webhookDeliveriesError.get(),
290
+ [key]: null,
291
+ });
292
+ const r = await ctx.callJson<{
293
+ hook_id: number;
294
+ deliveries: WebhookDelivery[];
295
+ }>(endpoints.ghAdmin, {
296
+ command: 'webhooks.deliveries',
297
+ server_id: guildId,
298
+ owner,
299
+ repo,
300
+ limit,
301
+ });
302
+ const loading = { ...store.$webhookDeliveriesLoading.get() };
303
+ delete loading[key];
304
+ store.$webhookDeliveriesLoading.set(loading);
305
+ if (!r.ok) {
306
+ store.$webhookDeliveriesError.set({
307
+ ...store.$webhookDeliveriesError.get(),
308
+ [key]: r.error,
309
+ });
310
+ return;
311
+ }
312
+ const parsed = WebhookDeliverySchema.array().safeParse(
313
+ r.data.deliveries ?? [],
314
+ );
315
+ if (!parsed.success) {
316
+ store.$webhookDeliveriesError.set({
317
+ ...store.$webhookDeliveriesError.get(),
318
+ [key]: 'Delivery list failed schema validation',
319
+ });
320
+ return;
321
+ }
322
+ store.$webhookDeliveries.set({
323
+ ...store.$webhookDeliveries.get(),
324
+ [key]: parsed.data,
325
+ });
326
+ }
327
+
328
+ function webhookUrlFor(guildId: string): string {
329
+ return `${endpoints.ghWebhookBase}/${guildId}`;
330
+ }
331
+
332
+ return {
333
+ setWebhookDraft,
334
+ clearWebhookError,
335
+ saveWebhookDraft,
336
+ installRepoWebhook,
337
+ pingRepoWebhook,
338
+ listRepoWebhooks,
339
+ setWebhookInstallSelected,
340
+ clearWebhookInstallResult,
341
+ installWebhookForGuild,
342
+ pingWebhookForGuild,
343
+ verifyWebhookInstall,
344
+ rotateWebhookForGuild,
345
+ deleteWebhookForGuild,
346
+ loadWebhookDeliveries,
347
+ webhookUrlFor,
348
+ };
349
+ }
@@ -0,0 +1,35 @@
1
+ export { createAgents } from './createAgents';
2
+ export type { AgentsApi, AgentsMethods } from './api-types';
3
+ export type { AgentsStore } from './state/atoms';
4
+ export type {
5
+ AgentsConfig,
6
+ AgentsNotice,
7
+ AgentsSession,
8
+ ResolvedAgentsConfig,
9
+ } from './config';
10
+ export {
11
+ emptyBotConfigFormDraft,
12
+ botConfigToFormDraft,
13
+ botConfigFromFormDraft,
14
+ } from './formDrafts';
15
+ export { parseJwtPayload, extractJwtOwnedGuildIds } from './cache';
16
+ export type {
17
+ AgentTokenRow,
18
+ AuthState as AgentsAuthState,
19
+ BackfillResult,
20
+ BotConfigFormDraft,
21
+ DiscordChannel,
22
+ DiscordForumChannel,
23
+ DiscordGuild,
24
+ DiscordshConfig,
25
+ EventQueueStats,
26
+ FailedEvent,
27
+ GithubRepoHook,
28
+ GuildChannels,
29
+ PatValidation,
30
+ PendingEvent,
31
+ Result as AgentsResult,
32
+ WebhookDelivery,
33
+ WebhookInstallResult,
34
+ WebhookOpResult,
35
+ } from './types';
@@ -0,0 +1,122 @@
1
+ import { atom, type WritableAtom } from 'nanostores';
2
+ import { persistentAtom } from '@nanostores/persistent';
3
+ import type {
4
+ AgentTokenRow,
5
+ AuthState,
6
+ BackfillResult,
7
+ BotConfigFormDraft,
8
+ DiscordGuild,
9
+ EventQueueStats,
10
+ FailedEvent,
11
+ GuildChannels,
12
+ PatValidation,
13
+ PendingEvent,
14
+ WebhookDelivery,
15
+ WebhookInstallResult,
16
+ WebhookOpResult,
17
+ } from '../types';
18
+
19
+ type Fail = { ok: false; error: string };
20
+
21
+ function jsonPersistent<T>(key: string, fallback: T): WritableAtom<T> {
22
+ return persistentAtom<T>(key, fallback, {
23
+ encode: (v) => JSON.stringify(v),
24
+ decode: (raw) => {
25
+ try {
26
+ return raw ? (JSON.parse(raw) as T) : fallback;
27
+ } catch {
28
+ return fallback;
29
+ }
30
+ },
31
+ });
32
+ }
33
+
34
+ export function createAgentsStore() {
35
+ return {
36
+ $authState: atom<AuthState>('loading'),
37
+ $accessToken: atom<string | null>(null),
38
+ $providerToken: atom<string | null>(null),
39
+ $userId: atom<string | null>(null),
40
+
41
+ $guilds: atom<DiscordGuild[]>([]),
42
+ $guildsLoading: atom<boolean>(false),
43
+ $guildsError: atom<string | null>(null),
44
+ $guildsStale: atom<boolean>(false),
45
+
46
+ $selectedGuildId: atom<string | null>(null),
47
+
48
+ $tokens: atom<AgentTokenRow[]>([]),
49
+ $tokensLoading: atom<boolean>(false),
50
+ $tokensError: atom<string | null>(null),
51
+
52
+ $botConfigDrafts: atom<Record<string, BotConfigFormDraft>>({}),
53
+ $botConfigSavingFor: atom<Record<string, boolean>>({}),
54
+ $botConfigErrors: atom<Record<string, string | null>>({}),
55
+ $botConfigLoadedFor: atom<Record<string, boolean>>({}),
56
+
57
+ $repoAllowlistDrafts: atom<Record<string, string[]>>({}),
58
+ $repoAllowlistSavingFor: atom<Record<string, boolean>>({}),
59
+ $repoAllowlistErrors: atom<Record<string, string | null>>({}),
60
+ $repoAllowlistLoadedFor: atom<Record<string, boolean>>({}),
61
+
62
+ $webhookDrafts: atom<Record<string, string | null>>({}),
63
+ $webhookSavingFor: atom<Record<string, boolean>>({}),
64
+ $webhookErrors: atom<Record<string, string | null>>({}),
65
+
66
+ $patDrafts: atom<Record<string, string>>({}),
67
+ $patValidatedFor: atom<Record<string, PatValidation | null>>({}),
68
+ $patValidatingFor: atom<Record<string, boolean>>({}),
69
+ $patSavingFor: atom<Record<string, boolean>>({}),
70
+ $patErrors: atom<Record<string, string | null>>({}),
71
+
72
+ $backfillDrafts: atom<Record<string, { owner: string; repo: string }>>(
73
+ {},
74
+ ),
75
+ $backfillBusyFor: atom<Record<string, boolean>>({}),
76
+ $backfillResults: atom<Record<string, BackfillResult | Fail | null>>(
77
+ {},
78
+ ),
79
+
80
+ $webhookInstallSelected: atom<Record<string, string>>({}),
81
+ $webhookInstallBusyFor: atom<Record<string, boolean>>({}),
82
+ $webhookInstallResults: atom<
83
+ Record<string, WebhookInstallResult | Fail | null>
84
+ >({}),
85
+ $webhookPingBusyFor: atom<Record<string, boolean>>({}),
86
+ $webhookPingResults: atom<
87
+ Record<string, WebhookOpResult | Fail | null>
88
+ >({}),
89
+
90
+ $guildChannels: jsonPersistent<
91
+ Record<string, GuildChannels & { cached_at: number }>
92
+ >('kbve:agents:guild_channels:v1', {}),
93
+ $guildChannelsLoading: atom<Record<string, boolean>>({}),
94
+ $guildChannelsError: atom<Record<string, string | null>>({}),
95
+
96
+ $botMembership: jsonPersistent<
97
+ Record<string, { isMember: boolean; cached_at: number }>
98
+ >('kbve:agents:bot_membership:v1', {}),
99
+ $botMembershipLoading: atom<Record<string, boolean>>({}),
100
+ $botMembershipError: atom<Record<string, string | null>>({}),
101
+
102
+ $webhookDeliveries: atom<Record<string, WebhookDelivery[]>>({}),
103
+ $webhookDeliveriesLoading: atom<Record<string, boolean>>({}),
104
+ $webhookDeliveriesError: atom<Record<string, string | null>>({}),
105
+
106
+ $webhookRotateBusyFor: atom<Record<string, boolean>>({}),
107
+ $webhookRotateResults: atom<
108
+ Record<string, WebhookOpResult | Fail | null>
109
+ >({}),
110
+ $webhookDeleteBusyFor: atom<Record<string, boolean>>({}),
111
+
112
+ $eventStats: atom<Record<string, EventQueueStats>>({}),
113
+ $eventStatsLoading: atom<Record<string, boolean>>({}),
114
+ $failedEvents: atom<Record<string, FailedEvent[]>>({}),
115
+ $failedEventsLoading: atom<Record<string, boolean>>({}),
116
+ $pendingEvents: atom<Record<string, PendingEvent[]>>({}),
117
+ $pendingEventsLoading: atom<Record<string, boolean>>({}),
118
+ $eventRequeueBusyFor: atom<Record<string, boolean>>({}),
119
+ };
120
+ }
121
+
122
+ export type AgentsStore = ReturnType<typeof createAgentsStore>;
@@ -0,0 +1,83 @@
1
+ export type AuthState =
2
+ | 'loading'
3
+ | 'authenticated'
4
+ | 'unauthenticated'
5
+ | 'discord_reauth_required';
6
+
7
+ export interface DiscordGuild {
8
+ id: string;
9
+ name: string;
10
+ icon: string | null;
11
+ owner: boolean;
12
+ permissions: string;
13
+ }
14
+
15
+ export interface BotConfigFormDraft {
16
+ default_repo: string;
17
+ claim_channel_id: string;
18
+ forum_channel_id: string;
19
+ noticeboard_channel_id: string;
20
+ taskboard_channel_id: string;
21
+ max_assignees: string;
22
+ mirror_pr_events: boolean;
23
+ active: boolean;
24
+ }
25
+
26
+ export type { AgentTokenRow, AgentError } from './generated/agents-schema';
27
+ export type {
28
+ DiscordshConfig,
29
+ GithubRepoHook,
30
+ WebhookDelivery,
31
+ EventQueueStats,
32
+ FailedEvent,
33
+ PendingEvent,
34
+ } from './generated/discordsh-agents-schema';
35
+
36
+ export interface DiscordForumChannel {
37
+ id: string;
38
+ name: string;
39
+ parent_id: string | null;
40
+ position: number;
41
+ }
42
+
43
+ export interface DiscordChannel {
44
+ id: string;
45
+ name: string;
46
+ parent_id: string | null;
47
+ position: number;
48
+ }
49
+
50
+ export interface GuildChannels {
51
+ forums: DiscordChannel[];
52
+ texts: DiscordChannel[];
53
+ }
54
+
55
+ export interface PatValidation {
56
+ login: string;
57
+ scopes: string[];
58
+ tokenType: string;
59
+ }
60
+
61
+ export interface BackfillResult {
62
+ ok: true;
63
+ upserted: number;
64
+ pages: number;
65
+ rateLimitRemaining: number | null;
66
+ }
67
+
68
+ export interface WebhookInstallResult {
69
+ ok: true;
70
+ installed: boolean;
71
+ alreadyPresent: boolean;
72
+ hookId: number | null;
73
+ }
74
+
75
+ export interface WebhookOpResult {
76
+ ok: true;
77
+ hookId: number;
78
+ at: number;
79
+ }
80
+
81
+ export type Result<T = unknown> =
82
+ | ({ ok: true } & T)
83
+ | { ok: false; error: string };
package/src/lib/api.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Unified API fetch for the KBVE web + mobile clients.
3
+ *
4
+ * One place for base-URL join, bearer auth, JSON headers, error parsing, and
5
+ * 204 handling — so callers stop reimplementing `authedFetch`/`publicGet` per
6
+ * component (which drifted and caused base-URL/CORS bugs). The `apiBase` is
7
+ * injected by the app (web dev = same-origin dev proxy, web prod = '', mobile
8
+ * = absolute origin) so this module stays platform-agnostic.
9
+ */
10
+
11
+ export class ApiError extends Error {
12
+ status: number;
13
+ code?: string;
14
+ constructor(message: string, status: number, code?: string) {
15
+ super(message);
16
+ this.name = 'ApiError';
17
+ this.status = status;
18
+ this.code = code;
19
+ }
20
+ }
21
+
22
+ async function parseApiError(res: Response): Promise<ApiError> {
23
+ let detail = '';
24
+ let code: string | undefined;
25
+ try {
26
+ const txt = await res.text();
27
+ try {
28
+ const parsed = JSON.parse(txt) as {
29
+ message?: string;
30
+ error?: string;
31
+ detail?: string;
32
+ };
33
+ detail = parsed.message || parsed.error || parsed.detail || txt;
34
+ code = parsed.error;
35
+ } catch {
36
+ detail = txt;
37
+ }
38
+ } catch {
39
+ /* body already consumed or unavailable */
40
+ }
41
+ return new ApiError(detail || res.statusText, res.status, code);
42
+ }
43
+
44
+ export interface ApiFetchOptions extends RequestInit {
45
+ /** Bearer token; sets `Authorization` when present. */
46
+ token?: string | null;
47
+ /** Origin/prefix joined before `path`. '' = same-origin. */
48
+ apiBase?: string;
49
+ }
50
+
51
+ /**
52
+ * Fetch `${apiBase}${path}`, attaching the bearer token + JSON headers,
53
+ * throwing {@link ApiError} on non-2xx, and returning parsed JSON (or
54
+ * `undefined` for 204).
55
+ */
56
+ export async function apiFetch<T>(
57
+ path: string,
58
+ opts: ApiFetchOptions = {},
59
+ ): Promise<T> {
60
+ const { token, apiBase = '', ...init } = opts;
61
+ const headers = new Headers(init.headers);
62
+ if (token) headers.set('Authorization', `Bearer ${token}`);
63
+ if (!headers.has('Accept')) headers.set('Accept', 'application/json');
64
+ if (init.body && !headers.has('Content-Type')) {
65
+ headers.set('Content-Type', 'application/json');
66
+ }
67
+ const res = await fetch(`${apiBase}${path}`, { ...init, headers });
68
+ if (!res.ok) throw await parseApiError(res);
69
+ if (res.status === 204) return undefined as T;
70
+ return (await res.json()) as T;
71
+ }