ikanban-web 0.1.1

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 (175) hide show
  1. package/README.md +65 -0
  2. package/bin/cli.js +1055 -0
  3. package/dist/apple-touch-icon-120x120.png +0 -0
  4. package/dist/apple-touch-icon-152x152.png +0 -0
  5. package/dist/apple-touch-icon-167x167.png +0 -0
  6. package/dist/apple-touch-icon-180x180.png +0 -0
  7. package/dist/apple-touch-icon.png +0 -0
  8. package/dist/apple-touch-icon.svg +16 -0
  9. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  10. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  11. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  15. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  21. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  24. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  26. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  27. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  28. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  29. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  31. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  32. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  33. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  34. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  35. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  36. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  37. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  38. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  39. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  40. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  41. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  51. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  52. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  53. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  54. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  55. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  56. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  57. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  58. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  59. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  60. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  61. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  62. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  63. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  64. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  65. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  66. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  67. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  68. package/dist/assets/ToolOutputDialog-BgFj3J4u.js +5 -0
  69. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  70. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  71. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  72. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  73. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  74. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  75. package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  76. package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  77. package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  78. package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  79. package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  80. package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  81. package/dist/assets/index-BQJPAP5f.css +1 -0
  82. package/dist/assets/index-MBuF9PXk.js +2 -0
  83. package/dist/assets/main-C1GggQif.css +1 -0
  84. package/dist/assets/main-Dyy8WvPJ.js +311 -0
  85. package/dist/assets/vendor--DEobTDja.css +1 -0
  86. package/dist/assets/vendor-.bun-KwPUu5oc.js +4594 -0
  87. package/dist/assets/wasm-CG6Dc4jp.js +1 -0
  88. package/dist/assets/worker-suRQxAtO.js +155 -0
  89. package/dist/favicon-16.png +0 -0
  90. package/dist/favicon-32.png +0 -0
  91. package/dist/favicon.png +0 -0
  92. package/dist/favicon.svg +15 -0
  93. package/dist/index.html +325 -0
  94. package/dist/logo-dark-192x192.png +0 -0
  95. package/dist/logo-dark-512x512.svg +16 -0
  96. package/dist/logo-light-192x192.png +0 -0
  97. package/dist/logo-light-512x512.svg +16 -0
  98. package/dist/pwa-192.png +0 -0
  99. package/dist/pwa-512.png +0 -0
  100. package/dist/pwa-maskable-192.png +0 -0
  101. package/dist/pwa-maskable-512.png +0 -0
  102. package/dist/site.webmanifest +22 -0
  103. package/dist/sw.js +1 -0
  104. package/package.json +102 -0
  105. package/public/apple-touch-icon-120x120.png +0 -0
  106. package/public/apple-touch-icon-152x152.png +0 -0
  107. package/public/apple-touch-icon-167x167.png +0 -0
  108. package/public/apple-touch-icon-180x180.png +0 -0
  109. package/public/apple-touch-icon.png +0 -0
  110. package/public/apple-touch-icon.svg +16 -0
  111. package/public/favicon-16.png +0 -0
  112. package/public/favicon-32.png +0 -0
  113. package/public/favicon.png +0 -0
  114. package/public/favicon.svg +15 -0
  115. package/public/logo-dark-192x192.png +0 -0
  116. package/public/logo-dark-512x512.svg +16 -0
  117. package/public/logo-light-192x192.png +0 -0
  118. package/public/logo-light-512x512.svg +16 -0
  119. package/public/pwa-192.png +0 -0
  120. package/public/pwa-512.png +0 -0
  121. package/public/pwa-maskable-192.png +0 -0
  122. package/public/pwa-maskable-512.png +0 -0
  123. package/public/site.webmanifest +22 -0
  124. package/server/TERMINAL_INPUT_WS_PROTOCOL.md +44 -0
  125. package/server/index.d.ts +28 -0
  126. package/server/index.js +11181 -0
  127. package/server/lib/cloudflare-tunnel.js +196 -0
  128. package/server/lib/git-credentials.js +74 -0
  129. package/server/lib/git-identity-storage.js +110 -0
  130. package/server/lib/git-service.js +2690 -0
  131. package/server/lib/github-auth.js +307 -0
  132. package/server/lib/github-device-flow.js +50 -0
  133. package/server/lib/github-octokit.js +10 -0
  134. package/server/lib/github-repo.js +55 -0
  135. package/server/lib/notification-message.js +49 -0
  136. package/server/lib/notification-message.test.js +59 -0
  137. package/server/lib/opencode-auth.js +81 -0
  138. package/server/lib/opencode-config.js +1840 -0
  139. package/server/lib/opencode-config.js.d.ts +12 -0
  140. package/server/lib/package-manager.js +255 -0
  141. package/server/lib/quota/DOCUMENTATION.md +52 -0
  142. package/server/lib/quota/index.js +21 -0
  143. package/server/lib/quota/providers/claude.js +107 -0
  144. package/server/lib/quota/providers/codex.js +111 -0
  145. package/server/lib/quota/providers/copilot.js +165 -0
  146. package/server/lib/quota/providers/google/api.js +92 -0
  147. package/server/lib/quota/providers/google/auth.js +108 -0
  148. package/server/lib/quota/providers/google/index.js +124 -0
  149. package/server/lib/quota/providers/google/transforms.js +109 -0
  150. package/server/lib/quota/providers/index.js +128 -0
  151. package/server/lib/quota/providers/interface.js +55 -0
  152. package/server/lib/quota/providers/kimi.js +108 -0
  153. package/server/lib/quota/providers/nanogpt.js +124 -0
  154. package/server/lib/quota/providers/openai.js +91 -0
  155. package/server/lib/quota/providers/openrouter.js +92 -0
  156. package/server/lib/quota/providers/zai.js +91 -0
  157. package/server/lib/quota/utils/auth.js +46 -0
  158. package/server/lib/quota/utils/formatters.js +76 -0
  159. package/server/lib/quota/utils/index.js +10 -0
  160. package/server/lib/quota/utils/transformers.js +55 -0
  161. package/server/lib/skills-catalog/cache.js +29 -0
  162. package/server/lib/skills-catalog/clawdhub/api.js +158 -0
  163. package/server/lib/skills-catalog/clawdhub/index.js +30 -0
  164. package/server/lib/skills-catalog/clawdhub/install.js +223 -0
  165. package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
  166. package/server/lib/skills-catalog/curated-sources.js +21 -0
  167. package/server/lib/skills-catalog/git.js +76 -0
  168. package/server/lib/skills-catalog/install.js +280 -0
  169. package/server/lib/skills-catalog/scan.js +221 -0
  170. package/server/lib/skills-catalog/source.js +85 -0
  171. package/server/lib/summarization-service.js +171 -0
  172. package/server/lib/terminal-input-ws-protocol.js +66 -0
  173. package/server/lib/terminal-input-ws-protocol.test.js +138 -0
  174. package/server/lib/tts-service.js +162 -0
  175. package/server/lib/ui-auth.js +518 -0
@@ -0,0 +1,307 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const IKANBAN_DATA_DIR = process.env.IKANBAN_DATA_DIR
6
+ ? path.resolve(process.env.IKANBAN_DATA_DIR)
7
+ : path.join(os.homedir(), '.config', 'ikanban');
8
+
9
+ const STORAGE_DIR = IKANBAN_DATA_DIR;
10
+ const STORAGE_FILE = path.join(STORAGE_DIR, 'github-auth.json');
11
+ const SETTINGS_FILE = path.join(IKANBAN_DATA_DIR, 'settings.json');
12
+
13
+ const DEFAULT_GITHUB_CLIENT_ID = 'Ov23liNd8TxDcMXtAHHM';
14
+ const DEFAULT_GITHUB_SCOPES = 'repo read:org workflow read:user user:email';
15
+
16
+ function ensureStorageDir() {
17
+ if (!fs.existsSync(STORAGE_DIR)) {
18
+ fs.mkdirSync(STORAGE_DIR, { recursive: true });
19
+ }
20
+ }
21
+
22
+ function readJsonFile() {
23
+ ensureStorageDir();
24
+ if (!fs.existsSync(STORAGE_FILE)) {
25
+ return null;
26
+ }
27
+ try {
28
+ const raw = fs.readFileSync(STORAGE_FILE, 'utf8');
29
+ const trimmed = raw.trim();
30
+ if (!trimmed) {
31
+ return null;
32
+ }
33
+ const parsed = JSON.parse(trimmed);
34
+ if (!parsed || typeof parsed !== 'object') {
35
+ return null;
36
+ }
37
+ return parsed;
38
+ } catch (error) {
39
+ console.error('Failed to read GitHub auth file:', error);
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function writeJsonFile(payload) {
45
+ ensureStorageDir();
46
+
47
+ // Atomic write so multiple IKanban instances can safely share the same file.
48
+ const tmpFile = `${STORAGE_FILE}.${process.pid}.${Date.now()}.tmp`;
49
+ fs.writeFileSync(tmpFile, JSON.stringify(payload, null, 2), 'utf8');
50
+ try {
51
+ fs.chmodSync(tmpFile, 0o600);
52
+ } catch {
53
+ // best-effort
54
+ }
55
+
56
+ fs.renameSync(tmpFile, STORAGE_FILE);
57
+ try {
58
+ fs.chmodSync(STORAGE_FILE, 0o600);
59
+ } catch {
60
+ // best-effort
61
+ }
62
+ }
63
+
64
+ function resolveAccountId({ user, accessToken, accountId }) {
65
+ if (typeof accountId === 'string' && accountId.trim()) {
66
+ return accountId.trim();
67
+ }
68
+ if (user && typeof user.login === 'string' && user.login.trim()) {
69
+ return user.login.trim();
70
+ }
71
+ if (user && typeof user.id === 'number') {
72
+ return String(user.id);
73
+ }
74
+ if (typeof accessToken === 'string' && accessToken.trim()) {
75
+ return `token:${accessToken.slice(0, 8)}`;
76
+ }
77
+ return '';
78
+ }
79
+
80
+ function normalizeAuthEntry(entry) {
81
+ if (!entry || typeof entry !== 'object') return null;
82
+ const accessToken = typeof entry.accessToken === 'string' ? entry.accessToken : '';
83
+ if (!accessToken) return null;
84
+ const user = entry.user && typeof entry.user === 'object'
85
+ ? {
86
+ login: typeof entry.user.login === 'string' ? entry.user.login : null,
87
+ avatarUrl: typeof entry.user.avatarUrl === 'string' ? entry.user.avatarUrl : null,
88
+ id: typeof entry.user.id === 'number' ? entry.user.id : null,
89
+ name: typeof entry.user.name === 'string' ? entry.user.name : null,
90
+ email: typeof entry.user.email === 'string' ? entry.user.email : null,
91
+ }
92
+ : null;
93
+
94
+ const accountId = resolveAccountId({
95
+ user,
96
+ accessToken,
97
+ accountId: typeof entry.accountId === 'string' ? entry.accountId : '',
98
+ });
99
+
100
+ return {
101
+ accessToken,
102
+ scope: typeof entry.scope === 'string' ? entry.scope : '',
103
+ tokenType: typeof entry.tokenType === 'string' ? entry.tokenType : 'bearer',
104
+ createdAt: typeof entry.createdAt === 'number' ? entry.createdAt : null,
105
+ user,
106
+ current: Boolean(entry.current),
107
+ accountId,
108
+ };
109
+ }
110
+
111
+ function normalizeAuthList(raw) {
112
+ const list = (Array.isArray(raw) ? raw : [raw])
113
+ .map((entry) => normalizeAuthEntry(entry))
114
+ .filter(Boolean);
115
+
116
+ if (!list.length) {
117
+ return { list: [], changed: false };
118
+ }
119
+
120
+ let changed = false;
121
+ let currentFound = false;
122
+ list.forEach((entry) => {
123
+ if (entry.current && !currentFound) {
124
+ currentFound = true;
125
+ } else if (entry.current && currentFound) {
126
+ entry.current = false;
127
+ changed = true;
128
+ }
129
+ });
130
+
131
+ if (!currentFound && list[0]) {
132
+ list[0].current = true;
133
+ changed = true;
134
+ }
135
+
136
+ list.forEach((entry) => {
137
+ if (!entry.accountId) {
138
+ entry.accountId = resolveAccountId(entry);
139
+ changed = true;
140
+ }
141
+ });
142
+
143
+ return { list, changed };
144
+ }
145
+
146
+ function readAuthList() {
147
+ const data = readJsonFile();
148
+ if (!data) {
149
+ return [];
150
+ }
151
+ const { list, changed } = normalizeAuthList(data);
152
+ if (changed) {
153
+ writeJsonFile(list);
154
+ }
155
+ return list;
156
+ }
157
+
158
+ function writeAuthList(list) {
159
+ writeJsonFile(list);
160
+ }
161
+
162
+ export function getGitHubAuth() {
163
+ const list = readAuthList();
164
+ if (!list.length) {
165
+ return null;
166
+ }
167
+ const current = list.find((entry) => entry.current) || list[0];
168
+ if (!current?.accessToken) {
169
+ return null;
170
+ }
171
+ return current;
172
+ }
173
+
174
+ export function getGitHubAuthAccounts() {
175
+ const list = readAuthList();
176
+ return list
177
+ .filter((entry) => entry?.user && entry.accountId)
178
+ .map((entry) => ({
179
+ id: entry.accountId,
180
+ user: entry.user,
181
+ scope: entry.scope || '',
182
+ current: Boolean(entry.current),
183
+ }));
184
+ }
185
+
186
+ export function setGitHubAuth({ accessToken, scope, tokenType, user, accountId }) {
187
+ if (!accessToken || typeof accessToken !== 'string') {
188
+ throw new Error('accessToken is required');
189
+ }
190
+ const normalizedUser = user && typeof user === 'object'
191
+ ? {
192
+ login: typeof user.login === 'string' ? user.login : undefined,
193
+ avatarUrl: typeof user.avatarUrl === 'string' ? user.avatarUrl : undefined,
194
+ id: typeof user.id === 'number' ? user.id : undefined,
195
+ name: typeof user.name === 'string' ? user.name : undefined,
196
+ email: typeof user.email === 'string' ? user.email : undefined,
197
+ }
198
+ : undefined;
199
+
200
+ const resolvedAccountId = resolveAccountId({
201
+ user: normalizedUser,
202
+ accessToken,
203
+ accountId,
204
+ });
205
+
206
+ const list = readAuthList();
207
+ const existingIndex = list.findIndex((entry) => entry.accountId === resolvedAccountId);
208
+ const nextEntry = {
209
+ accessToken,
210
+ scope: typeof scope === 'string' ? scope : '',
211
+ tokenType: typeof tokenType === 'string' ? tokenType : 'bearer',
212
+ createdAt: Date.now(),
213
+ user: normalizedUser || null,
214
+ current: true,
215
+ accountId: resolvedAccountId,
216
+ };
217
+
218
+ if (existingIndex >= 0) {
219
+ list[existingIndex] = nextEntry;
220
+ } else {
221
+ list.push(nextEntry);
222
+ }
223
+
224
+ list.forEach((entry, index) => {
225
+ entry.current = index === (existingIndex >= 0 ? existingIndex : list.length - 1);
226
+ });
227
+ writeAuthList(list);
228
+ return nextEntry;
229
+ }
230
+
231
+ export function activateGitHubAuth(accountId) {
232
+ if (typeof accountId !== 'string' || !accountId.trim()) {
233
+ return false;
234
+ }
235
+ const list = readAuthList();
236
+ const index = list.findIndex((entry) => entry.accountId === accountId.trim());
237
+ if (index === -1) {
238
+ return false;
239
+ }
240
+ list.forEach((entry, idx) => {
241
+ entry.current = idx === index;
242
+ });
243
+ writeAuthList(list);
244
+ return true;
245
+ }
246
+
247
+ export function clearGitHubAuth() {
248
+ try {
249
+ const list = readAuthList();
250
+ if (!list.length) {
251
+ return true;
252
+ }
253
+ const remaining = list.filter((entry) => !entry.current);
254
+ if (!remaining.length) {
255
+ if (fs.existsSync(STORAGE_FILE)) {
256
+ fs.unlinkSync(STORAGE_FILE);
257
+ }
258
+ return true;
259
+ }
260
+ remaining.forEach((entry, index) => {
261
+ entry.current = index === 0;
262
+ });
263
+ writeAuthList(remaining);
264
+ return true;
265
+ } catch (error) {
266
+ console.error('Failed to clear GitHub auth file:', error);
267
+ return false;
268
+ }
269
+ }
270
+
271
+ export function getGitHubClientId() {
272
+ const raw = process.env.IKANBAN_GITHUB_CLIENT_ID;
273
+ const clientId = typeof raw === 'string' ? raw.trim() : '';
274
+ if (clientId) return clientId;
275
+
276
+ try {
277
+ if (fs.existsSync(SETTINGS_FILE)) {
278
+ const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
279
+ const stored = typeof parsed?.githubClientId === 'string' ? parsed.githubClientId.trim() : '';
280
+ if (stored) return stored;
281
+ }
282
+ } catch {
283
+ // ignore
284
+ }
285
+
286
+ return DEFAULT_GITHUB_CLIENT_ID;
287
+ }
288
+
289
+ export function getGitHubScopes() {
290
+ const raw = process.env.IKANBAN_GITHUB_SCOPES;
291
+ const fromEnv = typeof raw === 'string' ? raw.trim() : '';
292
+ if (fromEnv) return fromEnv;
293
+
294
+ try {
295
+ if (fs.existsSync(SETTINGS_FILE)) {
296
+ const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
297
+ const stored = typeof parsed?.githubScopes === 'string' ? parsed.githubScopes.trim() : '';
298
+ if (stored) return stored;
299
+ }
300
+ } catch {
301
+ // ignore
302
+ }
303
+
304
+ return DEFAULT_GITHUB_SCOPES;
305
+ }
306
+
307
+ export const GITHUB_AUTH_FILE = STORAGE_FILE;
@@ -0,0 +1,50 @@
1
+ const DEVICE_CODE_URL = 'https://github.com/login/device/code';
2
+ const ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token';
3
+ const DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
4
+
5
+ const encodeForm = (params) => {
6
+ const body = new URLSearchParams();
7
+ for (const [key, value] of Object.entries(params)) {
8
+ if (value == null) continue;
9
+ body.set(key, String(value));
10
+ }
11
+ return body.toString();
12
+ };
13
+
14
+ async function postForm(url, params) {
15
+ const response = await fetch(url, {
16
+ method: 'POST',
17
+ headers: {
18
+ 'Content-Type': 'application/x-www-form-urlencoded',
19
+ Accept: 'application/json',
20
+ },
21
+ body: encodeForm(params),
22
+ });
23
+
24
+ const payload = await response.json().catch(() => null);
25
+ if (!response.ok) {
26
+ const message = payload?.error_description || payload?.error || response.statusText;
27
+ const error = new Error(message || 'GitHub request failed');
28
+ error.status = response.status;
29
+ error.payload = payload;
30
+ throw error;
31
+ }
32
+ return payload;
33
+ }
34
+
35
+ export async function startDeviceFlow({ clientId, scope }) {
36
+ return postForm(DEVICE_CODE_URL, {
37
+ client_id: clientId,
38
+ scope,
39
+ });
40
+ }
41
+
42
+ export async function exchangeDeviceCode({ clientId, deviceCode }) {
43
+ // GitHub returns 200 with {error: 'authorization_pending'|...} for non-success states.
44
+ const payload = await postForm(ACCESS_TOKEN_URL, {
45
+ client_id: clientId,
46
+ device_code: deviceCode,
47
+ grant_type: DEVICE_GRANT_TYPE,
48
+ });
49
+ return payload;
50
+ }
@@ -0,0 +1,10 @@
1
+ import { Octokit } from '@octokit/rest';
2
+ import { getGitHubAuth } from './github-auth.js';
3
+
4
+ export function getOctokitOrNull() {
5
+ const auth = getGitHubAuth();
6
+ if (!auth?.accessToken) {
7
+ return null;
8
+ }
9
+ return new Octokit({ auth: auth.accessToken });
10
+ }
@@ -0,0 +1,55 @@
1
+ import { getRemoteUrl } from './git-service.js';
2
+
3
+ export const parseGitHubRemoteUrl = (raw) => {
4
+ if (typeof raw !== 'string') {
5
+ return null;
6
+ }
7
+ const value = raw.trim();
8
+ if (!value) {
9
+ return null;
10
+ }
11
+
12
+ // git@github.com:OWNER/REPO.git
13
+ if (value.startsWith('git@github.com:')) {
14
+ const rest = value.slice('git@github.com:'.length);
15
+ const cleaned = rest.endsWith('.git') ? rest.slice(0, -4) : rest;
16
+ const [owner, repo] = cleaned.split('/');
17
+ if (!owner || !repo) return null;
18
+ return { owner, repo, url: `https://github.com/${owner}/${repo}` };
19
+ }
20
+
21
+ // ssh://git@github.com/OWNER/REPO.git
22
+ if (value.startsWith('ssh://git@github.com/')) {
23
+ const rest = value.slice('ssh://git@github.com/'.length);
24
+ const cleaned = rest.endsWith('.git') ? rest.slice(0, -4) : rest;
25
+ const [owner, repo] = cleaned.split('/');
26
+ if (!owner || !repo) return null;
27
+ return { owner, repo, url: `https://github.com/${owner}/${repo}` };
28
+ }
29
+
30
+ // https://github.com/OWNER/REPO(.git)
31
+ try {
32
+ const url = new URL(value);
33
+ if (url.hostname !== 'github.com') {
34
+ return null;
35
+ }
36
+ const path = url.pathname.replace(/^\/+/, '').replace(/\/+$/, '');
37
+ const cleaned = path.endsWith('.git') ? path.slice(0, -4) : path;
38
+ const [owner, repo] = cleaned.split('/');
39
+ if (!owner || !repo) return null;
40
+ return { owner, repo, url: `https://github.com/${owner}/${repo}` };
41
+ } catch {
42
+ return null;
43
+ }
44
+ };
45
+
46
+ export async function resolveGitHubRepoFromDirectory(directory, remoteName = 'origin') {
47
+ const remoteUrl = await getRemoteUrl(directory, remoteName).catch(() => null);
48
+ if (!remoteUrl) {
49
+ return { repo: null, remoteUrl: null };
50
+ }
51
+ return {
52
+ repo: parseGitHubRemoteUrl(remoteUrl),
53
+ remoteUrl,
54
+ };
55
+ }
@@ -0,0 +1,49 @@
1
+ const DEFAULT_NOTIFICATION_MESSAGE_MAX_LENGTH = 250;
2
+ const DEFAULT_NOTIFICATION_SUMMARY_THRESHOLD = 200;
3
+ const DEFAULT_NOTIFICATION_SUMMARY_LENGTH = 100;
4
+
5
+ const resolvePositiveNumber = (value, fallback) => {
6
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
7
+ return fallback;
8
+ }
9
+ return value;
10
+ };
11
+
12
+ export const truncateNotificationText = (text, maxLength = DEFAULT_NOTIFICATION_MESSAGE_MAX_LENGTH) => {
13
+ if (typeof text !== 'string') {
14
+ return '';
15
+ }
16
+
17
+ const safeMaxLength = resolvePositiveNumber(maxLength, DEFAULT_NOTIFICATION_MESSAGE_MAX_LENGTH);
18
+ if (text.length <= safeMaxLength) {
19
+ return text;
20
+ }
21
+
22
+ return `${text.slice(0, safeMaxLength)}...`;
23
+ };
24
+
25
+ export const prepareNotificationLastMessage = async ({ message, settings, summarize }) => {
26
+ const originalMessage = typeof message === 'string' ? message : '';
27
+ if (!originalMessage) {
28
+ return '';
29
+ }
30
+
31
+ const shouldSummarize = settings?.summarizeLastMessage === true && typeof summarize === 'function';
32
+ const summaryThreshold = resolvePositiveNumber(settings?.summaryThreshold, DEFAULT_NOTIFICATION_SUMMARY_THRESHOLD);
33
+ const summaryLength = resolvePositiveNumber(settings?.summaryLength, DEFAULT_NOTIFICATION_SUMMARY_LENGTH);
34
+ const maxLastMessageLength = resolvePositiveNumber(settings?.maxLastMessageLength, DEFAULT_NOTIFICATION_MESSAGE_MAX_LENGTH);
35
+
36
+ let messageForNotification = originalMessage;
37
+ if (shouldSummarize && originalMessage.length > summaryThreshold) {
38
+ try {
39
+ const summary = await summarize(originalMessage, summaryLength);
40
+ if (typeof summary === 'string' && summary.trim().length > 0) {
41
+ messageForNotification = summary;
42
+ }
43
+ } catch {
44
+ messageForNotification = originalMessage;
45
+ }
46
+ }
47
+
48
+ return truncateNotificationText(messageForNotification, maxLastMessageLength);
49
+ };
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { prepareNotificationLastMessage, truncateNotificationText } from './notification-message.js';
4
+
5
+ describe('notification message helpers', () => {
6
+ it('truncates oversized notification text', () => {
7
+ expect(truncateNotificationText('abcdef', 3)).toBe('abc...');
8
+ });
9
+
10
+ it('falls back to original message when summarization fails', async () => {
11
+ const message = '0123456789';
12
+ const summarize = async () => {
13
+ throw new Error('summarization failed');
14
+ };
15
+
16
+ const result = await prepareNotificationLastMessage({
17
+ message,
18
+ summarize,
19
+ settings: {
20
+ summarizeLastMessage: true,
21
+ summaryThreshold: 5,
22
+ summaryLength: 3,
23
+ maxLastMessageLength: 4,
24
+ },
25
+ });
26
+
27
+ expect(result).toBe('0123...');
28
+ });
29
+
30
+ it('falls back to original message when summary is empty', async () => {
31
+ const result = await prepareNotificationLastMessage({
32
+ message: '0123456789',
33
+ summarize: async () => ' ',
34
+ settings: {
35
+ summarizeLastMessage: true,
36
+ summaryThreshold: 5,
37
+ summaryLength: 3,
38
+ maxLastMessageLength: 4,
39
+ },
40
+ });
41
+
42
+ expect(result).toBe('0123...');
43
+ });
44
+
45
+ it('uses summary when summarization succeeds', async () => {
46
+ const result = await prepareNotificationLastMessage({
47
+ message: '0123456789',
48
+ summarize: async () => 'short summary',
49
+ settings: {
50
+ summarizeLastMessage: true,
51
+ summaryThreshold: 5,
52
+ summaryLength: 3,
53
+ maxLastMessageLength: 100,
54
+ },
55
+ });
56
+
57
+ expect(result).toBe('short summary');
58
+ });
59
+ });
@@ -0,0 +1,81 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const OPENCODE_DATA_DIR = path.join(os.homedir(), '.local', 'share', 'opencode');
6
+ const AUTH_FILE = path.join(OPENCODE_DATA_DIR, 'auth.json');
7
+
8
+ function readAuthFile() {
9
+ if (!fs.existsSync(AUTH_FILE)) {
10
+ return {};
11
+ }
12
+ try {
13
+ const content = fs.readFileSync(AUTH_FILE, 'utf8');
14
+ const trimmed = content.trim();
15
+ if (!trimmed) {
16
+ return {};
17
+ }
18
+ return JSON.parse(trimmed);
19
+ } catch (error) {
20
+ console.error('Failed to read auth file:', error);
21
+ throw new Error('Failed to read OpenCode auth configuration');
22
+ }
23
+ }
24
+
25
+ function writeAuthFile(auth) {
26
+ try {
27
+ if (!fs.existsSync(OPENCODE_DATA_DIR)) {
28
+ fs.mkdirSync(OPENCODE_DATA_DIR, { recursive: true });
29
+ }
30
+
31
+ if (fs.existsSync(AUTH_FILE)) {
32
+ const backupFile = `${AUTH_FILE}.ikanban.backup`;
33
+ fs.copyFileSync(AUTH_FILE, backupFile);
34
+ console.log(`Created auth backup: ${backupFile}`);
35
+ }
36
+
37
+ fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), 'utf8');
38
+ console.log('Successfully wrote auth file');
39
+ } catch (error) {
40
+ console.error('Failed to write auth file:', error);
41
+ throw new Error('Failed to write OpenCode auth configuration');
42
+ }
43
+ }
44
+
45
+ function removeProviderAuth(providerId) {
46
+ if (!providerId || typeof providerId !== 'string') {
47
+ throw new Error('Provider ID is required');
48
+ }
49
+
50
+ const auth = readAuthFile();
51
+
52
+ if (!auth[providerId]) {
53
+ console.log(`Provider ${providerId} not found in auth file, nothing to remove`);
54
+ return false;
55
+ }
56
+
57
+ delete auth[providerId];
58
+ writeAuthFile(auth);
59
+ console.log(`Removed provider auth: ${providerId}`);
60
+ return true;
61
+ }
62
+
63
+ function getProviderAuth(providerId) {
64
+ const auth = readAuthFile();
65
+ return auth[providerId] || null;
66
+ }
67
+
68
+ function listProviderAuths() {
69
+ const auth = readAuthFile();
70
+ return Object.keys(auth);
71
+ }
72
+
73
+ export {
74
+ readAuthFile,
75
+ writeAuthFile,
76
+ removeProviderAuth,
77
+ getProviderAuth,
78
+ listProviderAuths,
79
+ AUTH_FILE,
80
+ OPENCODE_DATA_DIR
81
+ };