bincode-cli 1.0.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 (300) hide show
  1. package/AGENTS.md +27 -0
  2. package/README.md +15 -0
  3. package/bin/bincode +98 -0
  4. package/bunfig.toml +4 -0
  5. package/package.json +124 -0
  6. package/parsers-config.ts +239 -0
  7. package/script/build.ts +167 -0
  8. package/script/postinstall.mjs +206 -0
  9. package/script/publish.ts +99 -0
  10. package/script/schema.ts +47 -0
  11. package/src/acp/README.md +164 -0
  12. package/src/acp/agent.ts +1051 -0
  13. package/src/acp/session.ts +101 -0
  14. package/src/acp/types.ts +22 -0
  15. package/src/agent/agent.ts +398 -0
  16. package/src/agent/generate.txt +75 -0
  17. package/src/agent/prompt/compaction.txt +12 -0
  18. package/src/agent/prompt/explore.txt +18 -0
  19. package/src/agent/prompt/summary.txt +10 -0
  20. package/src/agent/prompt/title.txt +36 -0
  21. package/src/auth/bineric-login.ts +506 -0
  22. package/src/auth/index.ts +70 -0
  23. package/src/bun/index.ts +114 -0
  24. package/src/bus/bus-event.ts +43 -0
  25. package/src/bus/global.ts +10 -0
  26. package/src/bus/index.ts +105 -0
  27. package/src/cli/auth-check.ts +61 -0
  28. package/src/cli/bootstrap.ts +21 -0
  29. package/src/cli/cmd/acp.ts +88 -0
  30. package/src/cli/cmd/agent.ts +256 -0
  31. package/src/cli/cmd/auth.ts +436 -0
  32. package/src/cli/cmd/cmd.ts +7 -0
  33. package/src/cli/cmd/debug/config.ts +15 -0
  34. package/src/cli/cmd/debug/file.ts +91 -0
  35. package/src/cli/cmd/debug/index.ts +43 -0
  36. package/src/cli/cmd/debug/lsp.ts +48 -0
  37. package/src/cli/cmd/debug/ripgrep.ts +83 -0
  38. package/src/cli/cmd/debug/scrap.ts +15 -0
  39. package/src/cli/cmd/debug/skill.ts +15 -0
  40. package/src/cli/cmd/debug/snapshot.ts +48 -0
  41. package/src/cli/cmd/export.ts +88 -0
  42. package/src/cli/cmd/generate.ts +38 -0
  43. package/src/cli/cmd/github.ts +1399 -0
  44. package/src/cli/cmd/import.ts +98 -0
  45. package/src/cli/cmd/login.ts +112 -0
  46. package/src/cli/cmd/logout.ts +38 -0
  47. package/src/cli/cmd/mcp.ts +654 -0
  48. package/src/cli/cmd/models.ts +77 -0
  49. package/src/cli/cmd/pr.ts +112 -0
  50. package/src/cli/cmd/run.ts +368 -0
  51. package/src/cli/cmd/serve.ts +31 -0
  52. package/src/cli/cmd/session.ts +106 -0
  53. package/src/cli/cmd/stats.ts +298 -0
  54. package/src/cli/cmd/tui/app.tsx +669 -0
  55. package/src/cli/cmd/tui/attach.ts +30 -0
  56. package/src/cli/cmd/tui/component/border.tsx +21 -0
  57. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  58. package/src/cli/cmd/tui/component/dialog-command.tsx +123 -0
  59. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  60. package/src/cli/cmd/tui/component/dialog-model.tsx +223 -0
  61. package/src/cli/cmd/tui/component/dialog-provider.tsx +224 -0
  62. package/src/cli/cmd/tui/component/dialog-session-list.tsx +102 -0
  63. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  64. package/src/cli/cmd/tui/component/dialog-status.tsx +162 -0
  65. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  66. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  67. package/src/cli/cmd/tui/component/logo.tsx +32 -0
  68. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +560 -0
  69. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  70. package/src/cli/cmd/tui/component/prompt/index.tsx +1052 -0
  71. package/src/cli/cmd/tui/context/args.tsx +14 -0
  72. package/src/cli/cmd/tui/context/directory.ts +13 -0
  73. package/src/cli/cmd/tui/context/exit.tsx +23 -0
  74. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  75. package/src/cli/cmd/tui/context/keybind.tsx +101 -0
  76. package/src/cli/cmd/tui/context/kv.tsx +49 -0
  77. package/src/cli/cmd/tui/context/local.tsx +339 -0
  78. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  79. package/src/cli/cmd/tui/context/route.tsx +46 -0
  80. package/src/cli/cmd/tui/context/sdk.tsx +74 -0
  81. package/src/cli/cmd/tui/context/sync.tsx +372 -0
  82. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  83. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  84. package/src/cli/cmd/tui/context/theme/bincode.json +245 -0
  85. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  86. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  87. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  88. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  89. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  90. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  91. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  92. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  93. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  94. package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
  95. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  96. package/src/cli/cmd/tui/context/theme/lucent-orng.json +227 -0
  97. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  98. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  99. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  100. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  101. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  102. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  103. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  104. package/src/cli/cmd/tui/context/theme/orng.json +245 -0
  105. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  106. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  107. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  108. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  109. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  110. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  111. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  112. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  113. package/src/cli/cmd/tui/context/theme.tsx +1109 -0
  114. package/src/cli/cmd/tui/event.ts +40 -0
  115. package/src/cli/cmd/tui/routes/home.tsx +105 -0
  116. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
  117. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
  118. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  119. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  120. package/src/cli/cmd/tui/routes/session/footer.tsx +88 -0
  121. package/src/cli/cmd/tui/routes/session/header.tsx +141 -0
  122. package/src/cli/cmd/tui/routes/session/index.tsx +1888 -0
  123. package/src/cli/cmd/tui/routes/session/sidebar.tsx +321 -0
  124. package/src/cli/cmd/tui/spawn.ts +60 -0
  125. package/src/cli/cmd/tui/thread.ts +120 -0
  126. package/src/cli/cmd/tui/ui/dialog-alert.tsx +57 -0
  127. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
  128. package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
  129. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +77 -0
  130. package/src/cli/cmd/tui/ui/dialog-select.tsx +330 -0
  131. package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
  132. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  133. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  134. package/src/cli/cmd/tui/util/clipboard.ts +127 -0
  135. package/src/cli/cmd/tui/util/editor.ts +32 -0
  136. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  137. package/src/cli/cmd/tui/worker.ts +63 -0
  138. package/src/cli/cmd/uninstall.ts +344 -0
  139. package/src/cli/cmd/upgrade.ts +67 -0
  140. package/src/cli/cmd/web.ts +84 -0
  141. package/src/cli/error.ts +55 -0
  142. package/src/cli/ui.ts +84 -0
  143. package/src/cli/upgrade.ts +25 -0
  144. package/src/command/index.ts +80 -0
  145. package/src/command/template/initialize.txt +10 -0
  146. package/src/command/template/review.txt +97 -0
  147. package/src/config/config.ts +995 -0
  148. package/src/config/markdown.ts +41 -0
  149. package/src/env/index.ts +26 -0
  150. package/src/file/ignore.ts +83 -0
  151. package/src/file/index.ts +328 -0
  152. package/src/file/ripgrep.ts +393 -0
  153. package/src/file/time.ts +64 -0
  154. package/src/file/watcher.ts +103 -0
  155. package/src/flag/flag.ts +46 -0
  156. package/src/format/formatter.ts +315 -0
  157. package/src/format/index.ts +137 -0
  158. package/src/global/index.ts +52 -0
  159. package/src/id/id.ts +73 -0
  160. package/src/ide/index.ts +76 -0
  161. package/src/index.ts +217 -0
  162. package/src/installation/index.ts +196 -0
  163. package/src/lsp/client.ts +229 -0
  164. package/src/lsp/index.ts +485 -0
  165. package/src/lsp/language.ts +116 -0
  166. package/src/lsp/server.ts +1895 -0
  167. package/src/mcp/auth.ts +135 -0
  168. package/src/mcp/index.ts +654 -0
  169. package/src/mcp/oauth-callback.ts +200 -0
  170. package/src/mcp/oauth-provider.ts +154 -0
  171. package/src/patch/index.ts +622 -0
  172. package/src/permission/index.ts +199 -0
  173. package/src/plugin/index.ts +101 -0
  174. package/src/project/bootstrap.ts +31 -0
  175. package/src/project/instance.ts +78 -0
  176. package/src/project/project.ts +221 -0
  177. package/src/project/state.ts +65 -0
  178. package/src/project/vcs.ts +76 -0
  179. package/src/provider/auth.ts +143 -0
  180. package/src/provider/models-macro.ts +11 -0
  181. package/src/provider/models.ts +106 -0
  182. package/src/provider/provider.ts +1071 -0
  183. package/src/provider/sdk/openai-compatible/src/README.md +5 -0
  184. package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
  185. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +101 -0
  186. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
  187. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +22 -0
  188. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
  189. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
  190. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
  191. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1713 -0
  192. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
  193. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
  194. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
  195. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
  196. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
  197. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
  198. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
  199. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
  200. package/src/provider/transform.ts +455 -0
  201. package/src/pty/index.ts +231 -0
  202. package/src/server/error.ts +36 -0
  203. package/src/server/project.ts +79 -0
  204. package/src/server/server.ts +2642 -0
  205. package/src/server/tui.ts +71 -0
  206. package/src/session/compaction.ts +223 -0
  207. package/src/session/index.ts +458 -0
  208. package/src/session/llm.ts +201 -0
  209. package/src/session/message-v2.ts +659 -0
  210. package/src/session/message.ts +189 -0
  211. package/src/session/processor.ts +409 -0
  212. package/src/session/prompt/anthropic-20250930.txt +166 -0
  213. package/src/session/prompt/anthropic.txt +104 -0
  214. package/src/session/prompt/anthropic_spoof.txt +1 -0
  215. package/src/session/prompt/beast.txt +147 -0
  216. package/src/session/prompt/build-switch.txt +5 -0
  217. package/src/session/prompt/codex.txt +318 -0
  218. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  219. package/src/session/prompt/gemini.txt +155 -0
  220. package/src/session/prompt/max-steps.txt +16 -0
  221. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  222. package/src/session/prompt/plan.txt +26 -0
  223. package/src/session/prompt/polaris.txt +106 -0
  224. package/src/session/prompt/qwen.txt +109 -0
  225. package/src/session/prompt.ts +1446 -0
  226. package/src/session/retry.ts +86 -0
  227. package/src/session/revert.ts +108 -0
  228. package/src/session/status.ts +76 -0
  229. package/src/session/summary.ts +194 -0
  230. package/src/session/system.ts +120 -0
  231. package/src/session/todo.ts +37 -0
  232. package/src/share/share-next.ts +194 -0
  233. package/src/share/share.ts +87 -0
  234. package/src/shell/shell.ts +67 -0
  235. package/src/skill/index.ts +1 -0
  236. package/src/skill/skill.ts +83 -0
  237. package/src/snapshot/index.ts +197 -0
  238. package/src/storage/storage.ts +226 -0
  239. package/src/tool/bash.ts +306 -0
  240. package/src/tool/bash.txt +158 -0
  241. package/src/tool/batch.ts +175 -0
  242. package/src/tool/batch.txt +24 -0
  243. package/src/tool/codesearch.ts +138 -0
  244. package/src/tool/codesearch.txt +12 -0
  245. package/src/tool/edit.ts +675 -0
  246. package/src/tool/edit.txt +10 -0
  247. package/src/tool/glob.ts +65 -0
  248. package/src/tool/glob.txt +6 -0
  249. package/src/tool/grep.ts +121 -0
  250. package/src/tool/grep.txt +8 -0
  251. package/src/tool/invalid.ts +17 -0
  252. package/src/tool/ls.ts +110 -0
  253. package/src/tool/ls.txt +1 -0
  254. package/src/tool/lsp-diagnostics.ts +26 -0
  255. package/src/tool/lsp-diagnostics.txt +1 -0
  256. package/src/tool/lsp-hover.ts +31 -0
  257. package/src/tool/lsp-hover.txt +1 -0
  258. package/src/tool/lsp.ts +87 -0
  259. package/src/tool/lsp.txt +19 -0
  260. package/src/tool/multiedit.ts +46 -0
  261. package/src/tool/multiedit.txt +41 -0
  262. package/src/tool/patch.ts +233 -0
  263. package/src/tool/patch.txt +1 -0
  264. package/src/tool/read.ts +219 -0
  265. package/src/tool/read.txt +12 -0
  266. package/src/tool/registry.ts +162 -0
  267. package/src/tool/skill.ts +100 -0
  268. package/src/tool/task.ts +136 -0
  269. package/src/tool/task.txt +60 -0
  270. package/src/tool/todo.ts +39 -0
  271. package/src/tool/todoread.txt +14 -0
  272. package/src/tool/todowrite.txt +167 -0
  273. package/src/tool/tool.ts +71 -0
  274. package/src/tool/webfetch.ts +187 -0
  275. package/src/tool/webfetch.txt +13 -0
  276. package/src/tool/websearch.ts +150 -0
  277. package/src/tool/websearch.txt +11 -0
  278. package/src/tool/write.ts +110 -0
  279. package/src/tool/write.txt +8 -0
  280. package/src/util/archive.ts +16 -0
  281. package/src/util/color.ts +19 -0
  282. package/src/util/context.ts +25 -0
  283. package/src/util/defer.ts +12 -0
  284. package/src/util/eventloop.ts +20 -0
  285. package/src/util/filesystem.ts +83 -0
  286. package/src/util/fn.ts +11 -0
  287. package/src/util/iife.ts +3 -0
  288. package/src/util/keybind.ts +102 -0
  289. package/src/util/lazy.ts +11 -0
  290. package/src/util/locale.ts +81 -0
  291. package/src/util/lock.ts +98 -0
  292. package/src/util/log.ts +180 -0
  293. package/src/util/queue.ts +32 -0
  294. package/src/util/rpc.ts +42 -0
  295. package/src/util/scrap.ts +10 -0
  296. package/src/util/signal.ts +12 -0
  297. package/src/util/timeout.ts +14 -0
  298. package/src/util/token.ts +7 -0
  299. package/src/util/wildcard.ts +54 -0
  300. package/tsconfig.json +16 -0
@@ -0,0 +1,506 @@
1
+ import { Log } from "../util/log"
2
+ import open from "open"
3
+ import { Filesystem } from "../util/filesystem"
4
+ import { Instance } from "../project/instance"
5
+ import * as prompts from "@clack/prompts"
6
+ import { UI } from "../cli/ui"
7
+ import path from "path"
8
+ import { Env } from "../env"
9
+
10
+ const log = Log.create({ service: "auth.bineric-login" })
11
+
12
+ const OAUTH_CALLBACK_PORT = 19877
13
+ const OAUTH_CALLBACK_PATH = "/auth/bineric/callback"
14
+
15
+ interface PendingAuth {
16
+ resolve: (data: { token: string }) => void
17
+ reject: (error: Error) => void
18
+ timeout: ReturnType<typeof setTimeout>
19
+ createdAt: number
20
+ isTimedOut?: boolean
21
+ }
22
+
23
+ export namespace BinericLogin {
24
+ let server: ReturnType<typeof Bun.serve> | undefined
25
+ const pendingAuths = new Map<string, PendingAuth>()
26
+
27
+ const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
28
+ const GRACE_PERIOD_MS = 3 * 60 * 1000 // 3 minutes grace period after timeout
29
+
30
+ // Minimal HTML that immediately tries to close the window
31
+ // Shows nothing visible - just a blank page that closes itself
32
+ const HTML_SUCCESS = `<!DOCTYPE html>
33
+ <html>
34
+ <head>
35
+ <title>Login Complete</title>
36
+ <meta charset="utf-8">
37
+ <style>
38
+ body {
39
+ margin: 0;
40
+ padding: 0;
41
+ background: #1a1a2e;
42
+ font-family: system-ui, -apple-system, sans-serif;
43
+ }
44
+ .msg {
45
+ display: none;
46
+ position: fixed;
47
+ top: 50%;
48
+ left: 50%;
49
+ transform: translate(-50%, -50%);
50
+ text-align: center;
51
+ color: #4ade80;
52
+ font-size: 1.2rem;
53
+ }
54
+ .msg.show { display: block; }
55
+ </style>
56
+ </head>
57
+ <body>
58
+ <div class="msg" id="msg">✓ Login successful. You can close this tab.</div>
59
+ <script>
60
+ (function() {
61
+ // Try to close immediately - multiple attempts
62
+ function tryClose() {
63
+ try { window.close(); } catch(e) {}
64
+ try { self.close(); } catch(e) {}
65
+ try { window.open('', '_self').close(); } catch(e) {}
66
+ }
67
+
68
+ // Immediate attempts
69
+ tryClose();
70
+ setTimeout(tryClose, 50);
71
+ setTimeout(tryClose, 100);
72
+ setTimeout(tryClose, 200);
73
+ setTimeout(tryClose, 500);
74
+
75
+ // After 600ms, if still open, show minimal message
76
+ setTimeout(function() {
77
+ document.getElementById('msg').classList.add('show');
78
+ }, 600);
79
+ })();
80
+ </script>
81
+ </body>
82
+ </html>`
83
+
84
+ const HTML_ERROR = (error: string) => `<!DOCTYPE html>
85
+ <html>
86
+ <head>
87
+ <title>Bineric - Login Failed</title>
88
+ <style>
89
+ body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
90
+ .container { text-align: center; padding: 2rem; }
91
+ h1 { color: #f87171; margin-bottom: 1rem; }
92
+ p { color: #aaa; }
93
+ .error { color: #fca5a5; font-family: monospace; margin-top: 1rem; padding: 1rem; background: rgba(248,113,113,0.1); border-radius: 0.5rem; }
94
+ </style>
95
+ </head>
96
+ <body>
97
+ <div class="container">
98
+ <h1>Login Failed</h1>
99
+ <p>An error occurred during login.</p>
100
+ <div class="error">${error}</div>
101
+ </div>
102
+ </body>
103
+ </html>`
104
+
105
+ export async function ensureServer(): Promise<void> {
106
+ if (server) return
107
+
108
+ const running = await isPortInUse()
109
+ if (running) {
110
+ log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
111
+ return
112
+ }
113
+
114
+ server = Bun.serve({
115
+ port: OAUTH_CALLBACK_PORT,
116
+ fetch(req) {
117
+ const url = new URL(req.url)
118
+
119
+ if (url.pathname !== OAUTH_CALLBACK_PATH) {
120
+ return new Response("Not found", { status: 404 })
121
+ }
122
+
123
+ const token = url.searchParams.get("token")
124
+ const state = url.searchParams.get("state")
125
+ const error = url.searchParams.get("error")
126
+ const errorDescription = url.searchParams.get("error_description")
127
+
128
+ log.info("received oauth callback", { hasToken: !!token, state, error })
129
+
130
+ // Enforce state parameter presence
131
+ if (!state) {
132
+ const errorMsg = "Missing required state parameter"
133
+ log.error("oauth callback missing state parameter", { url: url.toString() })
134
+ return new Response(HTML_ERROR(errorMsg), {
135
+ status: 400,
136
+ headers: { "Content-Type": "text/html" },
137
+ })
138
+ }
139
+
140
+ if (error) {
141
+ const errorMsg = errorDescription || error
142
+ if (pendingAuths.has(state)) {
143
+ const pending = pendingAuths.get(state)!
144
+ clearTimeout(pending.timeout)
145
+ pendingAuths.delete(state)
146
+ pending.reject(new Error(errorMsg))
147
+ }
148
+ return new Response(HTML_ERROR(errorMsg), {
149
+ headers: { "Content-Type": "text/html" },
150
+ })
151
+ }
152
+
153
+ if (!token) {
154
+ return new Response(HTML_ERROR("Missing token"), {
155
+ status: 400,
156
+ headers: { "Content-Type": "text/html" },
157
+ })
158
+ }
159
+
160
+ // Validate state parameter
161
+ if (!pendingAuths.has(state)) {
162
+ const errorMsg = "Invalid or expired state parameter. Please run the login command again."
163
+ log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) })
164
+ return new Response(HTML_ERROR(errorMsg), {
165
+ status: 400,
166
+ headers: { "Content-Type": "text/html" },
167
+ })
168
+ }
169
+
170
+ const pending = pendingAuths.get(state)!
171
+ const now = Date.now()
172
+ const age = now - pending.createdAt
173
+ const isAfterTimeout = age > CALLBACK_TIMEOUT_MS
174
+ const isAfterGracePeriod = age > (CALLBACK_TIMEOUT_MS + GRACE_PERIOD_MS)
175
+
176
+ // If callback came after grace period, reject it
177
+ if (isAfterGracePeriod) {
178
+ const errorMsg = "Login session expired. Please run the login command again."
179
+ log.warn("oauth callback received after grace period", { state, age: Math.floor(age / 1000) + "s" })
180
+ pendingAuths.delete(state)
181
+ return new Response(HTML_ERROR(errorMsg), {
182
+ status: 400,
183
+ headers: { "Content-Type": "text/html" },
184
+ })
185
+ }
186
+
187
+ // If callback came after timeout but within grace period, accept it with warning
188
+ if (isAfterTimeout) {
189
+ log.warn("oauth callback received after timeout but within grace period", {
190
+ state,
191
+ timeoutAge: Math.floor((age - CALLBACK_TIMEOUT_MS) / 1000) + "s"
192
+ })
193
+ // Still accept the token, but mark as timed out
194
+ pending.isTimedOut = true
195
+ }
196
+
197
+ clearTimeout(pending.timeout)
198
+ pendingAuths.delete(state)
199
+
200
+ // Try to resolve, but if promise already rejected (timeout), that's ok
201
+ // The token will still be returned and can be saved
202
+ try {
203
+ pending.resolve({ token })
204
+ } catch (e) {
205
+ // Promise already rejected (timeout), but we still have the token
206
+ // This is handled in the login function
207
+ log.info("callback received after timeout, token available for manual save", { state })
208
+ }
209
+
210
+ return new Response(HTML_SUCCESS, {
211
+ headers: { "Content-Type": "text/html" },
212
+ })
213
+ },
214
+ })
215
+
216
+ log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
217
+ }
218
+
219
+ export function waitForCallback(state: string): Promise<{ token: string }> {
220
+ return new Promise((resolve, reject) => {
221
+ const timeout = setTimeout(() => {
222
+ if (pendingAuths.has(state)) {
223
+ const pending = pendingAuths.get(state)!
224
+ // Mark as timed out but don't reject yet - wait for grace period
225
+ pending.isTimedOut = true
226
+ log.warn("login timeout reached, but keeping state for grace period", { state })
227
+
228
+ // Set another timeout for grace period
229
+ setTimeout(() => {
230
+ if (pendingAuths.has(state)) {
231
+ const pendingAfterGrace = pendingAuths.get(state)!
232
+ if (pendingAfterGrace.isTimedOut) {
233
+ // Grace period also expired, now reject
234
+ pendingAuths.delete(state)
235
+ clearTimeout(pendingAfterGrace.timeout)
236
+ pendingAfterGrace.reject(new Error("Login timeout - authorization took too long. Please run the login command again."))
237
+ }
238
+ }
239
+ }, GRACE_PERIOD_MS)
240
+ }
241
+ }, CALLBACK_TIMEOUT_MS)
242
+
243
+ pendingAuths.set(state, {
244
+ resolve,
245
+ reject,
246
+ timeout,
247
+ createdAt: Date.now()
248
+ })
249
+ })
250
+ }
251
+
252
+ export async function isPortInUse(): Promise<boolean> {
253
+ return new Promise((resolve) => {
254
+ Bun.connect({
255
+ hostname: "127.0.0.1",
256
+ port: OAUTH_CALLBACK_PORT,
257
+ socket: {
258
+ open(socket) {
259
+ socket.end()
260
+ resolve(true)
261
+ },
262
+ error() {
263
+ resolve(false)
264
+ },
265
+ data() {},
266
+ close() {},
267
+ },
268
+ }).catch(() => {
269
+ resolve(false)
270
+ })
271
+ })
272
+ }
273
+
274
+ export async function stop(): Promise<void> {
275
+ if (server) {
276
+ server.stop()
277
+ server = undefined
278
+ log.info("oauth callback server stopped")
279
+ }
280
+
281
+ for (const [state, pending] of pendingAuths) {
282
+ clearTimeout(pending.timeout)
283
+ pending.reject(new Error("OAuth callback server stopped"))
284
+ }
285
+ pendingAuths.clear()
286
+ }
287
+
288
+ /**
289
+ * Check if Bineric provider has a token (CLI token) in bincode.json
290
+ */
291
+ export async function hasApiKey(): Promise<boolean> {
292
+ try {
293
+ const configFiles = ["bincode.jsonc", "bincode.json"]
294
+ for (const file of configFiles) {
295
+ const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
296
+ for (const configPath of found) {
297
+ const config = await Bun.file(configPath).json().catch(() => null)
298
+ // Check for token (preferred) or apiKey (legacy)
299
+ const token = config?.provider?.Bineric?.options?.token
300
+ const apiKey = config?.provider?.Bineric?.options?.apiKey
301
+
302
+ if (token && token.length > 10) {
303
+ return true
304
+ }
305
+
306
+ // Legacy: check apiKey if token not found
307
+ if (apiKey && apiKey !== "not-needed" && apiKey.length > 10) {
308
+ return true
309
+ }
310
+ }
311
+ }
312
+ return false
313
+ } catch (error) {
314
+ log.error("error checking token/api key", { error })
315
+ return false
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Get the base URL from environment variable, config file, or default
321
+ * Priority: BINERIC_API_URL env var > bincode.json config > default
322
+ */
323
+ export async function getBaseUrl(): Promise<string> {
324
+ // Check environment variable first
325
+ const envUrl = Env.get("BINERIC_API_URL")
326
+ if (envUrl && envUrl.trim()) {
327
+ return envUrl.trim()
328
+ }
329
+
330
+ try {
331
+ const configFiles = ["bincode.jsonc", "bincode.json"]
332
+ for (const file of configFiles) {
333
+ const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
334
+ for (const configPath of found) {
335
+ const config = await Bun.file(configPath).json().catch(() => null)
336
+ const configUrl = config?.provider?.Bineric?.options?.baseURL
337
+ if (configUrl && configUrl.trim()) {
338
+ return configUrl.trim()
339
+ }
340
+ }
341
+ }
342
+ // No default - must be set via env var or config
343
+ throw new Error("BINERIC_API_URL environment variable or baseURL in bincode.json must be set")
344
+ } catch (error) {
345
+ log.error("error getting base url", { error })
346
+ throw new Error("BINERIC_API_URL environment variable or baseURL in bincode.json must be set")
347
+ }
348
+ }
349
+
350
+ export async function updateConfig(token: string): Promise<void> {
351
+ try {
352
+ const configFiles = ["bincode.jsonc", "bincode.json"]
353
+ let configPath: string | null = null
354
+ let config: any = null
355
+
356
+ // Find the config file
357
+ for (const file of configFiles) {
358
+ const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
359
+ if (found.length > 0) {
360
+ configPath = found[0]
361
+ config = await Bun.file(configPath).json().catch(() => null)
362
+ if (config) break
363
+ }
364
+ }
365
+
366
+ // Get baseURL from env var - required
367
+ const defaultBaseURL = Env.get("BINERIC_API_URL")
368
+ if (!defaultBaseURL || !defaultBaseURL.trim()) {
369
+ throw new Error("BINERIC_API_URL environment variable must be set")
370
+ }
371
+
372
+ // If no config found, create one in the current directory
373
+ if (!config) {
374
+ configPath = path.join(Instance.directory, "bincode.json")
375
+ config = {
376
+ $schema: "https://bincode.ai/config.json",
377
+ enabled_providers: ["Bineric"],
378
+ provider: {
379
+ Bineric: {
380
+ name: "Bineric",
381
+ npm: "@ai-sdk/openai-compatible",
382
+ options: {
383
+ baseURL: defaultBaseURL,
384
+ },
385
+ },
386
+ },
387
+ }
388
+ }
389
+
390
+ // Update the config
391
+ if (!config.provider) {
392
+ config.provider = {}
393
+ }
394
+ if (!config.provider.Bineric) {
395
+ config.provider.Bineric = {
396
+ name: "Bineric",
397
+ npm: "@ai-sdk/openai-compatible",
398
+ options: {
399
+ baseURL: defaultBaseURL,
400
+ },
401
+ }
402
+ }
403
+ if (!config.provider.Bineric.options) {
404
+ config.provider.Bineric.options = {}
405
+ }
406
+
407
+ // Store token (contains API key and email)
408
+ config.provider.Bineric.options.token = token
409
+
410
+ await Bun.write(configPath!, JSON.stringify(config, null, 2))
411
+ log.info("updated config with token", { configPath })
412
+ } catch (error) {
413
+ log.error("error updating config", { error })
414
+ throw error
415
+ }
416
+ }
417
+
418
+ export async function removeToken(): Promise<void> {
419
+ try {
420
+ const configFiles = ["bincode.jsonc", "bincode.json"]
421
+ let configPath: string | null = null
422
+ let config: any = null
423
+
424
+ for (const file of configFiles) {
425
+ const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
426
+ if (found.length > 0) {
427
+ configPath = found[0]
428
+ config = await Bun.file(configPath).json().catch(() => null)
429
+ if (config) break
430
+ }
431
+ }
432
+
433
+ if (!config || !config.provider?.Bineric?.options) {
434
+ return
435
+ }
436
+
437
+ delete config.provider.Bineric.options.token
438
+ delete config.provider.Bineric.options.email
439
+ delete config.provider.Bineric.options.apiKey
440
+
441
+ await Bun.write(configPath!, JSON.stringify(config, null, 2))
442
+ log.info("removed token from config", { configPath })
443
+ } catch (error) {
444
+ log.error("error removing token", { error })
445
+ throw error
446
+ }
447
+ }
448
+
449
+ export async function login(baseUrl?: string): Promise<{ token: string }> {
450
+ // Get frontend URL from env var - required
451
+ const apiWebUrl = Env.get("BINERIC_FRONTEND_URL") || Env.get("NEXT_PUBLIC_APIWEB_URL")
452
+ if (!apiWebUrl || !apiWebUrl.trim()) {
453
+ throw new Error("BINERIC_FRONTEND_URL environment variable must be set for Bineric login")
454
+ }
455
+ const state = crypto.randomUUID()
456
+ const callbackUrl = `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`
457
+
458
+ const loginUrl = new URL(`${apiWebUrl}/auth/cli-login`)
459
+ loginUrl.searchParams.set("redirect_uri", callbackUrl)
460
+ loginUrl.searchParams.set("state", state)
461
+
462
+ await ensureServer()
463
+
464
+ prompts.log.info("Opening browser for login...")
465
+ prompts.log.info(`Login URL: ${loginUrl.toString()}`)
466
+ prompts.log.info("If browser doesn't open automatically, copy the URL above and open it in your browser")
467
+
468
+ await open(loginUrl.toString())
469
+
470
+ const spinner = prompts.spinner()
471
+ spinner.start("Waiting for authorization...")
472
+
473
+ // Set up timeout warning
474
+ const timeoutWarning = setTimeout(() => {
475
+ if (pendingAuths.has(state)) {
476
+ spinner.message("Still waiting... You can complete login in browser (3 more minutes)")
477
+ }
478
+ }, CALLBACK_TIMEOUT_MS)
479
+
480
+ try {
481
+ const { token } = await waitForCallback(state)
482
+ clearTimeout(timeoutWarning)
483
+
484
+ if (!token) {
485
+ throw new Error("No token received from login")
486
+ }
487
+
488
+ spinner.stop("Login successful!")
489
+ return { token }
490
+ } catch (error) {
491
+ clearTimeout(timeoutWarning)
492
+ spinner.stop("Login timeout", 1)
493
+
494
+ // Check if token was saved despite timeout (callback came in grace period)
495
+ const hasToken = await hasApiKey()
496
+ if (hasToken) {
497
+ prompts.log.success("Login completed after timeout. Token has been saved successfully!")
498
+ return { token: "saved" } // Return dummy token, actual token is already saved
499
+ }
500
+
501
+ prompts.log.warn("Login timeout. If you complete login in browser now, you may need to run the login command again.")
502
+ throw error
503
+ }
504
+ }
505
+ }
506
+
@@ -0,0 +1,70 @@
1
+ import path from "path"
2
+ import { Global } from "../global"
3
+ import fs from "fs/promises"
4
+ import z from "zod"
5
+
6
+ export namespace Auth {
7
+ export const Oauth = z
8
+ .object({
9
+ type: z.literal("oauth"),
10
+ refresh: z.string(),
11
+ access: z.string(),
12
+ expires: z.number(),
13
+ enterpriseUrl: z.string().optional(),
14
+ })
15
+ .meta({ ref: "OAuth" })
16
+
17
+ export const Api = z
18
+ .object({
19
+ type: z.literal("api"),
20
+ key: z.string(),
21
+ })
22
+ .meta({ ref: "ApiAuth" })
23
+
24
+ export const WellKnown = z
25
+ .object({
26
+ type: z.literal("wellknown"),
27
+ key: z.string(),
28
+ token: z.string(),
29
+ })
30
+ .meta({ ref: "WellKnownAuth" })
31
+
32
+ export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown]).meta({ ref: "Auth" })
33
+ export type Info = z.infer<typeof Info>
34
+
35
+ const filepath = path.join(Global.Path.data, "auth.json")
36
+
37
+ export async function get(providerID: string) {
38
+ const auth = await all()
39
+ return auth[providerID]
40
+ }
41
+
42
+ export async function all(): Promise<Record<string, Info>> {
43
+ const file = Bun.file(filepath)
44
+ const data = await file.json().catch(() => ({}) as Record<string, unknown>)
45
+ return Object.entries(data).reduce(
46
+ (acc, [key, value]) => {
47
+ const parsed = Info.safeParse(value)
48
+ if (!parsed.success) return acc
49
+ acc[key] = parsed.data
50
+ return acc
51
+ },
52
+ {} as Record<string, Info>,
53
+ )
54
+ }
55
+
56
+ export async function set(key: string, info: Info) {
57
+ const file = Bun.file(filepath)
58
+ const data = await all()
59
+ await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2))
60
+ await fs.chmod(file.name!, 0o600)
61
+ }
62
+
63
+ export async function remove(key: string) {
64
+ const file = Bun.file(filepath)
65
+ const data = await all()
66
+ delete data[key]
67
+ await Bun.write(file, JSON.stringify(data, null, 2))
68
+ await fs.chmod(file.name!, 0o600)
69
+ }
70
+ }
@@ -0,0 +1,114 @@
1
+ import z from "zod"
2
+ import { Global } from "../global"
3
+ import { Log } from "../util/log"
4
+ import path from "path"
5
+ import { NamedError } from "@bincode-ai/util/error"
6
+ import { readableStreamToText } from "bun"
7
+ import { createRequire } from "module"
8
+ import { Lock } from "../util/lock"
9
+
10
+ export namespace BunProc {
11
+ const log = Log.create({ service: "bun" })
12
+ const req = createRequire(import.meta.url)
13
+
14
+ export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
15
+ log.info("running", {
16
+ cmd: [which(), ...cmd],
17
+ ...options,
18
+ })
19
+ const result = Bun.spawn([which(), ...cmd], {
20
+ ...options,
21
+ stdout: "pipe",
22
+ stderr: "pipe",
23
+ env: {
24
+ ...process.env,
25
+ ...options?.env,
26
+ BUN_BE_BUN: "1",
27
+ },
28
+ })
29
+ const code = await result.exited
30
+ const stdout = result.stdout
31
+ ? typeof result.stdout === "number"
32
+ ? result.stdout
33
+ : await readableStreamToText(result.stdout)
34
+ : undefined
35
+ const stderr = result.stderr
36
+ ? typeof result.stderr === "number"
37
+ ? result.stderr
38
+ : await readableStreamToText(result.stderr)
39
+ : undefined
40
+ log.info("done", {
41
+ code,
42
+ stdout,
43
+ stderr,
44
+ })
45
+ if (code !== 0) {
46
+ throw new Error(`Command failed with exit code ${result.exitCode}`)
47
+ }
48
+ return result
49
+ }
50
+
51
+ export function which() {
52
+ return process.execPath
53
+ }
54
+
55
+ export const InstallFailedError = NamedError.create(
56
+ "BunInstallFailedError",
57
+ z.object({
58
+ pkg: z.string(),
59
+ version: z.string(),
60
+ }),
61
+ )
62
+
63
+ export async function install(pkg: string, version = "latest") {
64
+ // Use lock to ensure only one install at a time
65
+ using _ = await Lock.write("bun-install")
66
+
67
+ const mod = path.join(Global.Path.cache, "node_modules", pkg)
68
+ const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
69
+ const parsed = await pkgjson.json().catch(async () => {
70
+ const result = { dependencies: {} }
71
+ await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2))
72
+ return result
73
+ })
74
+ if (parsed.dependencies[pkg] === version) return mod
75
+
76
+ // Build command arguments
77
+ const args = ["add", "--force", "--exact", "--cwd", Global.Path.cache, pkg + "@" + version]
78
+
79
+ // Let Bun handle registry resolution:
80
+ // - If .npmrc files exist, Bun will use them automatically
81
+ // - If no .npmrc files exist, Bun will default to https://registry.npmjs.org
82
+ // - No need to pass --registry flag
83
+ log.info("installing package using Bun's default registry resolution", {
84
+ pkg,
85
+ version,
86
+ })
87
+
88
+ await BunProc.run(args, {
89
+ cwd: Global.Path.cache,
90
+ }).catch((e) => {
91
+ throw new InstallFailedError(
92
+ { pkg, version },
93
+ {
94
+ cause: e,
95
+ },
96
+ )
97
+ })
98
+
99
+ // Resolve actual version from installed package when using "latest"
100
+ // This ensures subsequent starts use the cached version until explicitly updated
101
+ let resolvedVersion = version
102
+ if (version === "latest") {
103
+ const installedPkgJson = Bun.file(path.join(mod, "package.json"))
104
+ const installedPkg = await installedPkgJson.json().catch(() => null)
105
+ if (installedPkg?.version) {
106
+ resolvedVersion = installedPkg.version
107
+ }
108
+ }
109
+
110
+ parsed.dependencies[pkg] = resolvedVersion
111
+ await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
112
+ return mod
113
+ }
114
+ }