@kkelly-offical/kkcode 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 (196) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +445 -0
  3. package/package.json +46 -0
  4. package/src/agent/agent.mjs +170 -0
  5. package/src/agent/custom-agent-loader.mjs +158 -0
  6. package/src/agent/generator.mjs +115 -0
  7. package/src/agent/prompt/architect.txt +36 -0
  8. package/src/agent/prompt/build-fixer.txt +71 -0
  9. package/src/agent/prompt/build.txt +101 -0
  10. package/src/agent/prompt/compaction.txt +12 -0
  11. package/src/agent/prompt/explore.txt +29 -0
  12. package/src/agent/prompt/guide.txt +40 -0
  13. package/src/agent/prompt/longagent.txt +178 -0
  14. package/src/agent/prompt/plan.txt +50 -0
  15. package/src/agent/prompt/researcher.txt +23 -0
  16. package/src/agent/prompt/reviewer.txt +44 -0
  17. package/src/agent/prompt/security-reviewer.txt +62 -0
  18. package/src/agent/prompt/tdd-guide.txt +84 -0
  19. package/src/agent/prompt/title.txt +8 -0
  20. package/src/command/custom-commands.mjs +57 -0
  21. package/src/commands/agent.mjs +71 -0
  22. package/src/commands/audit.mjs +77 -0
  23. package/src/commands/background.mjs +86 -0
  24. package/src/commands/chat.mjs +114 -0
  25. package/src/commands/command.mjs +41 -0
  26. package/src/commands/config.mjs +44 -0
  27. package/src/commands/doctor.mjs +148 -0
  28. package/src/commands/hook.mjs +29 -0
  29. package/src/commands/init.mjs +141 -0
  30. package/src/commands/longagent.mjs +100 -0
  31. package/src/commands/mcp.mjs +89 -0
  32. package/src/commands/permission.mjs +36 -0
  33. package/src/commands/prompt.mjs +42 -0
  34. package/src/commands/review.mjs +266 -0
  35. package/src/commands/rule.mjs +34 -0
  36. package/src/commands/session.mjs +235 -0
  37. package/src/commands/theme.mjs +98 -0
  38. package/src/commands/usage.mjs +91 -0
  39. package/src/config/defaults.mjs +195 -0
  40. package/src/config/import-config.mjs +76 -0
  41. package/src/config/load-config.mjs +76 -0
  42. package/src/config/schema.mjs +509 -0
  43. package/src/context.mjs +40 -0
  44. package/src/core/constants.mjs +46 -0
  45. package/src/core/errors.mjs +57 -0
  46. package/src/core/events.mjs +29 -0
  47. package/src/core/types.mjs +57 -0
  48. package/src/github/api.mjs +78 -0
  49. package/src/github/auth.mjs +286 -0
  50. package/src/github/flow.mjs +298 -0
  51. package/src/github/workspace.mjs +212 -0
  52. package/src/index.mjs +82 -0
  53. package/src/knowledge/api-design.txt +9 -0
  54. package/src/knowledge/cpp.txt +10 -0
  55. package/src/knowledge/docker.txt +10 -0
  56. package/src/knowledge/dotnet.txt +9 -0
  57. package/src/knowledge/electron.txt +10 -0
  58. package/src/knowledge/flutter.txt +10 -0
  59. package/src/knowledge/go.txt +9 -0
  60. package/src/knowledge/graphql.txt +10 -0
  61. package/src/knowledge/java.txt +9 -0
  62. package/src/knowledge/kotlin.txt +10 -0
  63. package/src/knowledge/loader.mjs +125 -0
  64. package/src/knowledge/next.txt +8 -0
  65. package/src/knowledge/node.txt +8 -0
  66. package/src/knowledge/nuxt.txt +9 -0
  67. package/src/knowledge/php.txt +10 -0
  68. package/src/knowledge/python.txt +10 -0
  69. package/src/knowledge/react-native.txt +10 -0
  70. package/src/knowledge/react.txt +9 -0
  71. package/src/knowledge/ruby.txt +11 -0
  72. package/src/knowledge/rust.txt +9 -0
  73. package/src/knowledge/svelte.txt +9 -0
  74. package/src/knowledge/swift.txt +10 -0
  75. package/src/knowledge/tailwind.txt +10 -0
  76. package/src/knowledge/testing.txt +8 -0
  77. package/src/knowledge/typescript.txt +8 -0
  78. package/src/knowledge/vue.txt +9 -0
  79. package/src/mcp/client-http.mjs +157 -0
  80. package/src/mcp/client-sse.mjs +286 -0
  81. package/src/mcp/client-stdio.mjs +451 -0
  82. package/src/mcp/registry.mjs +394 -0
  83. package/src/mcp/stdio-framing.mjs +127 -0
  84. package/src/orchestration/background-manager.mjs +358 -0
  85. package/src/orchestration/background-worker.mjs +245 -0
  86. package/src/orchestration/longagent-manager.mjs +116 -0
  87. package/src/orchestration/stage-scheduler.mjs +489 -0
  88. package/src/orchestration/subagent-router.mjs +62 -0
  89. package/src/orchestration/task-scheduler.mjs +74 -0
  90. package/src/permission/engine.mjs +92 -0
  91. package/src/permission/exec-policy.mjs +372 -0
  92. package/src/permission/prompt.mjs +39 -0
  93. package/src/permission/rules.mjs +120 -0
  94. package/src/permission/workspace-trust.mjs +44 -0
  95. package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
  96. package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
  97. package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
  98. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
  99. package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
  100. package/src/plugin/hook-bus.mjs +154 -0
  101. package/src/provider/anthropic.mjs +389 -0
  102. package/src/provider/ollama.mjs +236 -0
  103. package/src/provider/openai-compatible.mjs +1 -0
  104. package/src/provider/openai.mjs +339 -0
  105. package/src/provider/retry-policy.mjs +68 -0
  106. package/src/provider/router.mjs +228 -0
  107. package/src/provider/sse.mjs +91 -0
  108. package/src/repl.mjs +2929 -0
  109. package/src/review/diff-parser.mjs +36 -0
  110. package/src/review/rejection-queue.mjs +62 -0
  111. package/src/review/review-store.mjs +21 -0
  112. package/src/review/risk-score.mjs +61 -0
  113. package/src/rules/load-rules.mjs +64 -0
  114. package/src/runtime.mjs +1 -0
  115. package/src/session/checkpoint.mjs +239 -0
  116. package/src/session/compaction.mjs +276 -0
  117. package/src/session/engine.mjs +225 -0
  118. package/src/session/instinct-manager.mjs +172 -0
  119. package/src/session/instruction-loader.mjs +25 -0
  120. package/src/session/longagent-plan.mjs +329 -0
  121. package/src/session/longagent-scaffold.mjs +100 -0
  122. package/src/session/longagent.mjs +1462 -0
  123. package/src/session/loop.mjs +905 -0
  124. package/src/session/memory-loader.mjs +75 -0
  125. package/src/session/project-context.mjs +367 -0
  126. package/src/session/prompt/anthropic.txt +151 -0
  127. package/src/session/prompt/beast.txt +37 -0
  128. package/src/session/prompt/max-steps.txt +6 -0
  129. package/src/session/prompt/plan.txt +9 -0
  130. package/src/session/prompt/qwen.txt +46 -0
  131. package/src/session/prompt-loader.mjs +18 -0
  132. package/src/session/recovery.mjs +52 -0
  133. package/src/session/store.mjs +503 -0
  134. package/src/session/system-prompt.mjs +260 -0
  135. package/src/session/task-validator.mjs +266 -0
  136. package/src/session/usability-gates.mjs +379 -0
  137. package/src/skill/builtin/backend-patterns.mjs +123 -0
  138. package/src/skill/builtin/commit.mjs +64 -0
  139. package/src/skill/builtin/debug.mjs +45 -0
  140. package/src/skill/builtin/frontend-patterns.mjs +120 -0
  141. package/src/skill/builtin/frontend.mjs +188 -0
  142. package/src/skill/builtin/init.mjs +220 -0
  143. package/src/skill/builtin/review.mjs +49 -0
  144. package/src/skill/builtin/security-checklist.mjs +80 -0
  145. package/src/skill/builtin/tdd.mjs +54 -0
  146. package/src/skill/generator.mjs +113 -0
  147. package/src/skill/registry.mjs +336 -0
  148. package/src/storage/audit-store.mjs +83 -0
  149. package/src/storage/event-log.mjs +82 -0
  150. package/src/storage/ghost-commit-store.mjs +235 -0
  151. package/src/storage/json-store.mjs +53 -0
  152. package/src/storage/paths.mjs +148 -0
  153. package/src/theme/color.mjs +64 -0
  154. package/src/theme/default-theme.mjs +29 -0
  155. package/src/theme/load-theme.mjs +71 -0
  156. package/src/theme/markdown.mjs +135 -0
  157. package/src/theme/schema.mjs +45 -0
  158. package/src/theme/status-bar.mjs +158 -0
  159. package/src/tool/audit-wrapper.mjs +38 -0
  160. package/src/tool/edit-transaction.mjs +126 -0
  161. package/src/tool/executor.mjs +109 -0
  162. package/src/tool/file-lock-manager.mjs +85 -0
  163. package/src/tool/git-auto.mjs +545 -0
  164. package/src/tool/git-full-auto.mjs +478 -0
  165. package/src/tool/image-util.mjs +276 -0
  166. package/src/tool/prompt/background_cancel.txt +1 -0
  167. package/src/tool/prompt/background_output.txt +1 -0
  168. package/src/tool/prompt/bash.txt +71 -0
  169. package/src/tool/prompt/codesearch.txt +18 -0
  170. package/src/tool/prompt/edit.txt +27 -0
  171. package/src/tool/prompt/enter_plan.txt +74 -0
  172. package/src/tool/prompt/exit_plan.txt +62 -0
  173. package/src/tool/prompt/glob.txt +33 -0
  174. package/src/tool/prompt/grep.txt +43 -0
  175. package/src/tool/prompt/list.txt +8 -0
  176. package/src/tool/prompt/multiedit.txt +20 -0
  177. package/src/tool/prompt/notebookedit.txt +21 -0
  178. package/src/tool/prompt/patch.txt +24 -0
  179. package/src/tool/prompt/question.txt +44 -0
  180. package/src/tool/prompt/read.txt +40 -0
  181. package/src/tool/prompt/task.txt +83 -0
  182. package/src/tool/prompt/todowrite.txt +117 -0
  183. package/src/tool/prompt/webfetch.txt +38 -0
  184. package/src/tool/prompt/websearch.txt +43 -0
  185. package/src/tool/prompt/write.txt +38 -0
  186. package/src/tool/prompt-loader.mjs +18 -0
  187. package/src/tool/question-prompt.mjs +86 -0
  188. package/src/tool/registry.mjs +1309 -0
  189. package/src/tool/task-tool.mjs +28 -0
  190. package/src/ui/activity-renderer.mjs +410 -0
  191. package/src/ui/repl-dashboard.mjs +357 -0
  192. package/src/usage/pricing.mjs +121 -0
  193. package/src/usage/usage-meter.mjs +113 -0
  194. package/src/util/git.mjs +496 -0
  195. package/src/util/template.mjs +10 -0
  196. package/src/util/yaml.mjs +100 -0
@@ -0,0 +1,57 @@
1
+ export class KkError extends Error {
2
+ constructor(code, message, details = null) {
3
+ super(message)
4
+ this.name = "KkError"
5
+ this.code = code
6
+ this.details = details
7
+ }
8
+ }
9
+
10
+ export class ValidationError extends KkError {
11
+ constructor(message, details = null) {
12
+ super("VALIDATION_ERROR", message, details)
13
+ this.name = "ValidationError"
14
+ }
15
+ }
16
+
17
+ export class ProviderError extends KkError {
18
+ constructor(message, details = null) {
19
+ super("PROVIDER_ERROR", message, details)
20
+ this.name = "ProviderError"
21
+ }
22
+ }
23
+
24
+ export class PermissionError extends KkError {
25
+ constructor(message, details = null) {
26
+ super("PERMISSION_DENIED", message, details)
27
+ this.name = "PermissionError"
28
+ }
29
+ }
30
+
31
+ export class ToolError extends KkError {
32
+ constructor(message, details = null) {
33
+ super("TOOL_ERROR", message, details)
34
+ this.name = "ToolError"
35
+ }
36
+ }
37
+
38
+ export class McpError extends KkError {
39
+ /**
40
+ * @param {string} message
41
+ * @param {{ reason?: string, server?: string, action?: string }} [details]
42
+ * reason: "timeout" | "spawn_failed" | "connection_refused" | "bad_response" | "server_crash" | "protocol_error" | "unknown"
43
+ */
44
+ constructor(message, details = null) {
45
+ super("MCP_ERROR", message, details)
46
+ this.name = "McpError"
47
+ this.reason = details?.reason || "unknown"
48
+ this.server = details?.server || null
49
+ }
50
+ }
51
+
52
+ export class SessionError extends KkError {
53
+ constructor(message, details = null) {
54
+ super("SESSION_ERROR", message, details)
55
+ this.name = "SessionError"
56
+ }
57
+ }
@@ -0,0 +1,29 @@
1
+ import { makeEventEnvelope } from "./types.mjs"
2
+
3
+ const listeners = new Set()
4
+ const sinks = new Set()
5
+
6
+ export const EventBus = {
7
+ subscribe(fn) {
8
+ listeners.add(fn)
9
+ return () => listeners.delete(fn)
10
+ },
11
+ registerSink(fn) {
12
+ sinks.add(fn)
13
+ return () => sinks.delete(fn)
14
+ },
15
+ async emit(input) {
16
+ const event = makeEventEnvelope(input)
17
+ for (const sink of sinks) {
18
+ try { await sink(event) } catch (err) {
19
+ console.error("[events] sink error:", err?.message || err)
20
+ }
21
+ }
22
+ for (const fn of listeners) {
23
+ try { await fn(event) } catch (err) {
24
+ console.error("[events] listener error:", err?.message || err)
25
+ }
26
+ }
27
+ return event
28
+ }
29
+ }
@@ -0,0 +1,57 @@
1
+ import { randomUUID } from "node:crypto"
2
+
3
+ export function nowMs() {
4
+ return Date.now()
5
+ }
6
+
7
+ export function newId(prefix) {
8
+ return `${prefix}_${randomUUID().slice(0, 12)}`
9
+ }
10
+
11
+ export function makeEventEnvelope({ type, sessionId = null, turnId = null, payload = {} }) {
12
+ return {
13
+ id: newId("evt"),
14
+ type,
15
+ sessionId,
16
+ turnId,
17
+ timestamp: nowMs(),
18
+ payload
19
+ }
20
+ }
21
+
22
+ export function makeToolResult({ name, status, output = "", error = null, durationMs = 0, metadata = {} }) {
23
+ return {
24
+ name,
25
+ status,
26
+ output,
27
+ error,
28
+ durationMs,
29
+ metadata
30
+ }
31
+ }
32
+
33
+ export function makeTurnResult({
34
+ sessionId,
35
+ turnId,
36
+ mode,
37
+ model,
38
+ reply,
39
+ usage,
40
+ cost,
41
+ toolEvents,
42
+ estimated = false,
43
+ warnings = []
44
+ }) {
45
+ return {
46
+ sessionId,
47
+ turnId,
48
+ mode,
49
+ model,
50
+ reply,
51
+ usage,
52
+ cost,
53
+ toolEvents,
54
+ estimated,
55
+ warnings
56
+ }
57
+ }
@@ -0,0 +1,78 @@
1
+ const API_BASE = "https://api.github.com"
2
+
3
+ function headers(token) {
4
+ return {
5
+ Authorization: `Bearer ${token}`,
6
+ Accept: "application/vnd.github+json",
7
+ "X-GitHub-Api-Version": "2022-11-28"
8
+ }
9
+ }
10
+
11
+ async function ghFetch(token, path, params = {}) {
12
+ const url = new URL(`${API_BASE}${path}`)
13
+ for (const [k, v] of Object.entries(params)) {
14
+ if (v !== undefined && v !== null) url.searchParams.set(k, String(v))
15
+ }
16
+ const res = await fetch(url.href, { headers: headers(token) })
17
+ if (!res.ok) {
18
+ const body = await res.text().catch(() => "")
19
+ throw new Error(`GitHub API ${res.status}: ${path} — ${body.slice(0, 200)}`)
20
+ }
21
+ return res.json()
22
+ }
23
+
24
+ export async function listUserRepos(token, { page = 1, perPage = 20, sort = "pushed" } = {}) {
25
+ const repos = await ghFetch(token, "/user/repos", {
26
+ sort,
27
+ per_page: perPage,
28
+ page,
29
+ affiliation: "owner,collaborator,organization_member"
30
+ })
31
+ return repos.map((r) => ({
32
+ full_name: r.full_name,
33
+ name: r.name,
34
+ owner: r.owner.login,
35
+ description: r.description || "",
36
+ default_branch: r.default_branch,
37
+ stars: r.stargazers_count,
38
+ pushed_at: r.pushed_at,
39
+ private: r.private
40
+ }))
41
+ }
42
+
43
+ export async function searchRepos(token, query, login) {
44
+ const q = login ? `${query} user:${login}` : query
45
+ const data = await ghFetch(token, "/search/repositories", { q, per_page: 20, sort: "updated" })
46
+ return (data.items || []).map((r) => ({
47
+ full_name: r.full_name,
48
+ name: r.name,
49
+ owner: r.owner.login,
50
+ description: r.description || "",
51
+ default_branch: r.default_branch,
52
+ stars: r.stargazers_count,
53
+ pushed_at: r.pushed_at,
54
+ private: r.private
55
+ }))
56
+ }
57
+
58
+ export async function listBranches(token, owner, repo) {
59
+ const branches = await ghFetch(token, `/repos/${owner}/${repo}/branches`, { per_page: 100 })
60
+ return branches.map((b) => ({
61
+ name: b.name,
62
+ protected: b.protected
63
+ }))
64
+ }
65
+
66
+ export async function getRepo(token, owner, repo) {
67
+ const r = await ghFetch(token, `/repos/${owner}/${repo}`)
68
+ return {
69
+ full_name: r.full_name,
70
+ name: r.name,
71
+ owner: r.owner.login,
72
+ description: r.description || "",
73
+ default_branch: r.default_branch,
74
+ stars: r.stargazers_count,
75
+ pushed_at: r.pushed_at,
76
+ private: r.private
77
+ }
78
+ }
@@ -0,0 +1,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
+ 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
+ }