@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,96 @@
1
+ /**
2
+ * AUTO-GENERATED by proto-to-zod — DO NOT EDIT
3
+ *
4
+ * Source: ../descriptors/discordsh_agents.binpb
5
+ * Config: ../discordsh_agents-zod-config.json
6
+ * Generated: 2026-07-20T00:37:21.620Z
7
+ */
8
+
9
+ import { z } from 'zod';
10
+
11
+ // PendingEvent
12
+ export const PendingEventSchema = z.object({
13
+ id: z.number(),
14
+ owner: z.string(),
15
+ repo: z.string(),
16
+ number: z.number(),
17
+ event_type: z.string(),
18
+ actor: z.string().nullish(),
19
+ delivery_state: z.number(),
20
+ delivery_attempts: z.number(),
21
+ created_at: z.string(),
22
+ claimed_at: z.string().nullish(),
23
+ });
24
+
25
+ export type PendingEvent = z.infer<typeof PendingEventSchema>;
26
+
27
+ // FailedEvent
28
+ export const FailedEventSchema = z.object({
29
+ id: z.number(),
30
+ owner: z.string(),
31
+ repo: z.string(),
32
+ number: z.number(),
33
+ event_type: z.string(),
34
+ actor: z.string().nullish(),
35
+ delivery_attempts: z.number(),
36
+ last_error: z.string().nullish(),
37
+ created_at: z.string(),
38
+ updated_at: z.string(),
39
+ });
40
+
41
+ export type FailedEvent = z.infer<typeof FailedEventSchema>;
42
+
43
+ // EventQueueStats
44
+ export const EventQueueStatsSchema = z.object({
45
+ last_delivered_at: z.string().nullish(),
46
+ last_recorded_at: z.string().nullish(),
47
+ pending_count: z.number(),
48
+ in_flight_count: z.number(),
49
+ delivered_count: z.number(),
50
+ failed_count: z.number(),
51
+ oldest_pending_at: z.string().nullish(),
52
+ });
53
+
54
+ export type EventQueueStats = z.infer<typeof EventQueueStatsSchema>;
55
+
56
+ // WebhookDelivery
57
+ export const WebhookDeliverySchema = z.object({
58
+ id: z.number(),
59
+ guid: z.string(),
60
+ delivered_at: z.string(),
61
+ redelivery: z.boolean(),
62
+ duration: z.number(),
63
+ status: z.string(),
64
+ status_code: z.number(),
65
+ event: z.string(),
66
+ action: z.string().nullish(),
67
+ });
68
+
69
+ export type WebhookDelivery = z.infer<typeof WebhookDeliverySchema>;
70
+
71
+ // GithubRepoHook
72
+ export const GithubRepoHookSchema = z.object({
73
+ id: z.number(),
74
+ name: z.string(),
75
+ active: z.boolean(),
76
+ events: z.array(z.string()),
77
+ url: z.string().nullish(),
78
+ is_kbve: z.boolean(),
79
+ updated_at: z.string(),
80
+ });
81
+
82
+ export type GithubRepoHook = z.infer<typeof GithubRepoHookSchema>;
83
+
84
+ // DiscordshConfig
85
+ export const DiscordshConfigSchema = z.object({
86
+ default_repo: z.string().optional(),
87
+ claim_channel_id: z.string().optional(),
88
+ forum_channel_id: z.string().optional(),
89
+ noticeboard_channel_id: z.string().optional(),
90
+ taskboard_channel_id: z.string().optional(),
91
+ max_assignees: z.number().optional(),
92
+ mirror_pr_events: z.boolean().optional(),
93
+ active: z.boolean().optional(),
94
+ });
95
+
96
+ export type DiscordshConfig = z.infer<typeof DiscordshConfigSchema>;
@@ -0,0 +1,126 @@
1
+ import type { AgentsCtx } from '../ctx';
2
+ import type { BackfillResult } from '../types';
3
+
4
+ type Fail = { ok: false; error: string };
5
+
6
+ export function makeBackfill(ctx: AgentsCtx) {
7
+ const { store, endpoints } = ctx;
8
+
9
+ function patchBackfillDraft(
10
+ guildId: string,
11
+ partial: Partial<{ owner: string; repo: string }>,
12
+ ): void {
13
+ const drafts = store.$backfillDrafts.get();
14
+ const cur = drafts[guildId] ?? { owner: '', repo: '' };
15
+ store.$backfillDrafts.set({
16
+ ...drafts,
17
+ [guildId]: { ...cur, ...partial },
18
+ });
19
+ }
20
+
21
+ function clearBackfillResult(guildId: string): void {
22
+ const m = { ...store.$backfillResults.get() };
23
+ delete m[guildId];
24
+ store.$backfillResults.set(m);
25
+ }
26
+
27
+ async function runBackfill(input: {
28
+ owner: string;
29
+ repo: string;
30
+ state?: 'open' | 'closed' | 'all';
31
+ maxPages?: number;
32
+ perPage?: number;
33
+ }): Promise<BackfillResult | Fail> {
34
+ const accessToken = store.$accessToken.get();
35
+ const guildId = store.$selectedGuildId.get();
36
+ if (!accessToken || !guildId) {
37
+ return { ok: false, error: 'No guild selected or session missing' };
38
+ }
39
+
40
+ try {
41
+ const resp = await fetch(endpoints.ghBackfill, {
42
+ method: 'POST',
43
+ headers: {
44
+ 'Content-Type': 'application/json',
45
+ Authorization: `Bearer ${accessToken}`,
46
+ },
47
+ body: JSON.stringify({
48
+ owner: input.owner,
49
+ repo: input.repo,
50
+ guild_id: guildId,
51
+ state: input.state ?? 'open',
52
+ max_pages: input.maxPages ?? 1,
53
+ per_page: input.perPage ?? 30,
54
+ }),
55
+ });
56
+ const text = await resp.text();
57
+ let body: unknown;
58
+ try {
59
+ body = JSON.parse(text);
60
+ } catch {
61
+ return {
62
+ ok: false,
63
+ error: `HTTP ${resp.status}: ${text.slice(0, 200)}`,
64
+ };
65
+ }
66
+ if (!resp.ok) {
67
+ const errMsg =
68
+ (body as { error?: string } | null)?.error ??
69
+ `HTTP ${resp.status}`;
70
+ return { ok: false, error: errMsg };
71
+ }
72
+ const b = body as {
73
+ upserted?: number;
74
+ pages_walked?: number;
75
+ rate_limit_remaining?: number | null;
76
+ };
77
+ return {
78
+ ok: true,
79
+ upserted: b.upserted ?? 0,
80
+ pages: b.pages_walked ?? 0,
81
+ rateLimitRemaining: b.rate_limit_remaining ?? null,
82
+ };
83
+ } catch (e) {
84
+ return {
85
+ ok: false,
86
+ error: e instanceof Error ? e.message : 'Network error',
87
+ };
88
+ }
89
+ }
90
+
91
+ async function runBackfillForGuild(guildId: string): Promise<void> {
92
+ const draft = store.$backfillDrafts.get()[guildId] ?? {
93
+ owner: '',
94
+ repo: '',
95
+ };
96
+ if (!draft.owner || !draft.repo) return;
97
+ store.$backfillBusyFor.set({
98
+ ...store.$backfillBusyFor.get(),
99
+ [guildId]: true,
100
+ });
101
+ const resultsClear = { ...store.$backfillResults.get() };
102
+ delete resultsClear[guildId];
103
+ store.$backfillResults.set(resultsClear);
104
+ const r = await runBackfill({
105
+ owner: draft.owner,
106
+ repo: draft.repo,
107
+ state: 'open',
108
+ maxPages: 1,
109
+ perPage: 30,
110
+ });
111
+ const busyDone = { ...store.$backfillBusyFor.get() };
112
+ delete busyDone[guildId];
113
+ store.$backfillBusyFor.set(busyDone);
114
+ store.$backfillResults.set({
115
+ ...store.$backfillResults.get(),
116
+ [guildId]: r,
117
+ });
118
+ }
119
+
120
+ return {
121
+ patchBackfillDraft,
122
+ clearBackfillResult,
123
+ runBackfill,
124
+ runBackfillForGuild,
125
+ };
126
+ }
@@ -0,0 +1,131 @@
1
+ import type { AgentsCtx } from '../ctx';
2
+ import type { AgentsApi } from '../api-types';
3
+ import {
4
+ EventQueueStatsSchema,
5
+ FailedEventSchema,
6
+ PendingEventSchema,
7
+ } from '../generated/discordsh-agents-schema';
8
+ import type {
9
+ EventQueueStats,
10
+ FailedEvent,
11
+ PendingEvent,
12
+ Result,
13
+ } from '../types';
14
+
15
+ export function makeEvents(ctx: AgentsCtx, api: AgentsApi) {
16
+ const { store, endpoints } = ctx;
17
+
18
+ async function loadEventStats(guildId: string): Promise<void> {
19
+ store.$eventStatsLoading.set({
20
+ ...store.$eventStatsLoading.get(),
21
+ [guildId]: true,
22
+ });
23
+ const r = await ctx.callJson<EventQueueStats>(endpoints.ghAdmin, {
24
+ command: 'events.stats',
25
+ server_id: guildId,
26
+ });
27
+ const loading = { ...store.$eventStatsLoading.get() };
28
+ delete loading[guildId];
29
+ store.$eventStatsLoading.set(loading);
30
+ if (!r.ok) return;
31
+ const parsed = EventQueueStatsSchema.safeParse(r.data);
32
+ if (!parsed.success) return;
33
+ store.$eventStats.set({
34
+ ...store.$eventStats.get(),
35
+ [guildId]: parsed.data,
36
+ });
37
+ }
38
+
39
+ async function loadFailedEvents(
40
+ guildId: string,
41
+ limit = 10,
42
+ ): Promise<void> {
43
+ store.$failedEventsLoading.set({
44
+ ...store.$failedEventsLoading.get(),
45
+ [guildId]: true,
46
+ });
47
+ const r = await ctx.callJson<{ events: FailedEvent[] }>(
48
+ endpoints.ghAdmin,
49
+ { command: 'events.failed', server_id: guildId, limit },
50
+ );
51
+ const loading = { ...store.$failedEventsLoading.get() };
52
+ delete loading[guildId];
53
+ store.$failedEventsLoading.set(loading);
54
+ if (!r.ok) return;
55
+ const parsed = FailedEventSchema.array().safeParse(r.data.events ?? []);
56
+ if (!parsed.success) return;
57
+ store.$failedEvents.set({
58
+ ...store.$failedEvents.get(),
59
+ [guildId]: parsed.data,
60
+ });
61
+ }
62
+
63
+ async function loadPendingEvents(
64
+ guildId: string,
65
+ limit = 10,
66
+ ): Promise<void> {
67
+ store.$pendingEventsLoading.set({
68
+ ...store.$pendingEventsLoading.get(),
69
+ [guildId]: true,
70
+ });
71
+ const r = await ctx.callJson<{ events: PendingEvent[] }>(
72
+ endpoints.ghAdmin,
73
+ { command: 'events.pending', server_id: guildId, limit },
74
+ );
75
+ const loading = { ...store.$pendingEventsLoading.get() };
76
+ delete loading[guildId];
77
+ store.$pendingEventsLoading.set(loading);
78
+ if (!r.ok) return;
79
+ const parsed = PendingEventSchema.array().safeParse(
80
+ r.data.events ?? [],
81
+ );
82
+ if (!parsed.success) return;
83
+ store.$pendingEvents.set({
84
+ ...store.$pendingEvents.get(),
85
+ [guildId]: parsed.data,
86
+ });
87
+ }
88
+
89
+ async function requeueEvent(
90
+ guildId: string,
91
+ eventId: number,
92
+ reason?: string,
93
+ ): Promise<Result> {
94
+ const key = `${guildId}:${eventId}`;
95
+ store.$eventRequeueBusyFor.set({
96
+ ...store.$eventRequeueBusyFor.get(),
97
+ [key]: true,
98
+ });
99
+ const trimmedReason =
100
+ typeof reason === 'string' && reason.trim().length > 0
101
+ ? reason.trim().slice(0, 512)
102
+ : undefined;
103
+ const r = await ctx.callJson<{ requeued: boolean; event_id: number }>(
104
+ endpoints.ghAdmin,
105
+ {
106
+ command: 'events.requeue',
107
+ server_id: guildId,
108
+ event_id: eventId,
109
+ ...(trimmedReason ? { reason: trimmedReason } : {}),
110
+ },
111
+ );
112
+ const busy = { ...store.$eventRequeueBusyFor.get() };
113
+ delete busy[key];
114
+ store.$eventRequeueBusyFor.set(busy);
115
+ if (!r.ok) return { ok: false, error: r.error };
116
+ const cur = store.$failedEvents.get()[guildId] ?? [];
117
+ store.$failedEvents.set({
118
+ ...store.$failedEvents.get(),
119
+ [guildId]: cur.filter((e) => e.id !== eventId),
120
+ });
121
+ void api.loadEventStats(guildId);
122
+ return { ok: true };
123
+ }
124
+
125
+ return {
126
+ loadEventStats,
127
+ loadFailedEvents,
128
+ loadPendingEvents,
129
+ requeueEvent,
130
+ };
131
+ }
@@ -0,0 +1,150 @@
1
+ import type { AgentsCtx } from '../ctx';
2
+ import type { AgentsApi } from '../api-types';
3
+ import type { PatValidation, Result } from '../types';
4
+
5
+ export function makePat(ctx: AgentsCtx, api: AgentsApi) {
6
+ const { store } = ctx;
7
+
8
+ function setPatDraft(guildId: string, pat: string): void {
9
+ store.$patDrafts.set({ ...store.$patDrafts.get(), [guildId]: pat });
10
+ const validated = { ...store.$patValidatedFor.get() };
11
+ if (validated[guildId]) {
12
+ delete validated[guildId];
13
+ store.$patValidatedFor.set(validated);
14
+ }
15
+ }
16
+
17
+ function clearPatState(guildId: string): void {
18
+ const drafts = { ...store.$patDrafts.get() };
19
+ delete drafts[guildId];
20
+ store.$patDrafts.set(drafts);
21
+ const validated = { ...store.$patValidatedFor.get() };
22
+ delete validated[guildId];
23
+ store.$patValidatedFor.set(validated);
24
+ const errs = { ...store.$patErrors.get() };
25
+ delete errs[guildId];
26
+ store.$patErrors.set(errs);
27
+ }
28
+
29
+ async function validateGithubPat(
30
+ pat: string,
31
+ ): Promise<Result<PatValidation>> {
32
+ try {
33
+ const resp = await fetch('https://api.github.com/user', {
34
+ headers: {
35
+ Authorization: `Bearer ${pat}`,
36
+ Accept: 'application/vnd.github+json',
37
+ 'X-GitHub-Api-Version': '2022-11-28',
38
+ },
39
+ });
40
+ if (resp.status === 401) {
41
+ return { ok: false, error: 'GitHub rejected the token (401).' };
42
+ }
43
+ if (!resp.ok) {
44
+ return {
45
+ ok: false,
46
+ error: `GitHub /user returned ${resp.status}.`,
47
+ };
48
+ }
49
+ const user = (await resp.json()) as { login?: string };
50
+ if (!user.login) {
51
+ return { ok: false, error: 'GitHub response missing login.' };
52
+ }
53
+ const scopesHeader = resp.headers.get('x-oauth-scopes') ?? '';
54
+ const tokenTypeHeader =
55
+ resp.headers.get('x-github-authentication-token-type') ??
56
+ resp.headers.get('x-github-token-type') ??
57
+ 'unknown';
58
+ const scopes = scopesHeader
59
+ .split(',')
60
+ .map((s) => s.trim())
61
+ .filter(Boolean);
62
+ return {
63
+ ok: true,
64
+ login: user.login,
65
+ scopes,
66
+ tokenType: tokenTypeHeader,
67
+ };
68
+ } catch (e) {
69
+ return {
70
+ ok: false,
71
+ error: e instanceof Error ? e.message : 'Network error',
72
+ };
73
+ }
74
+ }
75
+
76
+ async function validatePatForGuild(guildId: string): Promise<Result> {
77
+ const pat = store.$patDrafts.get()[guildId] ?? '';
78
+ if (!pat) return { ok: false, error: 'No PAT entered' };
79
+ store.$patValidatingFor.set({
80
+ ...store.$patValidatingFor.get(),
81
+ [guildId]: true,
82
+ });
83
+ store.$patErrors.set({ ...store.$patErrors.get(), [guildId]: null });
84
+ const validatedClear = { ...store.$patValidatedFor.get() };
85
+ delete validatedClear[guildId];
86
+ store.$patValidatedFor.set(validatedClear);
87
+ const r = await validateGithubPat(pat);
88
+ const validatingDone = { ...store.$patValidatingFor.get() };
89
+ delete validatingDone[guildId];
90
+ store.$patValidatingFor.set(validatingDone);
91
+ if (!r.ok) {
92
+ store.$patErrors.set({
93
+ ...store.$patErrors.get(),
94
+ [guildId]: r.error,
95
+ });
96
+ return r;
97
+ }
98
+ store.$patValidatedFor.set({
99
+ ...store.$patValidatedFor.get(),
100
+ [guildId]: {
101
+ login: r.login,
102
+ scopes: r.scopes,
103
+ tokenType: r.tokenType,
104
+ },
105
+ });
106
+ return { ok: true };
107
+ }
108
+
109
+ async function savePatForGuild(
110
+ guildId: string,
111
+ tokenName: string,
112
+ ): Promise<Result<{ tokenId: string }>> {
113
+ const pat = store.$patDrafts.get()[guildId] ?? '';
114
+ if (!pat) return { ok: false, error: 'No PAT entered' };
115
+ const validated = store.$patValidatedFor.get()[guildId] ?? null;
116
+ store.$patSavingFor.set({
117
+ ...store.$patSavingFor.get(),
118
+ [guildId]: true,
119
+ });
120
+ store.$patErrors.set({ ...store.$patErrors.get(), [guildId]: null });
121
+ const r = await api.addToken({
122
+ tokenName,
123
+ service: 'github',
124
+ tokenValue: pat,
125
+ description: validated
126
+ ? `GitHub PAT for ${validated.login}`
127
+ : 'GitHub PAT',
128
+ });
129
+ const savingDone = { ...store.$patSavingFor.get() };
130
+ delete savingDone[guildId];
131
+ store.$patSavingFor.set(savingDone);
132
+ if (r.ok) {
133
+ clearPatState(guildId);
134
+ } else {
135
+ store.$patErrors.set({
136
+ ...store.$patErrors.get(),
137
+ [guildId]: r.error,
138
+ });
139
+ }
140
+ return r;
141
+ }
142
+
143
+ return {
144
+ setPatDraft,
145
+ clearPatState,
146
+ validateGithubPat,
147
+ validatePatForGuild,
148
+ savePatForGuild,
149
+ };
150
+ }