surfcode 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +325 -2
- package/assets/surfcode-logo.gif +0 -0
- package/dist/cli/auth.js +315 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/config.js +228 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/headless.js +355 -0
- package/dist/cli/headless.js.map +1 -0
- package/dist/cli/inspect.js +462 -0
- package/dist/cli/inspect.js.map +1 -0
- package/dist/cli/interactive.js +130 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/cli/sessions.js +170 -0
- package/dist/cli/sessions.js.map +1 -0
- package/dist/cli/sync.js +190 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/cli/util.js +345 -0
- package/dist/cli/util.js.map +1 -0
- package/dist/cli.js +485 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/agent.js +298 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/agents.js +55 -0
- package/dist/core/agents.js.map +1 -0
- package/dist/core/auth.js +107 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/catalog.js +306 -0
- package/dist/core/catalog.js.map +1 -0
- package/dist/core/chat/anthropic.js +222 -0
- package/dist/core/chat/anthropic.js.map +1 -0
- package/dist/core/chat/common.js +367 -0
- package/dist/core/chat/common.js.map +1 -0
- package/dist/core/chat/google.js +180 -0
- package/dist/core/chat/google.js.map +1 -0
- package/dist/core/chat/index.js +80 -0
- package/dist/core/chat/index.js.map +1 -0
- package/dist/core/chat/openai.js +164 -0
- package/dist/core/chat/openai.js.map +1 -0
- package/dist/core/chat/types.js +2 -0
- package/dist/core/chat/types.js.map +1 -0
- package/dist/core/clipboard.js +34 -0
- package/dist/core/clipboard.js.map +1 -0
- package/dist/core/compact.js +39 -0
- package/dist/core/compact.js.map +1 -0
- package/dist/core/config.js +211 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/delegate.js +184 -0
- package/dist/core/delegate.js.map +1 -0
- package/dist/core/effort.js +80 -0
- package/dist/core/effort.js.map +1 -0
- package/dist/core/errors.js +32 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/free.js +21 -0
- package/dist/core/free.js.map +1 -0
- package/dist/core/github.js +445 -0
- package/dist/core/github.js.map +1 -0
- package/dist/core/paths.js +51 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/prompt.js +95 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/provider.js +298 -0
- package/dist/core/provider.js.map +1 -0
- package/dist/core/session.js +159 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/sync.js +126 -0
- package/dist/core/sync.js.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/usage.js +127 -0
- package/dist/core/usage.js.map +1 -0
- package/dist/core/workspace.js +70 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/data/models.json +1 -0
- package/dist/sync/server.js +179 -0
- package/dist/sync/server.js.map +1 -0
- package/dist/tools/diff.js +21 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/files.js +227 -0
- package/dist/tools/files.js.map +1 -0
- package/dist/tools/github.js +238 -0
- package/dist/tools/github.js.map +1 -0
- package/dist/tools/index.js +34 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/search.js +231 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/shell.js +118 -0
- package/dist/tools/shell.js.map +1 -0
- package/dist/tools/todos.js +50 -0
- package/dist/tools/todos.js.map +1 -0
- package/dist/tools/webfetch.js +108 -0
- package/dist/tools/webfetch.js.map +1 -0
- package/dist/tui/App.js +2984 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/ansi.js +151 -0
- package/dist/tui/ansi.js.map +1 -0
- package/dist/tui/commands.js +61 -0
- package/dist/tui/commands.js.map +1 -0
- package/dist/tui/explorer.js +152 -0
- package/dist/tui/explorer.js.map +1 -0
- package/dist/tui/icons.js +21 -0
- package/dist/tui/icons.js.map +1 -0
- package/dist/tui/layout.js +324 -0
- package/dist/tui/layout.js.map +1 -0
- package/dist/tui/markdown.js +355 -0
- package/dist/tui/markdown.js.map +1 -0
- package/dist/tui/overlays.js +316 -0
- package/dist/tui/overlays.js.map +1 -0
- package/dist/tui/render.js +234 -0
- package/dist/tui/render.js.map +1 -0
- package/dist/tui/theme.js +148 -0
- package/dist/tui/theme.js.map +1 -0
- package/dist/tui/types.js +73 -0
- package/dist/tui/types.js.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- 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
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
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)
|
|
13
|
+
[](https://nodejs.org)
|
|
14
|
+
[](tsconfig.json)
|
|
15
|
+
[](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
|
package/dist/cli/auth.js
ADDED
|
@@ -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
|