@kkelly-offical/kkcode 0.1.6 → 0.2.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 (163) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +452 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +19 -2
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +90 -0
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/config/defaults.mjs +280 -260
  29. package/src/config/import-config.mjs +1 -1
  30. package/src/config/load-config.mjs +61 -4
  31. package/src/config/schema.mjs +591 -574
  32. package/src/context.mjs +4 -1
  33. package/src/core/constants.mjs +97 -91
  34. package/src/core/types.mjs +1 -1
  35. package/src/github/api.mjs +78 -78
  36. package/src/github/auth.mjs +294 -286
  37. package/src/github/flow.mjs +298 -298
  38. package/src/github/workspace.mjs +225 -212
  39. package/src/index.mjs +84 -82
  40. package/src/knowledge/frontend-aesthetics.txt +38 -38
  41. package/src/mcp/client-http.mjs +139 -141
  42. package/src/mcp/client-sse.mjs +297 -288
  43. package/src/mcp/client-stdio.mjs +534 -533
  44. package/src/mcp/constants.mjs +2 -2
  45. package/src/mcp/registry.mjs +498 -479
  46. package/src/mcp/stdio-framing.mjs +135 -133
  47. package/src/mcp/tool-result.mjs +24 -24
  48. package/src/observability/edit-diagnostics.mjs +449 -0
  49. package/src/observability/index.mjs +42 -42
  50. package/src/observability/metrics.mjs +165 -137
  51. package/src/observability/tracer.mjs +137 -137
  52. package/src/onboarding.mjs +209 -0
  53. package/src/orchestration/background-manager.mjs +567 -372
  54. package/src/orchestration/background-worker.mjs +419 -305
  55. package/src/orchestration/interruption-reason.mjs +21 -0
  56. package/src/orchestration/longagent-manager.mjs +197 -171
  57. package/src/orchestration/stage-scheduler.mjs +733 -728
  58. package/src/orchestration/subagent-router.mjs +7 -1
  59. package/src/orchestration/task-scheduler.mjs +219 -7
  60. package/src/permission/engine.mjs +1 -1
  61. package/src/permission/exec-policy.mjs +370 -370
  62. package/src/permission/file-edit-policy.mjs +108 -0
  63. package/src/permission/prompt.mjs +1 -1
  64. package/src/permission/rules.mjs +116 -7
  65. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  66. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  67. package/src/plugin/hook-bus.mjs +19 -5
  68. package/src/plugin/manifest-loader.mjs +222 -0
  69. package/src/provider/anthropic.mjs +396 -390
  70. package/src/provider/ollama.mjs +7 -1
  71. package/src/provider/openai.mjs +382 -340
  72. package/src/provider/retry-policy.mjs +74 -68
  73. package/src/provider/router.mjs +242 -241
  74. package/src/provider/sse.mjs +104 -104
  75. package/src/provider/wizard.mjs +556 -0
  76. package/src/repl/capability-facade.mjs +30 -0
  77. package/src/repl/command-surface.mjs +23 -0
  78. package/src/repl/controller-entry.mjs +40 -0
  79. package/src/repl/core-shell.mjs +208 -0
  80. package/src/repl/dialog-router.mjs +87 -0
  81. package/src/repl/input-engine.mjs +76 -0
  82. package/src/repl/keymap.mjs +7 -0
  83. package/src/repl/operator-surface.mjs +15 -0
  84. package/src/repl/permission-flow.mjs +49 -0
  85. package/src/repl/runtime-facade.mjs +36 -0
  86. package/src/repl/slash-router.mjs +62 -0
  87. package/src/repl/state-store.mjs +29 -0
  88. package/src/repl/turn-controller.mjs +58 -0
  89. package/src/repl/verification.mjs +23 -0
  90. package/src/repl.mjs +3368 -2929
  91. package/src/rules/load-rules.mjs +3 -3
  92. package/src/runtime.mjs +1 -1
  93. package/src/session/agent-transaction.mjs +86 -0
  94. package/src/session/checkpoint.mjs +302 -302
  95. package/src/session/compaction.mjs +36 -14
  96. package/src/session/engine.mjs +417 -227
  97. package/src/session/longagent-4stage.mjs +467 -460
  98. package/src/session/longagent-hybrid.mjs +1344 -1081
  99. package/src/session/longagent-plan.mjs +376 -365
  100. package/src/session/longagent-project-memory.mjs +53 -53
  101. package/src/session/longagent-scaffold.mjs +291 -291
  102. package/src/session/longagent-task-bus.mjs +138 -54
  103. package/src/session/longagent-utils.mjs +828 -472
  104. package/src/session/longagent.mjs +911 -884
  105. package/src/session/loop.mjs +1005 -905
  106. package/src/session/prompt/agent.txt +25 -0
  107. package/src/session/prompt/anthropic.txt +150 -150
  108. package/src/session/prompt/beast.txt +1 -1
  109. package/src/session/prompt/plan.txt +28 -6
  110. package/src/session/prompt/qwen.txt +46 -46
  111. package/src/session/recovery.mjs +21 -0
  112. package/src/session/rollback.mjs +197 -0
  113. package/src/session/routing-observability.mjs +72 -0
  114. package/src/session/runtime-state.mjs +47 -0
  115. package/src/session/store.mjs +523 -510
  116. package/src/session/system-prompt.mjs +56 -8
  117. package/src/session/task-validator.mjs +267 -267
  118. package/src/session/usability-gates.mjs +2 -2
  119. package/src/skill/builtin/commit.mjs +64 -64
  120. package/src/skill/builtin/design.mjs +76 -76
  121. package/src/skill/generator.mjs +18 -2
  122. package/src/skill/registry.mjs +642 -390
  123. package/src/storage/audit-store.mjs +18 -11
  124. package/src/storage/event-log.mjs +7 -1
  125. package/src/storage/ghost-commit-store.mjs +243 -245
  126. package/src/storage/paths.mjs +13 -0
  127. package/src/theme/default-theme.mjs +1 -1
  128. package/src/theme/markdown.mjs +4 -0
  129. package/src/theme/schema.mjs +1 -1
  130. package/src/theme/status-bar.mjs +162 -158
  131. package/src/tool/audit-wrapper.mjs +18 -2
  132. package/src/tool/edit-transaction.mjs +23 -0
  133. package/src/tool/executor.mjs +26 -1
  134. package/src/tool/file-read-state.mjs +65 -0
  135. package/src/tool/git-auto.mjs +526 -526
  136. package/src/tool/git-full-auto.mjs +487 -478
  137. package/src/tool/mutation-guard.mjs +54 -0
  138. package/src/tool/prompt/edit.txt +3 -3
  139. package/src/tool/prompt/multiedit.txt +1 -0
  140. package/src/tool/prompt/notebookedit.txt +2 -1
  141. package/src/tool/prompt/patch.txt +25 -24
  142. package/src/tool/prompt/read.txt +3 -3
  143. package/src/tool/prompt/sysinfo.txt +29 -0
  144. package/src/tool/prompt/task.txt +66 -4
  145. package/src/tool/prompt/write.txt +2 -2
  146. package/src/tool/question-prompt.mjs +17 -4
  147. package/src/tool/registry.mjs +1701 -1343
  148. package/src/tool/task-tool.mjs +14 -6
  149. package/src/ui/activity-renderer.mjs +667 -664
  150. package/src/ui/repl-background-panel.mjs +7 -0
  151. package/src/ui/repl-capability-panel.mjs +9 -0
  152. package/src/ui/repl-dashboard.mjs +54 -4
  153. package/src/ui/repl-help.mjs +110 -0
  154. package/src/ui/repl-operator-panel.mjs +12 -0
  155. package/src/ui/repl-route-feedback.mjs +35 -0
  156. package/src/ui/repl-status-view.mjs +76 -0
  157. package/src/ui/repl-task-panel.mjs +5 -0
  158. package/src/ui/repl-transcript-panel.mjs +56 -0
  159. package/src/ui/repl-turn-summary.mjs +135 -0
  160. package/src/usage/pricing.mjs +122 -121
  161. package/src/usage/usage-meter.mjs +1 -0
  162. package/src/util/git.mjs +562 -519
  163. package/src/util/template.mjs +6 -1
@@ -1,286 +1,294 @@
1
- import { readFile, writeFile, unlink, mkdir } from "node:fs/promises"
2
- import { dirname } from "node:path"
3
- import { spawn } from "node:child_process"
4
- import { githubTokenPath } from "../storage/paths.mjs"
5
-
6
- const CLIENT_ID = "Ov23liCqhJ6cRaqyv3uA"
7
- const DEVICE_CODE_URL = "https://github.com/login/device/code"
8
- const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
9
- const SCOPE = "repo"
10
-
11
- export async function getStoredToken() {
12
- try {
13
- const raw = await readFile(githubTokenPath(), "utf8")
14
- const data = JSON.parse(raw)
15
- if (data.token && data.login) return data
16
- return null
17
- } catch {
18
- return null
19
- }
20
- }
21
-
22
- async function saveToken(data) {
23
- const filePath = githubTokenPath()
24
- await mkdir(dirname(filePath), { recursive: true })
25
- await writeFile(filePath, JSON.stringify(data, null, 2), "utf8")
26
- }
27
-
28
- export async function logout() {
29
- try {
30
- await unlink(githubTokenPath())
31
- return true
32
- } catch {
33
- return false
34
- }
35
- }
36
-
37
- async function validateToken(token) {
38
- try {
39
- const res = await fetch("https://api.github.com/user", {
40
- headers: {
41
- Authorization: `Bearer ${token}`,
42
- Accept: "application/vnd.github+json"
43
- }
44
- })
45
- if (res.ok) {
46
- const user = await res.json()
47
- return { valid: true, login: user.login }
48
- }
49
- return { valid: false, login: null }
50
- } catch {
51
- return { valid: false, login: null }
52
- }
53
- }
54
-
55
- async function requestDeviceCode() {
56
- const res = await fetch(DEVICE_CODE_URL, {
57
- method: "POST",
58
- headers: {
59
- Accept: "application/json",
60
- "Content-Type": "application/json"
61
- },
62
- body: JSON.stringify({ client_id: CLIENT_ID, scope: SCOPE })
63
- })
64
- if (!res.ok) {
65
- throw new Error(`GitHub device code request failed: ${res.status}`)
66
- }
67
- return res.json()
68
- }
69
-
70
- function sleep(ms) {
71
- return new Promise((resolve) => setTimeout(resolve, ms))
72
- }
73
-
74
- /**
75
- * 跨平台打开浏览器
76
- * @param {string} url - 要打开的 URL
77
- */
78
- function openBrowser(url) {
79
- const platform = process.platform
80
- let command
81
- let args
82
-
83
- if (platform === "win32") {
84
- // Windows
85
- command = "cmd"
86
- args = ["/c", "start", "", url]
87
- } else if (platform === "darwin") {
88
- // macOS
89
- command = "open"
90
- args = [url]
91
- } else {
92
- // Linux 和其他平台
93
- command = "xdg-open"
94
- args = [url]
95
- }
96
-
97
- try {
98
- const child = spawn(command, args, { detached: true, stdio: "ignore" })
99
- child.unref()
100
- return true
101
- } catch {
102
- return false
103
- }
104
- }
105
-
106
- /**
107
- * 跨平台复制文本到剪贴板
108
- * @param {string} text - 要复制的文本
109
- * @returns {boolean} 是否成功
110
- */
111
- function copyToClipboard(text) {
112
- const platform = process.platform
113
- let command
114
- let args
115
- let input = text
116
-
117
- if (platform === "win32") {
118
- // Windows - 使用 clip 命令
119
- command = "clip"
120
- args = []
121
- } else if (platform === "darwin") {
122
- // macOS - 使用 pbcopy
123
- command = "pbcopy"
124
- args = []
125
- } else {
126
- // Linux - 尝试 wl-copy (Wayland) 或 xclip (X11)
127
- try {
128
- // 先尝试 wl-copy (Wayland)
129
- const child = spawn("wl-copy", [], { stdio: ["pipe", "ignore", "ignore"] })
130
- child.stdin.write(text)
131
- child.stdin.end()
132
- return true
133
- } catch {
134
- // 回退到 xclip (X11)
135
- try {
136
- const child = spawn("xclip", ["-selection", "clipboard"], { stdio: ["pipe", "ignore", "ignore"] })
137
- child.stdin.write(text)
138
- child.stdin.end()
139
- return true
140
- } catch {
141
- return false
142
- }
143
- }
144
- }
145
-
146
- try {
147
- const child = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"] })
148
- child.stdin.write(input)
149
- child.stdin.end()
150
- return true
151
- } catch {
152
- return false
153
- }
154
- }
155
-
156
- async function pollAccessToken(deviceCode, interval) {
157
- let retryCount = 0
158
- const maxRetries = 3
159
-
160
- while (true) {
161
- await sleep(interval * 1000)
162
-
163
- let res
164
- try {
165
- res = await fetch(ACCESS_TOKEN_URL, {
166
- method: "POST",
167
- headers: {
168
- Accept: "application/json",
169
- "Content-Type": "application/json"
170
- },
171
- body: JSON.stringify({
172
- client_id: CLIENT_ID,
173
- device_code: deviceCode,
174
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
175
- })
176
- })
177
- retryCount = 0 // 重置重试计数
178
- } catch (networkError) {
179
- retryCount++
180
- if (retryCount >= maxRetries) {
181
- throw new Error(`Network error after ${maxRetries} retries: ${networkError.message}`)
182
- }
183
- process.stdout.write(`\n \x1b[33m⚠ 网络错误,${maxRetries - retryCount} 秒后重试...\x1b[0m`)
184
- await sleep(1000)
185
- process.stdout.write("\r \x1b[K等待授权...")
186
- continue
187
- }
188
-
189
- let data
190
- try {
191
- data = await res.json()
192
- } catch {
193
- // 如果无法解析 JSON,可能是网络问题,继续等待
194
- continue
195
- }
196
-
197
- if (data.access_token) {
198
- return data.access_token
199
- }
200
- if (data.error === "authorization_pending") {
201
- process.stdout.write(".")
202
- continue
203
- }
204
- if (data.error === "slow_down") {
205
- interval = (data.interval || interval) + 1
206
- process.stdout.write("\n \x1b[33m⚠ 请求过于频繁,已放慢速度...\x1b[0m")
207
- continue
208
- }
209
- if (data.error === "expired_token") {
210
- throw new Error("Authorization timed out. Please try again.")
211
- }
212
- if (data.error === "access_denied") {
213
- throw new Error("Authorization denied by user.")
214
- }
215
- // 其他错误,记录但继续等待(可能是临时错误)
216
- process.stdout.write(`\n \x1b[33m⚠ 服务器返回: ${data.error || 'unknown'},继续等待...\x1b[0m`)
217
- }
218
- }
219
-
220
- export async function ensureGitHubAuth() {
221
- // Check stored token
222
- const stored = await getStoredToken()
223
- if (stored) {
224
- const check = await validateToken(stored.token)
225
- if (check.valid) {
226
- return { token: stored.token, login: check.login }
227
- }
228
- // Token expired, remove it
229
- await logout()
230
- }
231
-
232
- // Start Device Flow
233
- console.log("\n\x1b[33m🔐 GitHub 账户未登录,正在启动授权...\x1b[0m\n")
234
-
235
- const deviceData = await requestDeviceCode()
236
- const { device_code, user_code, verification_uri, interval } = deviceData
237
-
238
- // 自动复制代码到剪贴板
239
- const copied = copyToClipboard(user_code)
240
-
241
- console.log(` 请在浏览器中打开: \x1b[36m\x1b[4m${verification_uri}\x1b[0m`)
242
- if (copied) {
243
- console.log(` 输入代码: \x1b[1m\x1b[32m${user_code}\x1b[0m \x1b[2m✅ 已复制到剪贴板\x1b[0m\n`)
244
- } else {
245
- console.log(` 输入代码: \x1b[1m\x1b[32m${user_code}\x1b[0m\n`)
246
- }
247
-
248
- // 自动打开浏览器
249
- const opened = openBrowser(verification_uri)
250
- if (opened) {
251
- console.log(" \x1b[2m已自动打开浏览器,请完成授权...\x1b[0m")
252
- if (copied) {
253
- console.log(" \x1b[2m提示: 在 GitHub 页面按 Ctrl+V 粘贴代码\x1b[0m\n")
254
- } else {
255
- console.log("")
256
- }
257
- } else {
258
- console.log(" \x1b[33m⚠ 无法自动打开浏览器,请手动访问上述链接\x1b[0m\n")
259
- }
260
-
261
- process.stdout.write(" 等待授权...")
262
-
263
- const token = await pollAccessToken(device_code, interval || 5)
264
-
265
- // Get user info
266
- const userRes = await fetch("https://api.github.com/user", {
267
- headers: {
268
- Authorization: `Bearer ${token}`,
269
- Accept: "application/vnd.github+json"
270
- }
271
- })
272
- if (!userRes.ok) {
273
- throw new Error(`Failed to get user info: ${userRes.status}`)
274
- }
275
- const user = await userRes.json()
276
-
277
- await saveToken({
278
- token,
279
- login: user.login,
280
- login_at: new Date().toISOString()
281
- })
282
-
283
- console.log(` \x1b[32m✓ 已登录为 @${user.login}\x1b[0m\n`)
284
-
285
- return { token, login: user.login }
286
- }
1
+ import { readFile, writeFile, unlink, mkdir } from "node:fs/promises"
2
+ import { dirname } from "node:path"
3
+ import { spawn } from "node:child_process"
4
+ import { githubTokenPath } from "../storage/paths.mjs"
5
+
6
+ const CLIENT_ID = "Ov23liCqhJ6cRaqyv3uA"
7
+ const DEVICE_CODE_URL = "https://github.com/login/device/code"
8
+ const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
9
+ const SCOPE = "repo"
10
+
11
+ export async function getStoredToken() {
12
+ try {
13
+ const raw = await readFile(githubTokenPath(), "utf8")
14
+ const data = JSON.parse(raw)
15
+ if (data.token && data.login) return data
16
+ return null
17
+ } catch {
18
+ return null
19
+ }
20
+ }
21
+
22
+ async function saveToken(data) {
23
+ const filePath = githubTokenPath()
24
+ await mkdir(dirname(filePath), { recursive: true })
25
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf8")
26
+ }
27
+
28
+ export async function logout() {
29
+ try {
30
+ await unlink(githubTokenPath())
31
+ return true
32
+ } catch {
33
+ return false
34
+ }
35
+ }
36
+
37
+ async function validateToken(token) {
38
+ try {
39
+ const res = await fetch("https://api.github.com/user", {
40
+ headers: {
41
+ Authorization: `Bearer ${token}`,
42
+ Accept: "application/vnd.github+json"
43
+ }
44
+ })
45
+ if (res.ok) {
46
+ const user = await res.json()
47
+ return { valid: true, login: user.login }
48
+ }
49
+ return { valid: false, login: null }
50
+ } catch {
51
+ return { valid: false, login: null }
52
+ }
53
+ }
54
+
55
+ async function requestDeviceCode() {
56
+ const res = await fetch(DEVICE_CODE_URL, {
57
+ method: "POST",
58
+ headers: {
59
+ Accept: "application/json",
60
+ "Content-Type": "application/json"
61
+ },
62
+ body: JSON.stringify({ client_id: CLIENT_ID, scope: SCOPE })
63
+ })
64
+ if (!res.ok) {
65
+ throw new Error(`GitHub device code request failed: ${res.status}`)
66
+ }
67
+ return res.json()
68
+ }
69
+
70
+ function sleep(ms) {
71
+ return new Promise((resolve) => setTimeout(resolve, ms))
72
+ }
73
+
74
+ /**
75
+ * 跨平台打开浏览器
76
+ * @param {string} url - 要打开的 URL
77
+ */
78
+ function openBrowser(url) {
79
+ // Validate URL is a safe https:// URL before passing to shell
80
+ let parsed
81
+ try { parsed = new URL(url) } catch { return false }
82
+ if (parsed.protocol !== "https:") return false
83
+
84
+ const safeUrl = parsed.href
85
+ const platform = process.platform
86
+ let command
87
+ let args
88
+
89
+ if (platform === "win32") {
90
+ command = "cmd"
91
+ args = ["/c", "start", "", safeUrl]
92
+ } else if (platform === "darwin") {
93
+ command = "open"
94
+ args = [safeUrl]
95
+ } else {
96
+ command = "xdg-open"
97
+ args = [safeUrl]
98
+ }
99
+
100
+ try {
101
+ const child = spawn(command, args, { detached: true, stdio: "ignore" })
102
+ child.unref()
103
+ return true
104
+ } catch {
105
+ return false
106
+ }
107
+ }
108
+
109
+ /**
110
+ * 跨平台复制文本到剪贴板
111
+ * @param {string} text - 要复制的文本
112
+ * @returns {boolean} 是否成功
113
+ */
114
+ function copyToClipboard(text) {
115
+ const platform = process.platform
116
+ let command
117
+ let args
118
+ let input = text
119
+
120
+ if (platform === "win32") {
121
+ // Windows - 使用 clip 命令
122
+ command = "clip"
123
+ args = []
124
+ } else if (platform === "darwin") {
125
+ // macOS - 使用 pbcopy
126
+ command = "pbcopy"
127
+ args = []
128
+ } else {
129
+ // Linux - 尝试 wl-copy (Wayland) xclip (X11)
130
+ try {
131
+ const child = spawn("wl-copy", [], { stdio: ["pipe", "ignore", "ignore"] })
132
+ child.on("error", () => {}) // prevent unhandled error crash
133
+ child.stdin.write(text)
134
+ child.stdin.end()
135
+ return true
136
+ } catch {
137
+ try {
138
+ const child = spawn("xclip", ["-selection", "clipboard"], { stdio: ["pipe", "ignore", "ignore"] })
139
+ child.on("error", () => {}) // prevent unhandled error crash
140
+ child.stdin.write(text)
141
+ child.stdin.end()
142
+ return true
143
+ } catch {
144
+ return false
145
+ }
146
+ }
147
+ }
148
+
149
+ try {
150
+ const child = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"] })
151
+ child.on("error", () => {}) // prevent unhandled error crash
152
+ child.stdin.write(input)
153
+ child.stdin.end()
154
+ return true
155
+ } catch {
156
+ return false
157
+ }
158
+ }
159
+
160
+ async function pollAccessToken(deviceCode, interval) {
161
+ let retryCount = 0
162
+ const maxRetries = 3
163
+ const deadline = Date.now() + 10 * 60 * 1000 // 10 minute total timeout
164
+
165
+ while (true) {
166
+ if (Date.now() > deadline) {
167
+ throw new Error("Authorization timed out after 10 minutes. Please try again.")
168
+ }
169
+ await sleep(interval * 1000)
170
+
171
+ let res
172
+ try {
173
+ res = await fetch(ACCESS_TOKEN_URL, {
174
+ method: "POST",
175
+ headers: {
176
+ Accept: "application/json",
177
+ "Content-Type": "application/json"
178
+ },
179
+ body: JSON.stringify({
180
+ client_id: CLIENT_ID,
181
+ device_code: deviceCode,
182
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
183
+ })
184
+ })
185
+ retryCount = 0 // 重置重试计数
186
+ } catch (networkError) {
187
+ retryCount++
188
+ if (retryCount >= maxRetries) {
189
+ throw new Error(`Network error after ${maxRetries} retries: ${networkError.message}`)
190
+ }
191
+ process.stdout.write(`\n \x1b[33m⚠ 网络错误,${maxRetries - retryCount} 秒后重试...\x1b[0m`)
192
+ await sleep(1000)
193
+ process.stdout.write("\r \x1b[K等待授权...")
194
+ continue
195
+ }
196
+
197
+ let data
198
+ try {
199
+ data = await res.json()
200
+ } catch {
201
+ // 如果无法解析 JSON,可能是网络问题,继续等待
202
+ continue
203
+ }
204
+
205
+ if (data.access_token) {
206
+ return data.access_token
207
+ }
208
+ if (data.error === "authorization_pending") {
209
+ process.stdout.write(".")
210
+ continue
211
+ }
212
+ if (data.error === "slow_down") {
213
+ interval = (data.interval || interval) + 1
214
+ process.stdout.write("\n \x1b[33m⚠ 请求过于频繁,已放慢速度...\x1b[0m")
215
+ continue
216
+ }
217
+ if (data.error === "expired_token") {
218
+ throw new Error("Authorization timed out. Please try again.")
219
+ }
220
+ if (data.error === "access_denied") {
221
+ throw new Error("Authorization denied by user.")
222
+ }
223
+ // 其他错误,记录但继续等待(可能是临时错误)
224
+ process.stdout.write(`\n \x1b[33m⚠ 服务器返回: ${data.error || 'unknown'},继续等待...\x1b[0m`)
225
+ }
226
+ }
227
+
228
+ export async function ensureGitHubAuth() {
229
+ // Check stored token
230
+ const stored = await getStoredToken()
231
+ if (stored) {
232
+ const check = await validateToken(stored.token)
233
+ if (check.valid) {
234
+ return { token: stored.token, login: check.login }
235
+ }
236
+ // Token expired, remove it
237
+ await logout()
238
+ }
239
+
240
+ // Start Device Flow
241
+ console.log("\n\x1b[33m🔐 GitHub 账户未登录,正在启动授权...\x1b[0m\n")
242
+
243
+ const deviceData = await requestDeviceCode()
244
+ const { device_code, user_code, verification_uri, interval } = deviceData
245
+
246
+ // 自动复制代码到剪贴板
247
+ const copied = copyToClipboard(user_code)
248
+
249
+ console.log(` 请在浏览器中打开: \x1b[36m\x1b[4m${verification_uri}\x1b[0m`)
250
+ if (copied) {
251
+ console.log(` 输入代码: \x1b[1m\x1b[32m${user_code}\x1b[0m \x1b[2m✅ 已复制到剪贴板\x1b[0m\n`)
252
+ } else {
253
+ console.log(` 输入代码: \x1b[1m\x1b[32m${user_code}\x1b[0m\n`)
254
+ }
255
+
256
+ // 自动打开浏览器
257
+ const opened = openBrowser(verification_uri)
258
+ if (opened) {
259
+ console.log(" \x1b[2m已自动打开浏览器,请完成授权...\x1b[0m")
260
+ if (copied) {
261
+ console.log(" \x1b[2m提示: 在 GitHub 页面按 Ctrl+V 粘贴代码\x1b[0m\n")
262
+ } else {
263
+ console.log("")
264
+ }
265
+ } else {
266
+ console.log(" \x1b[33m⚠ 无法自动打开浏览器,请手动访问上述链接\x1b[0m\n")
267
+ }
268
+
269
+ process.stdout.write(" 等待授权...")
270
+
271
+ const token = await pollAccessToken(device_code, interval || 5)
272
+
273
+ // Get user info
274
+ const userRes = await fetch("https://api.github.com/user", {
275
+ headers: {
276
+ Authorization: `Bearer ${token}`,
277
+ Accept: "application/vnd.github+json"
278
+ }
279
+ })
280
+ if (!userRes.ok) {
281
+ throw new Error(`Failed to get user info: ${userRes.status}`)
282
+ }
283
+ const user = await userRes.json()
284
+
285
+ await saveToken({
286
+ token,
287
+ login: user.login,
288
+ login_at: new Date().toISOString()
289
+ })
290
+
291
+ console.log(` \x1b[32m✓ 已登录为 @${user.login}\x1b[0m\n`)
292
+
293
+ return { token, login: user.login }
294
+ }