@matterailab/orbcode 0.1.3
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 +471 -0
- package/bin/orbcode.js +2 -0
- package/dist/api/client.js +141 -0
- package/dist/api/headers.js +14 -0
- package/dist/api/models.js +49 -0
- package/dist/api/stream.js +1 -0
- package/dist/auth/auth.js +172 -0
- package/dist/branding.js +31 -0
- package/dist/config/promptHistory.js +33 -0
- package/dist/config/settings.js +112 -0
- package/dist/core/agent.js +459 -0
- package/dist/core/events.js +1 -0
- package/dist/core/sessions.js +44 -0
- package/dist/headless.js +64 -0
- package/dist/index.js +84 -0
- package/dist/prompts/system.js +379 -0
- package/dist/tools/executors/executeCommand.js +58 -0
- package/dist/tools/executors/files.js +197 -0
- package/dist/tools/executors/listFiles.js +65 -0
- package/dist/tools/executors/searchFiles.js +104 -0
- package/dist/tools/executors/web.js +72 -0
- package/dist/tools/index.js +85 -0
- package/dist/tools/schemas/ask_followup_question.js +40 -0
- package/dist/tools/schemas/attempt_completion.js +19 -0
- package/dist/tools/schemas/browser_action.js +60 -0
- package/dist/tools/schemas/check_past_chat_memories.js +23 -0
- package/dist/tools/schemas/codebase_search.js +23 -0
- package/dist/tools/schemas/execute_command.js +31 -0
- package/dist/tools/schemas/fetch_instructions.js +20 -0
- package/dist/tools/schemas/file_edit.js +31 -0
- package/dist/tools/schemas/file_write.js +27 -0
- package/dist/tools/schemas/generate_image.js +27 -0
- package/dist/tools/schemas/index.js +29 -0
- package/dist/tools/schemas/list_code_definition_names.js +19 -0
- package/dist/tools/schemas/list_files.js +23 -0
- package/dist/tools/schemas/lsp.js +46 -0
- package/dist/tools/schemas/multi_file_edit.js +42 -0
- package/dist/tools/schemas/new_task.js +27 -0
- package/dist/tools/schemas/read_file.js +27 -0
- package/dist/tools/schemas/run_slash_command.js +23 -0
- package/dist/tools/schemas/search_files.js +27 -0
- package/dist/tools/schemas/switch_mode.js +23 -0
- package/dist/tools/schemas/update_todo_list.js +19 -0
- package/dist/tools/schemas/use_skill.js +19 -0
- package/dist/tools/schemas/web_fetch.js +19 -0
- package/dist/tools/schemas/web_search.js +19 -0
- package/dist/tools/types.js +7 -0
- package/dist/ui/App.js +569 -0
- package/dist/ui/LoginView.js +82 -0
- package/dist/ui/components/ApprovalPrompt.js +21 -0
- package/dist/ui/components/FollowupPrompt.js +45 -0
- package/dist/ui/components/Header.js +12 -0
- package/dist/ui/components/InputBox.js +220 -0
- package/dist/ui/components/ModelPicker.js +51 -0
- package/dist/ui/components/SessionPicker.js +52 -0
- package/dist/ui/components/Spinner.js +19 -0
- package/dist/ui/components/StatusBar.js +18 -0
- package/dist/ui/components/rows.js +106 -0
- package/dist/ui/markdown.js +64 -0
- package/dist/utils/diff.js +144 -0
- package/dist/utils/shell.js +19 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MatterAI
|
|
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
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
# OrbCode CLI
|
|
2
|
+
|
|
3
|
+
Agentic coding in your terminal — powered by **Axon models by MatterAI**.
|
|
4
|
+
|
|
5
|
+
OrbCode CLI is a standalone terminal port of the Orbital extension: the same Axon
|
|
6
|
+
models, the same native tool schemas, the same MatterAI auth backend — rebuilt from
|
|
7
|
+
scratch as an interactive TUI with streaming chat, live thinking display, tool
|
|
8
|
+
activity rows, edit/command approvals, and todo tracking.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Table of contents
|
|
13
|
+
|
|
14
|
+
- [Install](#install)
|
|
15
|
+
- [Updating / relinking](#updating--relinking)
|
|
16
|
+
- [Usage](#usage)
|
|
17
|
+
- [Authentication](#authentication)
|
|
18
|
+
- [Models](#models)
|
|
19
|
+
- [The TUI](#the-tui)
|
|
20
|
+
- [Slash commands](#slash-commands)
|
|
21
|
+
- [Keyboard shortcuts](#keyboard-shortcuts)
|
|
22
|
+
- [Approvals & safety](#approvals--safety)
|
|
23
|
+
- [Headless mode](#headless-mode)
|
|
24
|
+
- [Configuration](#configuration)
|
|
25
|
+
- [Architecture](#architecture)
|
|
26
|
+
- [Tools](#tools)
|
|
27
|
+
- [Agent loop](#agent-loop)
|
|
28
|
+
- [Development](#development)
|
|
29
|
+
- [Tests](#tests)
|
|
30
|
+
- [Troubleshooting](#troubleshooting)
|
|
31
|
+
- [Contributing](#contributing)
|
|
32
|
+
- [License](#license)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
Requires **Node.js >= 20**.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g @matterailab/orbcode
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then, from any project directory:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
orbcode
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
To update later: `npm update -g @matterailab/orbcode` (or re-run the install command).
|
|
51
|
+
|
|
52
|
+
### From source (development)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/MatterAIOrg/OrbCode.git
|
|
56
|
+
cd OrbCode
|
|
57
|
+
npm install
|
|
58
|
+
npm run build
|
|
59
|
+
npm link # exposes the global `orbcode` command
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Updating / relinking
|
|
63
|
+
|
|
64
|
+
`npm link` creates a **symlink** to this repo, so after pulling changes you only
|
|
65
|
+
need to rebuild — no relink required:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm run build # the linked `orbcode` command picks this up immediately
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Relink only when the package **name or bin entry changes** (e.g. the package was
|
|
72
|
+
renamed `orbitalcode` → `orbcode`):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm unlink -g orbitalcode # remove a stale link under the old name (once)
|
|
76
|
+
npm link
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The version reported by `orbcode --version` is read from `package.json` at
|
|
80
|
+
runtime — bumping the version there is all that's needed.
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
orbcode start an interactive session in the current directory
|
|
86
|
+
orbcode "<prompt>" start an interactive session with an initial prompt
|
|
87
|
+
orbcode login sign in to MatterAI (browser device flow)
|
|
88
|
+
orbcode -p "<prompt>" run a single prompt non-interactively, print only the final response
|
|
89
|
+
orbcode -p "…" --yolo non-interactive with edits/commands auto-approved
|
|
90
|
+
orbcode --model <id> use a specific model for this run (also -m)
|
|
91
|
+
orbcode --resume <id> resume a previous session by id (also -r)
|
|
92
|
+
orbcode --version print version
|
|
93
|
+
orbcode --help show help
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The TUI always takes over the full terminal screen on launch (prior shell
|
|
97
|
+
output stays in scrollback).
|
|
98
|
+
|
|
99
|
+
The directory you launch from becomes the **workspace directory**: the default
|
|
100
|
+
target for file operations and commands, and the file listing the model sees in
|
|
101
|
+
its environment details.
|
|
102
|
+
|
|
103
|
+
## Authentication
|
|
104
|
+
|
|
105
|
+
Browser-based device flow with polling (no copy/paste needed):
|
|
106
|
+
|
|
107
|
+
1. `orbcode login` (or `/login` in the TUI) calls `POST /orbcode/auth/start` on
|
|
108
|
+
the MatterAI backend, which issues a one-time 48-hex **device code**
|
|
109
|
+
(10-minute lifetime, stored in redis).
|
|
110
|
+
2. The CLI opens
|
|
111
|
+
`https://app.matterai.so/orbital?loginType=orbcode&devicecode=<code>` in your
|
|
112
|
+
browser:
|
|
113
|
+
- **Already signed in** → the webapp shows the **Authorize OrbCode CLI**
|
|
114
|
+
dialog immediately.
|
|
115
|
+
- **Not signed in** → you're redirected to sign-in first. The `devicecode`
|
|
116
|
+
query param is preserved through the OAuth state (Google/Microsoft) and the
|
|
117
|
+
email/password path, so the authorize dialog appears right after sign-in.
|
|
118
|
+
3. Clicking **Authorize** binds your session token to the device code
|
|
119
|
+
(`POST /orbcode/auth/authorize`).
|
|
120
|
+
4. Meanwhile the CLI polls `GET /orbcode/auth/poll?devicecode=…` (every 3s by
|
|
121
|
+
default, bounded by the code's lifetime). The token is handed out **exactly
|
|
122
|
+
once** — the redis key is deleted on first successful poll.
|
|
123
|
+
5. The CLI verifies the token against the profile endpoint and saves it.
|
|
124
|
+
|
|
125
|
+
Fallbacks & overrides:
|
|
126
|
+
|
|
127
|
+
- **settings.json key**: set `apiKey` in `~/.orbcode/settings.json` (or a
|
|
128
|
+
project's `.orbcode/settings.json`) to skip login. The login screen itself is
|
|
129
|
+
browser-redirect only.
|
|
130
|
+
- **Env token**: set `ORBCODE_TOKEN` to skip login entirely (takes precedence
|
|
131
|
+
over everything).
|
|
132
|
+
- **Dev endpoints**: `ORBCODE_BACKEND_URL` (default `https://api.matterai.so`)
|
|
133
|
+
and `ORBCODE_APP_URL` (default `https://app.matterai.so`) override where the
|
|
134
|
+
device flow points — useful against a local backend/webapp.
|
|
135
|
+
- Tokens are MatterAI JWTs. A token whose payload has `env: "development"`
|
|
136
|
+
automatically routes API calls to `http://localhost:3000`, matching the
|
|
137
|
+
extension's behavior.
|
|
138
|
+
|
|
139
|
+
Sign out with `/logout` (removes the saved token).
|
|
140
|
+
|
|
141
|
+
## Models
|
|
142
|
+
|
|
143
|
+
The two Axon models are built in; `/model` opens a scroll-and-select picker
|
|
144
|
+
(`/model <id>` still selects directly). Additional models can be declared via
|
|
145
|
+
`customModels` in settings.json. The choice persists across sessions.
|
|
146
|
+
|
|
147
|
+
| id | context | max output | pricing |
|
|
148
|
+
|---------------------|---------|------------|--------------------------|
|
|
149
|
+
| `axon-code-2-5-pro` | 400k | 64k | $2/M in · $6/M out |
|
|
150
|
+
| `axon-code-2-5-mini`| 400k | 64k | free |
|
|
151
|
+
|
|
152
|
+
`axon-code-2-5-pro` is the default. Both support native JSON tool calls and
|
|
153
|
+
image input. Cost comes from the API's usage chunks (`is_byok`-aware) and is
|
|
154
|
+
shown in the status bar.
|
|
155
|
+
|
|
156
|
+
## The TUI
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
___ _ ____ _
|
|
160
|
+
/ _ \ _ __ | |__ / ___| ___ __| | ___
|
|
161
|
+
| | | || '__|| '_ \ | | / _ \ / _` | / _ \
|
|
162
|
+
| |_| || | | |_) || |___ | (_) || (_| || __/
|
|
163
|
+
\___/ |_| |_.__/ \____| \___/ \__,_| \___|
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- **Streaming responses** rendered as markdown (headers, lists, code fences,
|
|
167
|
+
inline code, links) via a lightweight ANSI renderer.
|
|
168
|
+
- **Thinking**: reasoning streams live under `✦ Thinking…` (last few lines,
|
|
169
|
+
dimmed) and collapses to `✦ Thought for Ns` when done. `ctrl+o` toggles
|
|
170
|
+
expanded thinking for subsequent turns. Reasoning arrives from the API as
|
|
171
|
+
`reasoning`/`reasoning_content` deltas or inline `<think>…</think>` blocks —
|
|
172
|
+
all are routed to the thinking display.
|
|
173
|
+
- **Tool rows**: each tool call shows a formatted name ("Read File", "Execute
|
|
174
|
+
Command"…), one-line summary (file path, command, query…), live "running"
|
|
175
|
+
state, then `✓`/`✗` with a short result preview.
|
|
176
|
+
- **Edit diffs**: file-modifying tools render a real diff — stats header
|
|
177
|
+
("Added 2 lines, removed 1 line"), line-number gutter, red/green backgrounds —
|
|
178
|
+
both in the approval prompt (before anything is written) and in the finished
|
|
179
|
+
tool row.
|
|
180
|
+
- **Tasks**: the model maintains a checklist via `update_todo_list`; it renders
|
|
181
|
+
as a compact Tasks panel (`□` pending / `◧` in progress / `■` done).
|
|
182
|
+
- **@-references**: type `@` in the input to fuzzy-search workspace files;
|
|
183
|
+
↑/↓ to choose, enter/tab inserts the top/selected match into the prompt.
|
|
184
|
+
- **Followup questions**: `ask_followup_question` renders a selectable menu
|
|
185
|
+
(arrow keys, number quick-pick, or free-text answer).
|
|
186
|
+
- **Completion**: `attempt_completion` renders a bordered "✔ Task completed"
|
|
187
|
+
card with the result.
|
|
188
|
+
- **Status bar**: approval mode (`⏵⏵ accept edits on`, shift+tab to cycle),
|
|
189
|
+
busy state, model name, context token usage, and session cost.
|
|
190
|
+
- **Input box**: top/bottom rule borders with a `❯` prompt, history (↑/↓),
|
|
191
|
+
cursor movement (←/→, ctrl+a/e), kill line (ctrl+u), multi-char paste (a
|
|
192
|
+
trailing newline submits), and a slash-command autocomplete menu when the
|
|
193
|
+
line starts with `/`. Every menu in the CLI (slash commands, @-files, model
|
|
194
|
+
picker, session picker, followups) is navigable with ↑/↓ and selectable with
|
|
195
|
+
enter; a partial command like `/mod` + enter runs the highlighted match.
|
|
196
|
+
|
|
197
|
+
## Slash commands
|
|
198
|
+
|
|
199
|
+
| command | action |
|
|
200
|
+
|------------|--------|
|
|
201
|
+
| `/help` | list commands |
|
|
202
|
+
| `/model` | scrollable model picker (`/model pro` / `/model mini` / full id selects directly) |
|
|
203
|
+
| `/clear` | clear the screen only, like the terminal's `clear` — the conversation and context continue |
|
|
204
|
+
| `/new` | start a fresh conversation/session with a clean slate |
|
|
205
|
+
| `/resume` | pick a previous session for this directory and continue it (screen is cleared, conversation replayed) |
|
|
206
|
+
| `/analytics` | open the MatterAI analytics dashboard (app.matterai.so/orbital) in the browser |
|
|
207
|
+
| `/compact` | summarize the conversation and replace history with the summary |
|
|
208
|
+
| `/tasks` | print the current task list |
|
|
209
|
+
| `/status` | version, model, account, gateway, context usage, cost, approval modes |
|
|
210
|
+
| `/cost` | show session cost and fetch account balance |
|
|
211
|
+
| `/init` | analyze the codebase and create/improve `AGENTS.md` |
|
|
212
|
+
| `/login` | start the browser sign-in flow |
|
|
213
|
+
| `/logout` | remove the saved token |
|
|
214
|
+
| `/version` | print the CLI version |
|
|
215
|
+
| `/exit` | quit |
|
|
216
|
+
|
|
217
|
+
## Keyboard shortcuts
|
|
218
|
+
|
|
219
|
+
| key | action |
|
|
220
|
+
|-----|--------|
|
|
221
|
+
| `Esc` | interrupt the running turn (or cancel login polling / close a menu) |
|
|
222
|
+
| `Ctrl+C` | quit |
|
|
223
|
+
| `Ctrl+O` | toggle thinking display for the whole transcript (past turns included) |
|
|
224
|
+
| `Shift+Tab` | cycle approval mode: ask → accept edits → auto-approve |
|
|
225
|
+
| `↑` / `↓` | input history, or navigate any open menu |
|
|
226
|
+
| `Ctrl+A` / `Ctrl+E` | start / end of line |
|
|
227
|
+
| `Ctrl+U` | clear the input line |
|
|
228
|
+
|
|
229
|
+
## Approvals & safety
|
|
230
|
+
|
|
231
|
+
Read-only tools (read/list/search/web/todos) run without prompting. Mutating
|
|
232
|
+
tools prompt first:
|
|
233
|
+
|
|
234
|
+
- **File edits** (`file_edit`, `multi_file_edit`, `file_write`) — prompt shows
|
|
235
|
+
the target; `y` allow once, `n` deny, `a` allow for the rest of the session.
|
|
236
|
+
- **Commands** (`execute_command`) — prompt shows the exact command line. The
|
|
237
|
+
model classifies commands with an `isDangerous` flag; dangerous commands
|
|
238
|
+
(deletes, force-pushes, system changes…) can **never** be auto-approved — no
|
|
239
|
+
`a` option, and `--yolo`/session-approval don't apply.
|
|
240
|
+
|
|
241
|
+
A denial is reported back to the model as "The user denied this operation." so
|
|
242
|
+
it can adjust course rather than fail.
|
|
243
|
+
|
|
244
|
+
## Headless mode
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
orbcode -p "explain the build pipeline in this repo"
|
|
248
|
+
orbcode -p "fix the lint errors" --yolo
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Prints **only the final content** to stdout (the completion result, or the last
|
|
252
|
+
assistant message) — no tool activity, no intermediate text. Errors go to
|
|
253
|
+
stderr. Without `--yolo`, edit/command approvals are auto-denied (read-only
|
|
254
|
+
analysis). Followup questions are auto-answered with "proceed with best
|
|
255
|
+
judgment".
|
|
256
|
+
|
|
257
|
+
## Configuration
|
|
258
|
+
|
|
259
|
+
Two kinds of files under `~/.orbcode/`:
|
|
260
|
+
|
|
261
|
+
- **`config.json`** — state written by the app itself (login token, chosen
|
|
262
|
+
model, approval defaults). Created on first save, mode 0600.
|
|
263
|
+
- **`settings.json`** — user-managed configuration, Claude-Code style. Created
|
|
264
|
+
automatically as an empty `{}` on first run so it's easy to find. A
|
|
265
|
+
project-level `.orbcode/settings.json` in the working directory layers on
|
|
266
|
+
top of the user-level file.
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
{
|
|
270
|
+
"apiKey": "<token used instead of logging in>",
|
|
271
|
+
"baseUrl": "https://my-gateway.example.com/v1",
|
|
272
|
+
"model": "my-custom-model",
|
|
273
|
+
"autoApproveEdits": false,
|
|
274
|
+
"autoApproveSafeCommands": false,
|
|
275
|
+
"customModels": [
|
|
276
|
+
{
|
|
277
|
+
"id": "my-custom-model",
|
|
278
|
+
"name": "My Custom Model",
|
|
279
|
+
"contextWindow": 128000,
|
|
280
|
+
"maxOutputTokens": 32000,
|
|
281
|
+
"inputPrice": 0.000001,
|
|
282
|
+
"outputPrice": 0.000002
|
|
283
|
+
}
|
|
284
|
+
],
|
|
285
|
+
"env": { "MY_VAR": "value" }
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
All keys are optional. `customModels` entries appear in the `/model` picker
|
|
290
|
+
alongside the built-in Axon models; `baseUrl` points the chat client at any
|
|
291
|
+
OpenAI-compatible gateway; `env` is applied to the process at startup.
|
|
292
|
+
Precedence: env vars > project settings.json > user settings.json >
|
|
293
|
+
config.json.
|
|
294
|
+
|
|
295
|
+
Sessions are stored in `~/.orbcode/sessions/<id>.json` and power `/resume`
|
|
296
|
+
and `--resume <id>`.
|
|
297
|
+
|
|
298
|
+
| env var | effect |
|
|
299
|
+
|---------|--------|
|
|
300
|
+
| `ORBCODE_TOKEN` | auth token (overrides everything) |
|
|
301
|
+
| `ORBCODE_API_KEY` | same as `apiKey` in settings.json |
|
|
302
|
+
| `ORBCODE_BASE_URL` | same as `baseUrl` in settings.json |
|
|
303
|
+
| `ORBCODE_MODEL` | model override (what `--model` sets internally) |
|
|
304
|
+
| `ORBCODE_CONFIG_DIR` | config directory (default `~/.orbcode`) |
|
|
305
|
+
| `ORBCODE_BACKEND_URL` | device-auth backend (default `https://api.matterai.so`) |
|
|
306
|
+
| `ORBCODE_APP_URL` | webapp for the authorize page (default `https://app.matterai.so`) |
|
|
307
|
+
|
|
308
|
+
`autoApproveEdits` / `autoApproveSafeCommands` set the session defaults for the
|
|
309
|
+
approval prompts (dangerous commands still always prompt); shift+tab cycles
|
|
310
|
+
them at runtime.
|
|
311
|
+
|
|
312
|
+
## Architecture
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
src/
|
|
316
|
+
index.tsx entry: arg parsing, interactive vs -p (headless) mode
|
|
317
|
+
branding.ts product name, logo, colors; VERSION read from package.json
|
|
318
|
+
headless.ts non-interactive -p runner
|
|
319
|
+
config/settings.ts load/save ~/.orbcode/config.json
|
|
320
|
+
auth/auth.ts device flow (start/poll), JWT→backend-URL mapping,
|
|
321
|
+
profile/balance fetch, token verification
|
|
322
|
+
api/
|
|
323
|
+
models.ts the two Axon models (ported from the extension registry)
|
|
324
|
+
client.ts OpenAI-compatible streaming client → api2.matterai.so/v1/web
|
|
325
|
+
stream.ts chunk model: text / reasoning / native_tool_calls / usage
|
|
326
|
+
headers.ts X-AxonCode-Version, X-AxonCode-TaskId, X-AXON-REPO, …
|
|
327
|
+
prompts/system.ts system prompt: agent roleDefinition + tool guide (ported
|
|
328
|
+
verbatim from the extension) + CLI system-info section
|
|
329
|
+
tools/
|
|
330
|
+
schemas/ native-tools JSON schemas, copied verbatim from the extension
|
|
331
|
+
executors/ CLI implementations (fs, child_process, search, web)
|
|
332
|
+
index.ts dispatch, approval classification, call summaries
|
|
333
|
+
core/
|
|
334
|
+
agent.ts the agent loop (see below)
|
|
335
|
+
events.ts AgentEvent model consumed by the UI
|
|
336
|
+
ui/
|
|
337
|
+
App.tsx main Ink app: static finalized rows + dynamic streaming area
|
|
338
|
+
LoginView.tsx device-flow login screen with paste fallback
|
|
339
|
+
components/ Header, InputBox, rows, ApprovalPrompt, FollowupPrompt,
|
|
340
|
+
Spinner, StatusBar
|
|
341
|
+
markdown.ts markdown → ANSI renderer
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Streaming** faithfully ports the extension's handler quirks: cumulative
|
|
345
|
+
content dedup (some backends re-send full content), `<think>` blocks routed to
|
|
346
|
+
reasoning, both `reasoning` and `reasoning_content` delta fields, tool-call
|
|
347
|
+
fragments accumulated by index (id/name in the first delta, argument chunks in
|
|
348
|
+
the rest), and cost taken from the final usage chunk.
|
|
349
|
+
|
|
350
|
+
**Requests** carry the extension-compatible headers (`X-Title`,
|
|
351
|
+
`X-AxonCode-Version`, `User-Agent: orbcode-cli/<version>`, per-task
|
|
352
|
+
`X-AxonCode-TaskId`, and `X-AXON-REPO` set from the git remote or folder name).
|
|
353
|
+
|
|
354
|
+
**Task titles**: after the first turn, the backend-generated task title is
|
|
355
|
+
fetched once per task from `/axoncode/meta/<taskId>` (with retries, like the
|
|
356
|
+
extension). It shows in the status bar, is written into the session file (so
|
|
357
|
+
`/resume` lists real titles), and becomes the terminal window title:
|
|
358
|
+
`<title> (orbcode)`.
|
|
359
|
+
|
|
360
|
+
**Usage data**: `/status` and `/cost` fetch `/axoncode/profile` and show the
|
|
361
|
+
plan, usage percentage (used/remaining), remaining reviews, and the credits
|
|
362
|
+
reset date — the same data as the extension's profile view.
|
|
363
|
+
|
|
364
|
+
## Tools
|
|
365
|
+
|
|
366
|
+
Active in the CLI (schemas byte-identical to the extension's `native-tools`):
|
|
367
|
+
|
|
368
|
+
| tool | executor notes |
|
|
369
|
+
|------|----------------|
|
|
370
|
+
| `read_file` | line-numbered output (`LINE\|content`, 6-char pad), 1000-line cap, offset/limit |
|
|
371
|
+
| `file_edit` | single replacement; unique-match enforcement; `replace_all`; empty `old_string` = whole file |
|
|
372
|
+
| `multi_file_edit` | batched edits grouped per file, per-edit OK/FAILED results |
|
|
373
|
+
| `file_write` | creates parent dirs, full-content writes |
|
|
374
|
+
| `list_files` | optional recursive, ignores node_modules/.git/build dirs, 800-entry cap |
|
|
375
|
+
| `search_files` | JS regex search with glob `file_pattern` (picomatch), 300-match cap, binary skip |
|
|
376
|
+
| `execute_command` | user's shell, 120s timeout, 30k output cap, optional cwd |
|
|
377
|
+
| `web_search` / `web_fetch` | proxied through the MatterAI backend with your token |
|
|
378
|
+
| `update_todo_list` | drives the TUI todo panel |
|
|
379
|
+
| `ask_followup_question` | interactive menu in the TUI |
|
|
380
|
+
| `attempt_completion` | ends the turn with a completion card |
|
|
381
|
+
|
|
382
|
+
Present in `tools/schemas/` but **inactive** (need IDE services): `codebase_search`,
|
|
383
|
+
`lsp`, `list_code_definition_names`, `use_skill`, `check_past_chat_memories`,
|
|
384
|
+
`browser_action`, `generate_image`, `new_task`, `switch_mode`,
|
|
385
|
+
`fetch_instructions`, `run_slash_command`.
|
|
386
|
+
|
|
387
|
+
## Agent loop
|
|
388
|
+
|
|
389
|
+
Per user message (`core/agent.ts`):
|
|
390
|
+
|
|
391
|
+
1. The first message is prefixed with `<environment_details>` (workspace file
|
|
392
|
+
listing, git branch/status, time); every user message is wrapped in
|
|
393
|
+
`<user_query>` tags, matching the extension's prompt contract.
|
|
394
|
+
2. Stream a completion (system prompt + history + tool schemas, temperature
|
|
395
|
+
0.2). Text/reasoning deltas are forwarded to the UI as they arrive;
|
|
396
|
+
tool-call fragments are accumulated.
|
|
397
|
+
3. If the model made tool calls: each one is summarized, approval is requested
|
|
398
|
+
when required, the executor runs, and the result is appended as a
|
|
399
|
+
`role: "tool"` message. `ask_followup_question` blocks on the user's answer;
|
|
400
|
+
`attempt_completion` ends the turn.
|
|
401
|
+
4. Repeat (max 50 steps per turn) until a plain text response or completion.
|
|
402
|
+
|
|
403
|
+
`Esc` aborts the in-flight request via `AbortController`; the interruption is
|
|
404
|
+
recorded in the conversation as a `<system_reminder>` so the model knows.
|
|
405
|
+
|
|
406
|
+
## Development
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
npm run dev # run from source (tsx)
|
|
410
|
+
npm run build # compile to dist/
|
|
411
|
+
npm run typecheck # tsc --noEmit
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Source-of-truth rule: behavior is **ported from the Orbital extension repo**
|
|
415
|
+
(tool schemas under `src/core/prompts/tools/native-tools`, prompts in
|
|
416
|
+
`src/core/prompts/`, models in `src/api/providers/kilocode-models.ts`) — keep
|
|
417
|
+
schemas byte-identical rather than editing them here.
|
|
418
|
+
|
|
419
|
+
Backend/web pieces of the device-auth flow live in:
|
|
420
|
+
- `gravity-console-backend` → `src/controller/orbcodeAuthController.ts`
|
|
421
|
+
(+ OAuth state in `router.ts`, callbacks in `authController.ts`)
|
|
422
|
+
- `gravity-console-webapp` → authorize dialog in `src/App.js`, sign-in q-p
|
|
423
|
+
preservation in `src/layouts/authentication/sign-up/index.js`
|
|
424
|
+
|
|
425
|
+
## Tests
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
node test-ui.mjs # in-process TUI test with a fake TTY
|
|
429
|
+
node test-device-auth.mjs # device-auth polling flow against a local mock
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
- `test-ui.mjs` drives the real App (ink-testing-library technique): header,
|
|
433
|
+
slash menu, `/help`, `/model` switching, message submission, and a **live**
|
|
434
|
+
round-trip to the API gateway — the bundled fake token yields a clean 401
|
|
435
|
+
error row. Self-contained: writes its own config fixture to
|
|
436
|
+
`/tmp/orbcode-test-config`.
|
|
437
|
+
- `test-device-auth.mjs` spins up a local HTTP mock of the backend endpoints
|
|
438
|
+
and verifies: code issuance, pending polls, authorization, one-time token
|
|
439
|
+
pickup, and expiry semantics.
|
|
440
|
+
|
|
441
|
+
## Troubleshooting
|
|
442
|
+
|
|
443
|
+
- **`orbcode: command not found`** — run `npm link` in this repo; check
|
|
444
|
+
`npm prefix -g`'s bin dir is on your PATH.
|
|
445
|
+
- **`--version` shows an old version** — rebuild (`npm run build`); the linked
|
|
446
|
+
command runs `dist/`, and the version is read from `package.json` at runtime.
|
|
447
|
+
- **Stale link after the rename** — `npm unlink -g orbitalcode && npm link`.
|
|
448
|
+
- **Login times out** — the device code lives 10 minutes; press Enter to retry,
|
|
449
|
+
or paste a token manually. Against a local stack, set `ORBCODE_BACKEND_URL`
|
|
450
|
+
and `ORBCODE_APP_URL`.
|
|
451
|
+
- **401 on chat** — token expired: `/logout` then `/login`.
|
|
452
|
+
- **Keyboard input does nothing** — OrbCode needs a real TTY (raw mode); it
|
|
453
|
+
won't accept piped stdin. Use `-p` for non-interactive runs.
|
|
454
|
+
- **`EPERM: operation not permitted` opening `bin/orbcode.js` on macOS** —
|
|
455
|
+
the repo lives in a protected folder (Documents, Desktop, Downloads) and the
|
|
456
|
+
terminal app hasn't been granted access to it. Allow it in System Settings →
|
|
457
|
+
Privacy & Security → Files and Folders (or Full Disk Access) for that
|
|
458
|
+
terminal, then restart it. Terminals that already prompted for access (e.g.
|
|
459
|
+
iTerm) keep working. A normal global install (`npm install -g @matterailab/orbcode`) is
|
|
460
|
+
unaffected because it lives outside protected folders.
|
|
461
|
+
|
|
462
|
+
## Contributing
|
|
463
|
+
|
|
464
|
+
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for how to
|
|
465
|
+
set up a development environment, run the tests, and submit a pull request.
|
|
466
|
+
Bug reports and feature requests go to
|
|
467
|
+
[GitHub Issues](https://github.com/MatterAIOrg/OrbCode/issues).
|
|
468
|
+
|
|
469
|
+
## License
|
|
470
|
+
|
|
471
|
+
[MIT](LICENSE) © MatterAI
|
package/bin/orbcode.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { API_GATEWAY_PATH, getUrlFromToken } from "../auth/auth.js";
|
|
3
|
+
import { DEFAULT_HEADERS, X_AXONCODE_TASKID, X_AXON_REPO, X_ORGANIZATIONID } from "./headers.js";
|
|
4
|
+
import { getModel } from "./models.js";
|
|
5
|
+
export class AxonClient {
|
|
6
|
+
client;
|
|
7
|
+
options;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.client = new OpenAI({
|
|
11
|
+
baseURL: options.baseUrl || getUrlFromToken(API_GATEWAY_PATH, options.token),
|
|
12
|
+
apiKey: options.token,
|
|
13
|
+
defaultHeaders: DEFAULT_HEADERS,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
requestHeaders() {
|
|
17
|
+
const headers = { [X_AXONCODE_TASKID]: this.options.taskId };
|
|
18
|
+
if (this.options.organizationId)
|
|
19
|
+
headers[X_ORGANIZATIONID] = this.options.organizationId;
|
|
20
|
+
if (this.options.repo)
|
|
21
|
+
headers[X_AXON_REPO] = this.options.repo;
|
|
22
|
+
return headers;
|
|
23
|
+
}
|
|
24
|
+
async *createMessage(systemPrompt, messages, tools, abortSignal) {
|
|
25
|
+
const model = getModel(this.options.modelId);
|
|
26
|
+
const requestOptions = {
|
|
27
|
+
model: model.id,
|
|
28
|
+
temperature: 0.2,
|
|
29
|
+
messages: [{ role: "system", content: systemPrompt }, ...messages],
|
|
30
|
+
stream: true,
|
|
31
|
+
stream_options: { include_usage: true },
|
|
32
|
+
max_tokens: model.maxOutputTokens,
|
|
33
|
+
};
|
|
34
|
+
if (tools.length > 0) {
|
|
35
|
+
requestOptions.tools = tools;
|
|
36
|
+
requestOptions.tool_choice = "auto";
|
|
37
|
+
requestOptions.parallel_tool_calls = true;
|
|
38
|
+
}
|
|
39
|
+
const stream = await this.client.chat.completions.create(requestOptions, {
|
|
40
|
+
headers: this.requestHeaders(),
|
|
41
|
+
signal: abortSignal,
|
|
42
|
+
});
|
|
43
|
+
let lastUsage;
|
|
44
|
+
let inferenceProvider;
|
|
45
|
+
let fullContent = "";
|
|
46
|
+
let isThinking = false;
|
|
47
|
+
for await (const chunk of stream) {
|
|
48
|
+
// The gateway may return an error object instead of throwing.
|
|
49
|
+
if ("error" in chunk) {
|
|
50
|
+
const error = chunk.error;
|
|
51
|
+
const err = new Error(`Axon API Error ${error?.code ?? ""}: ${error?.message ?? "unknown error"}`);
|
|
52
|
+
err.status = error?.code;
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
if ("provider" in chunk && typeof chunk.provider === "string") {
|
|
56
|
+
inferenceProvider = chunk.provider;
|
|
57
|
+
}
|
|
58
|
+
if (chunk.usage) {
|
|
59
|
+
lastUsage = chunk.usage;
|
|
60
|
+
}
|
|
61
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
62
|
+
if (!delta)
|
|
63
|
+
continue;
|
|
64
|
+
if (delta.content) {
|
|
65
|
+
// Some backends re-send cumulative content; only forward the new suffix.
|
|
66
|
+
let newText = delta.content;
|
|
67
|
+
if (fullContent && newText.startsWith(fullContent)) {
|
|
68
|
+
newText = newText.substring(fullContent.length);
|
|
69
|
+
}
|
|
70
|
+
fullContent = delta.content;
|
|
71
|
+
if (newText) {
|
|
72
|
+
// <think>…</think> blocks embedded in content are reasoning.
|
|
73
|
+
if (newText.includes("<think>")) {
|
|
74
|
+
isThinking = true;
|
|
75
|
+
}
|
|
76
|
+
if (newText.includes("<think>") || newText.includes("</think>") || isThinking) {
|
|
77
|
+
if (newText.includes("</think>")) {
|
|
78
|
+
isThinking = false;
|
|
79
|
+
}
|
|
80
|
+
yield { type: "reasoning", text: newText };
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
yield { type: "text", text: newText };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Models report reasoning under either key.
|
|
88
|
+
const deltaRecord = delta;
|
|
89
|
+
if (typeof deltaRecord.reasoning === "string" && deltaRecord.reasoning) {
|
|
90
|
+
yield { type: "reasoning", text: deltaRecord.reasoning };
|
|
91
|
+
}
|
|
92
|
+
if (typeof deltaRecord.reasoning_content === "string" && deltaRecord.reasoning_content) {
|
|
93
|
+
yield { type: "reasoning", text: deltaRecord.reasoning_content };
|
|
94
|
+
}
|
|
95
|
+
if (delta.tool_calls && delta.tool_calls.length > 0) {
|
|
96
|
+
const validToolCalls = delta.tool_calls
|
|
97
|
+
.filter((tc) => tc.function)
|
|
98
|
+
.filter((tc) => {
|
|
99
|
+
// First delta carries id + name; later deltas carry only
|
|
100
|
+
// index + argument fragments. Drop pure placeholders.
|
|
101
|
+
const hasValidId = tc.id !== null && tc.id !== undefined;
|
|
102
|
+
const hasValidName = !!tc.function.name;
|
|
103
|
+
const hasArguments = typeof tc.function.arguments === "string" && tc.function.arguments.length > 0;
|
|
104
|
+
return hasValidId || hasValidName || hasArguments;
|
|
105
|
+
})
|
|
106
|
+
.map((tc) => ({
|
|
107
|
+
index: tc.index,
|
|
108
|
+
id: tc.id,
|
|
109
|
+
type: tc.type,
|
|
110
|
+
function: {
|
|
111
|
+
name: tc.function.name || "",
|
|
112
|
+
arguments: tc.function.arguments || "",
|
|
113
|
+
},
|
|
114
|
+
}));
|
|
115
|
+
if (validToolCalls.length > 0) {
|
|
116
|
+
yield { type: "native_tool_calls", toolCalls: validToolCalls };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (lastUsage) {
|
|
121
|
+
yield {
|
|
122
|
+
type: "usage",
|
|
123
|
+
inputTokens: lastUsage.prompt_tokens || 0,
|
|
124
|
+
outputTokens: lastUsage.completion_tokens || 0,
|
|
125
|
+
cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,
|
|
126
|
+
reasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,
|
|
127
|
+
totalCost: this.getTotalCost(lastUsage),
|
|
128
|
+
inferenceProvider,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
getTotalCost(lastUsage) {
|
|
133
|
+
const model = getModel(this.options.modelId);
|
|
134
|
+
if (model.free)
|
|
135
|
+
return 0;
|
|
136
|
+
if (lastUsage.is_byok) {
|
|
137
|
+
return lastUsage.cost_details?.upstream_inference_cost || 0;
|
|
138
|
+
}
|
|
139
|
+
return lastUsage.cost || 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { VERSION } from "../branding.js";
|
|
2
|
+
// Header names ported from the Orbital extension (shared/kilocode/headers.ts).
|
|
3
|
+
export const X_AXONCODE_VERSION = "X-AxonCode-Version";
|
|
4
|
+
export const X_AXONCODE_TASKID = "X-AxonCode-TaskId";
|
|
5
|
+
export const X_ORGANIZATIONID = "X-KiloCode-OrganizationId";
|
|
6
|
+
export const X_AXON_REPO = "X-AXON-REPO";
|
|
7
|
+
export const DEFAULT_HEADERS = {
|
|
8
|
+
"HTTP-Referer": "https://matterai.so",
|
|
9
|
+
// Kept as "Orbital": this is the protocol value the MatterAI backend
|
|
10
|
+
// already receives from the extension family, not user-facing branding.
|
|
11
|
+
"X-Title": "Orbital",
|
|
12
|
+
[X_AXONCODE_VERSION]: VERSION,
|
|
13
|
+
"User-Agent": `orbcode-cli/${VERSION}`,
|
|
14
|
+
};
|