surfcode 0.0.0-stage → 0.1.0

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 (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -2
  3. package/assets/surfcode-logo.gif +0 -0
  4. package/dist/cli/auth.js +315 -0
  5. package/dist/cli/auth.js.map +1 -0
  6. package/dist/cli/config.js +228 -0
  7. package/dist/cli/config.js.map +1 -0
  8. package/dist/cli/headless.js +355 -0
  9. package/dist/cli/headless.js.map +1 -0
  10. package/dist/cli/inspect.js +462 -0
  11. package/dist/cli/inspect.js.map +1 -0
  12. package/dist/cli/interactive.js +130 -0
  13. package/dist/cli/interactive.js.map +1 -0
  14. package/dist/cli/sessions.js +170 -0
  15. package/dist/cli/sessions.js.map +1 -0
  16. package/dist/cli/sync.js +190 -0
  17. package/dist/cli/sync.js.map +1 -0
  18. package/dist/cli/util.js +345 -0
  19. package/dist/cli/util.js.map +1 -0
  20. package/dist/cli.js +485 -0
  21. package/dist/cli.js.map +1 -0
  22. package/dist/core/agent.js +281 -0
  23. package/dist/core/agent.js.map +1 -0
  24. package/dist/core/agents.js +55 -0
  25. package/dist/core/agents.js.map +1 -0
  26. package/dist/core/auth.js +107 -0
  27. package/dist/core/auth.js.map +1 -0
  28. package/dist/core/catalog.js +306 -0
  29. package/dist/core/catalog.js.map +1 -0
  30. package/dist/core/chat/anthropic.js +222 -0
  31. package/dist/core/chat/anthropic.js.map +1 -0
  32. package/dist/core/chat/common.js +367 -0
  33. package/dist/core/chat/common.js.map +1 -0
  34. package/dist/core/chat/google.js +180 -0
  35. package/dist/core/chat/google.js.map +1 -0
  36. package/dist/core/chat/index.js +80 -0
  37. package/dist/core/chat/index.js.map +1 -0
  38. package/dist/core/chat/openai.js +164 -0
  39. package/dist/core/chat/openai.js.map +1 -0
  40. package/dist/core/chat/types.js +2 -0
  41. package/dist/core/chat/types.js.map +1 -0
  42. package/dist/core/clipboard.js +34 -0
  43. package/dist/core/clipboard.js.map +1 -0
  44. package/dist/core/compact.js +39 -0
  45. package/dist/core/compact.js.map +1 -0
  46. package/dist/core/config.js +211 -0
  47. package/dist/core/config.js.map +1 -0
  48. package/dist/core/delegate.js +184 -0
  49. package/dist/core/delegate.js.map +1 -0
  50. package/dist/core/effort.js +80 -0
  51. package/dist/core/effort.js.map +1 -0
  52. package/dist/core/errors.js +32 -0
  53. package/dist/core/errors.js.map +1 -0
  54. package/dist/core/free.js +21 -0
  55. package/dist/core/free.js.map +1 -0
  56. package/dist/core/github.js +445 -0
  57. package/dist/core/github.js.map +1 -0
  58. package/dist/core/paths.js +51 -0
  59. package/dist/core/paths.js.map +1 -0
  60. package/dist/core/prompt.js +95 -0
  61. package/dist/core/prompt.js.map +1 -0
  62. package/dist/core/provider.js +298 -0
  63. package/dist/core/provider.js.map +1 -0
  64. package/dist/core/session.js +159 -0
  65. package/dist/core/session.js.map +1 -0
  66. package/dist/core/sync.js +126 -0
  67. package/dist/core/sync.js.map +1 -0
  68. package/dist/core/types.js +2 -0
  69. package/dist/core/types.js.map +1 -0
  70. package/dist/core/usage.js +127 -0
  71. package/dist/core/usage.js.map +1 -0
  72. package/dist/core/workspace.js +70 -0
  73. package/dist/core/workspace.js.map +1 -0
  74. package/dist/data/models.json +1 -0
  75. package/dist/sync/server.js +179 -0
  76. package/dist/sync/server.js.map +1 -0
  77. package/dist/tools/diff.js +21 -0
  78. package/dist/tools/diff.js.map +1 -0
  79. package/dist/tools/files.js +227 -0
  80. package/dist/tools/files.js.map +1 -0
  81. package/dist/tools/github.js +238 -0
  82. package/dist/tools/github.js.map +1 -0
  83. package/dist/tools/index.js +34 -0
  84. package/dist/tools/index.js.map +1 -0
  85. package/dist/tools/search.js +231 -0
  86. package/dist/tools/search.js.map +1 -0
  87. package/dist/tools/shell.js +118 -0
  88. package/dist/tools/shell.js.map +1 -0
  89. package/dist/tools/todos.js +50 -0
  90. package/dist/tools/todos.js.map +1 -0
  91. package/dist/tools/webfetch.js +108 -0
  92. package/dist/tools/webfetch.js.map +1 -0
  93. package/dist/tui/App.js +3002 -0
  94. package/dist/tui/App.js.map +1 -0
  95. package/dist/tui/ansi.js +151 -0
  96. package/dist/tui/ansi.js.map +1 -0
  97. package/dist/tui/commands.js +61 -0
  98. package/dist/tui/commands.js.map +1 -0
  99. package/dist/tui/explorer.js +152 -0
  100. package/dist/tui/explorer.js.map +1 -0
  101. package/dist/tui/icons.js +21 -0
  102. package/dist/tui/icons.js.map +1 -0
  103. package/dist/tui/layout.js +324 -0
  104. package/dist/tui/layout.js.map +1 -0
  105. package/dist/tui/markdown.js +335 -0
  106. package/dist/tui/markdown.js.map +1 -0
  107. package/dist/tui/overlays.js +316 -0
  108. package/dist/tui/overlays.js.map +1 -0
  109. package/dist/tui/render.js +234 -0
  110. package/dist/tui/render.js.map +1 -0
  111. package/dist/tui/theme.js +148 -0
  112. package/dist/tui/theme.js.map +1 -0
  113. package/dist/tui/types.js +73 -0
  114. package/dist/tui/types.js.map +1 -0
  115. package/dist/version.js +3 -0
  116. package/dist/version.js.map +1 -0
  117. package/package.json +75 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Geetank
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,326 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <img src="assets/surfcode-logo.gif" alt="SurfCode" width="480">
4
+
5
+ ### Ride the code.
6
+
7
+ **A beautiful, provider-agnostic AI coding agent for your terminal.**
8
+ One keyboard-driven TUI. Any model. Your keys, your machine, no telemetry.
9
+
10
+ <br>
11
+
12
+ [![License: MIT](https://img.shields.io/badge/license-MIT-2ea043?style=for-the-badge)](LICENSE)
13
+ [![Node](https://img.shields.io/badge/node-%E2%89%A522-3c873a?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org)
14
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?style=for-the-badge&logo=typescript&logoColor=white)](tsconfig.json)
15
+ [![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4?style=for-the-badge)](CONTRIBUTING.md)
16
+
17
+ [**Install**](#-install) · [**Quick start**](#-quick-start) · [**Features**](#-features) · [**Providers**](#-any-model-any-provider) · [**Headless**](#-headless--ci) · [**Full docs**](docs/REFERENCE.md)
18
+
19
+ </div>
20
+
21
+ <br>
22
+
23
+ ---
24
+
25
+ ## ✨ Why SurfCode?
26
+
27
+ Most coding agents lock you to one vendor, one model, and one pricing page. SurfCode doesn't.
28
+
29
+ | | |
30
+ | :-- | :-- |
31
+ | 🌊 **Provider-agnostic** | OpenAI, Anthropic, Google, OpenRouter, Groq, DeepSeek, Ollama… **232 providers and 9,000+ models** from the [models.dev](https://models.dev) catalog, with an offline snapshot bundled. |
32
+ | 🛡️ **You stay in control** | Reads are free, writes and shell commands ask first. A read-only **plan mode** lets the agent look but never touch. |
33
+ | 🔒 **Local-first** | Keys, sessions and config never leave your machine. **No telemetry. No background uploads.** |
34
+ | ⚡ **Fast and calm** | Alternate-screen TUI that only repaints what changed. Replies land complete; turn on live typing with `/stream on`. |
35
+ | 🤖 **Scriptable** | `surf run` gives clean stdout, NDJSON events and deterministic exit codes, so it drops straight into pipes and CI. |
36
+
37
+ <br>
38
+
39
+ ## 📦 Install
40
+
41
+ > Requires **Node.js 22+** and a modern terminal (iTerm2, Ghostty, Kitty, WezTerm, Windows Terminal, …).
42
+
43
+ **From npm**
44
+
45
+ ```bash
46
+ npm install -g surfcode
47
+ surf --version
48
+ ```
49
+
50
+ **Straight from GitHub**
51
+
52
+ ```bash
53
+ npm install -g github:Official-SurfCode/surfcode
54
+ ```
55
+
56
+ **From source**
57
+
58
+ ```bash
59
+ git clone https://github.com/Official-SurfCode/surfcode.git
60
+ cd surfcode
61
+ npm install # also builds dist/
62
+ npm link # puts `surf` and `surfcode` on your PATH
63
+ ```
64
+
65
+ Both commands, **`surf`** and **`surfcode`**, do the same thing.
66
+
67
+ <br>
68
+
69
+ ## 🚀 Quick start
70
+
71
+ ```bash
72
+ surf
73
+ ```
74
+
75
+ The first launch opens the full-screen UI. Then:
76
+
77
+ ```text
78
+ /connect pick a provider and paste a key (ctrl+v)
79
+ /model browse models from the providers you've connected
80
+ /help every command and shortcut
81
+ ```
82
+
83
+ Prefer the shell?
84
+
85
+ ```bash
86
+ surf auth anthropic # hidden key prompt
87
+ surf models --provider openai # browse the catalog
88
+ surf run "explain src/core/agent.ts" # one-shot, no UI
89
+ ```
90
+
91
+ Already have `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` or `GEMINI_API_KEY` in your environment? SurfCode finds it and picks a sensible default model on its own.
92
+
93
+ <br>
94
+
95
+ ## 🧰 Features
96
+
97
+ <table>
98
+ <tr>
99
+ <td width="50%" valign="top">
100
+
101
+ ### 🖥️ A TUI worth living in
102
+ - Alternate screen, smooth scrolling, markdown and syntax highlighting
103
+ - **Six themes**: `aurora` · `dark` · `ember` · `forest` · `violet` · `mono`
104
+ - A resizable **file explorer dock** with live previews
105
+ - `@file` attachments and `!command` shell passthrough
106
+ - Command palette, input history, tab completion
107
+
108
+ </td>
109
+ <td width="50%" valign="top">
110
+
111
+ ### 🔧 Real tool use
112
+ - `read` · `write` · `edit` · `ls` · `glob` · `grep` · `bash` · `webfetch` · `todo_write`
113
+ - Inline diffs for every edit
114
+ - Non-interactive shell with timeouts and capped output
115
+ - Project instructions from `SURFCODE.md`, `AGENTS.md` or `CLAUDE.md`
116
+
117
+ </td>
118
+ </tr>
119
+ <tr>
120
+ <td width="50%" valign="top">
121
+
122
+ ### 🧠 Reasoning on tap
123
+ - `/effort low|medium|high|auto`, mapped to each provider's native controls
124
+ - Gemini 3 `thinkingLevel`, Gemini 2.5 budgets, Anthropic extended thinking, OpenAI `reasoning_effort`
125
+ - `/thinking` shows or hides reasoning text
126
+
127
+ </td>
128
+ <td width="50%" valign="top">
129
+
130
+ ### 🎛️ Multi-model orchestration
131
+ - `/agent add provider/model` keeps your model as the **head**
132
+ - Up to **8 worker models** run in parallel via a read-only `delegate` tool
133
+ - Workers never touch your files; the head verifies and synthesizes
134
+
135
+ </td>
136
+ </tr>
137
+ <tr>
138
+ <td width="50%" valign="top">
139
+
140
+ ### 💬 Side questions, no baggage
141
+ - `/btw <question>` answers with the conversation as context
142
+ - No tools, and it leaves nothing in the session or usage totals
143
+
144
+ </td>
145
+ <td width="50%" valign="top">
146
+
147
+ ### 📊 Honest usage and cost
148
+ - Provider-reported input, output, reasoning and cache tokens
149
+ - `/cost` and `/context` show where your budget is going
150
+ - `/compact` summarizes history to free context
151
+
152
+ </td>
153
+ </tr>
154
+ <tr>
155
+ <td width="50%" valign="top">
156
+
157
+ ### 🐙 GitHub publishing, on request
158
+ - Log in with a token, then ask in plain language
159
+ - Create a repo, upload a project, commit and push later
160
+ - Never force-pushes, never rewrites history, never deletes a repo
161
+ - Nothing is published unless you ask
162
+
163
+ </td>
164
+ <td width="50%" valign="top">
165
+
166
+ ### ☁️ Optional BYOK session sync
167
+ - Run the tiny self-hosted sync server, or bring your own HTTPS endpoint
168
+ - **Only session records sync.** API keys never leave your machine
169
+
170
+ </td>
171
+ </tr>
172
+ </table>
173
+
174
+ <br>
175
+
176
+ ## 🌊 Any model, any provider
177
+
178
+ ```text
179
+ OpenAI · Anthropic · Google · Azure OpenAI · Vertex · OpenRouter · DeepSeek
180
+ xAI · Mistral · Groq · Cerebras · Together · Fireworks · DeepInfra · Cohere
181
+ Perplexity · Moonshot · Zhipu · GitHub Copilot · Cloudflare Workers AI …and 200+ more
182
+ ```
183
+
184
+ - Providers with a permanent free API tier are tagged **`[FREE API]`** in the picker.
185
+ - **Local models just work.** Ollama, llama.cpp, vLLM and LM Studio are auto-detected when running.
186
+ - Bring anything OpenAI-compatible with a custom `baseURL`.
187
+ - `surf update-models` refreshes the catalog; the bundled snapshot is the offline fallback.
188
+
189
+ <br>
190
+
191
+ ## ⌨️ Commands you'll use daily
192
+
193
+ | Command | What it does |
194
+ | :-- | :-- |
195
+ | `/model` · `/connect` · `/provider` | Switch model, add a key, browse providers |
196
+ | `/effort` · `/thinking` · `/stream` | Reasoning depth, visible thoughts, live typing |
197
+ | `/plan` · `/build` | Toggle read-only plan mode (also `shift+tab`) |
198
+ | `/agent` | Run worker models in parallel |
199
+ | `/btw <q>` | Ask a side question without saving it |
200
+ | `/explorer` · `/theme` · `/options` | File dock, themes, settings |
201
+ | `/sessions` · `/new` · `/compact` · `/export` | Resume, restart, shrink, save as Markdown |
202
+ | `/github` | Connect GitHub and publish code |
203
+ | `/cost` · `/context` | Tokens, spend and context usage |
204
+
205
+ <details>
206
+ <summary><b>Keyboard shortcuts</b></summary>
207
+
208
+ <br>
209
+
210
+ | Key | Action |
211
+ | :-- | :-- |
212
+ | `enter` | Send prompt |
213
+ | `ctrl+j` | New line |
214
+ | `esc` | Stop generating / leave dock / close overlay |
215
+ | `ctrl+c` | Interrupt (press twice to exit) |
216
+ | `tab` | Complete a command or file |
217
+ | `shift+tab` | Toggle plan / build mode |
218
+ | `ctrl+t` | Cycle themes |
219
+ | `ctrl+o` | Focus the file dock |
220
+ | `ctrl+←` / `ctrl+→` | Resize the dock |
221
+ | `ctrl+v` | Paste from the clipboard |
222
+ | `pgup` / `pgdn` | Scroll the transcript |
223
+ | `?` | Help (when the prompt is empty) |
224
+
225
+ </details>
226
+
227
+ <br>
228
+
229
+ ## 🛡️ Safe by default
230
+
231
+ Every tool action is classified, and you decide what is allowed.
232
+
233
+ | Action | Default |
234
+ | :-- | :-- |
235
+ | `read` · `fetch` | ✅ allow |
236
+ | `write` · `shell` | ❓ ask first |
237
+ | Outside the workspace | 🚫 off |
238
+ | **Plan mode** | 🔒 read-only, nothing can mutate |
239
+
240
+ - Choose **yes**, **always** or **no** at each prompt. Deny an action for good with `"permissions": { "shell": "deny" }`.
241
+ - Destructive shell commands are flagged as high-risk in the prompt.
242
+ - `/access full` is an explicit opt-in for trusted machines. A red badge shows while it's on.
243
+ - `surf config` redacts keys, tokens and auth headers.
244
+
245
+ <br>
246
+
247
+ ## 🤖 Headless & CI
248
+
249
+ ```bash
250
+ surf run "summarize this repository"
251
+ cat error.log | surf run "explain this failure"
252
+ surf run --plan "map the auth flow before changing it"
253
+ surf run --yes "run the tests and fix failures"
254
+ surf run --json --yes "list the files" | jq -c 'select(.type == "result")'
255
+ ```
256
+
257
+ Answers stream to **stdout**, tool progress to **stderr**, so pipes stay clean. With `--json` you get one NDJSON event per line. Exit codes are `0` success, `1` error, `130` interrupted.
258
+
259
+ <br>
260
+
261
+ ## 🩺 Handy CLI
262
+
263
+ ```text
264
+ surf open the TUI
265
+ surf run [prompt] one-shot headless run
266
+ surf auth [provider] connect a provider or GitHub
267
+ surf models [query] browse the model catalog
268
+ surf providers provider status (--all for the full catalog)
269
+ surf sessions list and manage saved sessions
270
+ surf config inspect or edit configuration
271
+ surf doctor diagnose problems (--network to test connectivity)
272
+ surf update-models refresh the models.dev catalog
273
+ ```
274
+
275
+ <br>
276
+
277
+ ## 🏗️ Project layout
278
+
279
+ ```text
280
+ src/
281
+ cli.tsx command-line entry
282
+ cli/ auth, config, sessions, inspect, headless, interactive
283
+ core/ catalog, providers, chat dialects, agent loop, sessions
284
+ tools/ read, write, edit, grep, bash, webfetch, GitHub…
285
+ tui/ Ink UI, rendering, themes, overlays, file explorer
286
+ sync/ self-hosted session sync server
287
+ data/ bundled models.dev snapshot
288
+ test/ unit and CLI integration tests (mock SSE servers, no keys needed)
289
+ scripts/ catalog updates, logo rendering, asset copying
290
+ docs/ full reference
291
+ ```
292
+
293
+ <br>
294
+
295
+ ## 🤝 Contributing
296
+
297
+ Contributions are welcome. The short version:
298
+
299
+ ```bash
300
+ npm install
301
+ npm run dev # run from source
302
+ npm run typecheck
303
+ npm test # no API keys required
304
+ ```
305
+
306
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for how to add a tool or a new provider dialect.
307
+
308
+ <br>
309
+
310
+ ## 📚 Documentation
311
+
312
+ The **[full reference](docs/REFERENCE.md)** covers every setting and flag: configuration, custom endpoints, session sync, multi-model agents, GitHub publishing, JSON event schema, troubleshooting and more.
313
+
314
+ <br>
315
+
316
+ ## 📄 License
317
+
318
+ MIT © 2026 Geetank. See [LICENSE](LICENSE).
319
+
320
+ <div align="center">
321
+
322
+ <br>
323
+
324
+ **If SurfCode saves you time, a ⭐ on the repo means a lot.**
325
+
326
+ </div>
Binary file
@@ -0,0 +1,315 @@
1
+ import { credentialStatus, isKeylessProvider, mask, removeProviderKey, saveProviderKey } from '../core/auth.js';
2
+ import { loadCatalog } from '../core/catalog.js';
3
+ import { loadConfig } from '../core/config.js';
4
+ import { SurfError } from '../core/errors.js';
5
+ import { FREE_API_HINT, FREE_API_LABEL, hasFreeApi } from '../core/free.js';
6
+ import { githubTokenStatus, githubViewer } from '../core/github.js';
7
+ import { providerHasEndpoint } from '../core/provider.js';
8
+ import { accent, credentialUsable, danger, muted, oneLine, PRIORITY_PROVIDERS, providerStatusLabel, readSecret, success, table, title, warning, write } from './util.js';
9
+ const PRIORITY = new Set(PRIORITY_PROVIDERS);
10
+ export async function authCommand(options) {
11
+ const config = await loadConfig();
12
+ const catalog = await loadCatalog(config);
13
+ const providerArg = options.provider && options.provider !== 'list' ? options.provider : undefined;
14
+ if (!providerArg) {
15
+ if (options.remove || options.key || options.env) {
16
+ throw new SurfError('A provider is required for this option.', {
17
+ hint: 'Example: surf auth anthropic --key sk-ant-…'
18
+ });
19
+ }
20
+ return await listAuth(config, options);
21
+ }
22
+ if (providerArg.toLowerCase() === 'github') {
23
+ return await githubAuthCommand(options);
24
+ }
25
+ const provider = catalog.providers.find((entry) => entry.id === providerArg ||
26
+ entry.name.toLowerCase() === providerArg.toLowerCase());
27
+ if (!provider) {
28
+ throw new SurfError(`Unknown provider "${providerArg}"`, {
29
+ hint: 'Run `surf providers --all` to browse provider ids.'
30
+ });
31
+ }
32
+ const statuses = await credentialStatus([provider], config);
33
+ const status = statuses[0];
34
+ if (options.env) {
35
+ if (provider.env.length === 0) {
36
+ write(`${warning(provider.name)} does not use environment keys.`);
37
+ if (provider.local)
38
+ write(muted(' Local providers are keyless; start the server and use /connect.'));
39
+ return 0;
40
+ }
41
+ write(`${title(provider.name)} ${muted('environment variables')}`);
42
+ for (const name of provider.env) {
43
+ const value = process.env[name]?.trim();
44
+ write(` ${value ? success('●') : muted('○')} ${accent(name)}${value ? muted(' · set') : ''}`);
45
+ }
46
+ return 0;
47
+ }
48
+ if (options.remove) {
49
+ const removed = await removeProviderKey(provider.id);
50
+ if (removed) {
51
+ write(`${success('✓')} Removed the saved key for ${provider.name}.`);
52
+ }
53
+ else {
54
+ write(muted(`No saved key for ${provider.name}.`));
55
+ }
56
+ if (config.providers[provider.id]?.apiKey || config.customProviders[provider.id]?.apiKey) {
57
+ const location = config.providers[provider.id]?.apiKey
58
+ ? `providers.${provider.id}.apiKey`
59
+ : `customProviders.${provider.id}.apiKey`;
60
+ write(warning(` A key is still set in ${location} inside config.json. Remove it there too.`));
61
+ }
62
+ return 0;
63
+ }
64
+ if (options.key?.trim()) {
65
+ return await persistKey(provider, options.key, config, Boolean(options.json));
66
+ }
67
+ if (isKeylessProvider(provider)) {
68
+ write(`${success('✓')} ${provider.name} is keyless${provider.local ? ' when its local server is running' : ''}.`);
69
+ write(muted(' No API key is needed. Use `surf` and /connect, or select a model with --provider.'));
70
+ return 0;
71
+ }
72
+ if (!process.stdin.isTTY) {
73
+ throw new SurfError(`No key supplied for ${provider.name}`, {
74
+ hint: `Pass --key, or set ${provider.env[0] ?? 'the provider API key'} in the environment.`
75
+ });
76
+ }
77
+ write(`${title(provider.name)} ${muted('· paste your API key')}`);
78
+ if (provider.env[0])
79
+ write(muted(` ${provider.env[0]} works too.`));
80
+ const key = await readSecret(` key › `);
81
+ if (!key)
82
+ throw new SurfError('The key was empty.');
83
+ return await persistKey(provider, key, config, Boolean(options.json));
84
+ }
85
+ async function githubAuthCommand(options) {
86
+ const status = await githubTokenStatus();
87
+ if (options.env) {
88
+ write(`${title('GitHub')} ${muted('environment variables')}`);
89
+ for (const name of ['GITHUB_TOKEN', 'GH_TOKEN']) {
90
+ const value = process.env[name]?.trim();
91
+ write(` ${value ? success('●') : muted('○')} ${accent(name)}${value ? muted(' · set') : ''}`);
92
+ }
93
+ if (status.source === 'auth') {
94
+ write(muted(' A token is also saved in auth.json.'));
95
+ }
96
+ return 0;
97
+ }
98
+ if (options.remove) {
99
+ const removed = await removeProviderKey('github');
100
+ const remaining = await githubTokenStatus();
101
+ if (!removed && !remaining.configured) {
102
+ write(muted('No saved GitHub token.'));
103
+ return 0;
104
+ }
105
+ if (removed)
106
+ write(`${success('✓')} Removed the saved GitHub token.`);
107
+ if (remaining.configured) {
108
+ write(warning(` ${remaining.envName ?? 'An environment token'} is still set and will keep working.`));
109
+ }
110
+ else {
111
+ write(muted(' GitHub tools will ask you to connect again.'));
112
+ }
113
+ return 0;
114
+ }
115
+ if (options.key?.trim()) {
116
+ return await persistGithubKey(options.key, Boolean(options.json));
117
+ }
118
+ if (!process.stdin.isTTY) {
119
+ throw new SurfError('No GitHub token supplied', {
120
+ hint: 'Pass --key, or set GITHUB_TOKEN / GH_TOKEN in the environment.'
121
+ });
122
+ }
123
+ write(`${title('GitHub')} ${muted('· paste a personal access token')}`);
124
+ write(muted(' Classic scope: repo. Fine-grained: Contents + Administration read/write.'));
125
+ write(muted(' The token is stored in auth.json with mode 600 and is never printed in full.'));
126
+ const key = await readSecret(' token › ');
127
+ if (!key)
128
+ throw new SurfError('The token was empty.');
129
+ return await persistGithubKey(key, Boolean(options.json));
130
+ }
131
+ async function persistGithubKey(raw, json) {
132
+ const key = raw.trim();
133
+ if (!key)
134
+ throw new SurfError('The token was empty.');
135
+ if (/\s/.test(key)) {
136
+ throw new SurfError('That token contains whitespace.', {
137
+ hint: 'Copy it again without line breaks or spaces.'
138
+ });
139
+ }
140
+ const viewer = await githubViewer(key);
141
+ await saveProviderKey('github', key);
142
+ if (json) {
143
+ write(JSON.stringify({
144
+ provider: 'github',
145
+ configured: true,
146
+ source: 'auth',
147
+ login: viewer.login,
148
+ masked: mask(key)
149
+ }, null, 2));
150
+ return 0;
151
+ }
152
+ write(`${success('✓')} Connected to GitHub as ${accent(viewer.login)} ${muted(`· ${mask(key)}`)}`);
153
+ write(muted(' Ask the agent: “create a private repo for this project and upload everything”.'));
154
+ write(muted(' The agent can create repos, upload code, and commit only when you ask.'));
155
+ return 0;
156
+ }
157
+ async function persistKey(provider, raw, config, json) {
158
+ const key = raw.trim();
159
+ if (!key)
160
+ throw new SurfError('The key was empty.');
161
+ if (/\s/.test(key)) {
162
+ throw new SurfError('That key contains whitespace.', {
163
+ hint: 'Copy it again without line breaks or spaces.'
164
+ });
165
+ }
166
+ await saveProviderKey(provider.id, key);
167
+ if (json) {
168
+ write(JSON.stringify({
169
+ provider: provider.id,
170
+ configured: true,
171
+ source: 'auth',
172
+ masked: mask(key)
173
+ }, null, 2));
174
+ return 0;
175
+ }
176
+ write(`${success('✓')} Saved a key for ${provider.name} ${muted(`· ${mask(key)}`)}`);
177
+ if (!providerHasEndpoint(provider, config)) {
178
+ const location = config.customProviders[provider.id]
179
+ ? `customProviders.${provider.id}.baseURL`
180
+ : `providers.${provider.id}.baseURL`;
181
+ write(warning(` This provider still needs an endpoint. Add ${location} to the config.`));
182
+ }
183
+ const configured = (await credentialStatus([provider], config))[0];
184
+ if (configured?.configured === false) {
185
+ write(warning(' The key was saved but the provider is not ready yet.'));
186
+ }
187
+ write(muted(' Start SurfCode with `surf`, or pick a model with `surf models`.'));
188
+ return 0;
189
+ }
190
+ async function listAuth(config, options) {
191
+ const catalog = await loadCatalog(config);
192
+ const statuses = await credentialStatus(catalog.providers, config);
193
+ const github = await githubTokenStatus();
194
+ const byId = new Map(statuses.map((status) => [status.provider.id, status]));
195
+ const configured = statuses.filter((status) => status.configured);
196
+ const ready = statuses.filter((status) => credentialUsable(status, config));
197
+ const readyIds = new Set(ready.map((status) => status.provider.id));
198
+ let providers = catalog.providers.filter((provider) => {
199
+ if (options.all)
200
+ return true;
201
+ const status = byId.get(provider.id);
202
+ return (readyIds.has(provider.id) ||
203
+ Boolean(status?.configured && status.source !== 'keyless') ||
204
+ PRIORITY.has(provider.id) ||
205
+ provider.id === config.provider ||
206
+ provider.custom);
207
+ });
208
+ providers.sort((a, b) => {
209
+ const readyOrder = Number(readyIds.has(b.id)) - Number(readyIds.has(a.id));
210
+ if (readyOrder !== 0)
211
+ return readyOrder;
212
+ const priority = (PRIORITY.has(a.id) ? PRIORITY_PROVIDERS.indexOf(a.id) : PRIORITY.size) -
213
+ (PRIORITY.has(b.id) ? PRIORITY_PROVIDERS.indexOf(b.id) : PRIORITY.size);
214
+ if (priority !== 0)
215
+ return priority;
216
+ return a.name.localeCompare(b.name);
217
+ });
218
+ if (options.json) {
219
+ write(JSON.stringify({
220
+ configured: configured.length,
221
+ ready: ready.length,
222
+ total: catalog.providers.length,
223
+ github: {
224
+ configured: github.configured,
225
+ source: github.source ?? null,
226
+ envName: github.envName ?? null,
227
+ masked: github.masked ?? null
228
+ },
229
+ providers: providers.map((provider) => {
230
+ const status = byId.get(provider.id);
231
+ return {
232
+ id: provider.id,
233
+ name: provider.name,
234
+ configured: Boolean(status?.configured),
235
+ usable: status ? credentialUsable(status, config) : false,
236
+ source: status?.source ?? null,
237
+ env: provider.env,
238
+ envName: status?.envName ?? null,
239
+ local: Boolean(provider.local),
240
+ custom: Boolean(provider.custom),
241
+ freeApi: hasFreeApi(provider.id)
242
+ };
243
+ })
244
+ }, null, 2));
245
+ return 0;
246
+ }
247
+ write(`${title('Authentication')} ${muted(`· ${ready.length} ready · ${catalog.providers.length} providers`)}`);
248
+ if (ready.length === 0) {
249
+ write();
250
+ write(muted('No provider credentials found. Add one with `surf auth <provider>`.'));
251
+ write(muted('Environment variables are also supported; run `surf auth <provider> --env`.'));
252
+ }
253
+ write();
254
+ table(['Provider', 'ID', 'Status', 'Source', 'Environment'], providers.map((provider) => {
255
+ const status = byId.get(provider.id);
256
+ const ready = status ? credentialUsable(status, config) : false;
257
+ let label = providerStatusLabel(status, provider, config);
258
+ if (status?.configured && !ready) {
259
+ label = warning(providerHasEndpoint(provider, config) ? 'key saved · not ready' : 'key saved · no endpoint');
260
+ }
261
+ const source = status?.configured
262
+ ? status.source === 'auth'
263
+ ? 'auth.json'
264
+ : status.source === 'env'
265
+ ? oneLine(status.envName ?? 'environment', 20)
266
+ : (status.source ?? '—')
267
+ : '—';
268
+ return [
269
+ `${provider.name}${hasFreeApi(provider.id) ? ` ${success(FREE_API_LABEL)}` : ''}`,
270
+ provider.id,
271
+ label,
272
+ muted(source),
273
+ muted(oneLine(provider.env.join(', ') || '—', 34))
274
+ ];
275
+ }), { protectFirst: true });
276
+ if (!options.all) {
277
+ write();
278
+ write(muted('Showing configured providers and popular defaults. Use --all to list all.'));
279
+ }
280
+ write();
281
+ write(`${github.configured ? success('●') : muted('○')} ${title('GitHub')} ${muted(github.configured
282
+ ? `· connected via ${github.source === 'env' ? github.envName : 'auth.json'} · ${github.masked ?? ''}`
283
+ : '· not connected · /github login or surf auth github')}`);
284
+ if (providers.some((provider) => hasFreeApi(provider.id))) {
285
+ write(muted(`${FREE_API_LABEL} = ${FREE_API_HINT}; model availability varies.`));
286
+ }
287
+ if (ready.length > 0) {
288
+ write();
289
+ write(`${success('●')} ${muted(`Ready: ${ready.slice(0, 12).map((status) => status.provider.id).join(', ')}${ready.length > 12 ? '…' : ''}`)}`);
290
+ }
291
+ return 0;
292
+ }
293
+ export function authHelpText() {
294
+ return [
295
+ `${title('surf auth')} ${muted('[provider]')}`,
296
+ muted(' Save an API key in auth.json, or inspect environment variables.'),
297
+ '',
298
+ `${muted('examples')}`,
299
+ ` surf auth anthropic`,
300
+ ` surf auth openai --key sk-…`,
301
+ ` surf auth google --env`,
302
+ ` surf auth openrouter --remove`,
303
+ ` surf auth github`,
304
+ ` surf auth github --key ghp_…`,
305
+ '',
306
+ `${muted('GitHub uses a personal access token for repository creation and publishing.')}`,
307
+ `${muted('keys are stored with mode 600 and are never printed in full.')}`
308
+ ].join('\n');
309
+ }
310
+ export function missingKeyHint(provider) {
311
+ return provider.env.length > 0
312
+ ? `Run \`surf auth ${provider.id}\` or set ${danger(provider.env.join(' or '))}.`
313
+ : `Run \`surf auth ${provider.id}\`.`;
314
+ }
315
+ //# sourceMappingURL=auth.js.map