@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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +471 -0
  3. package/bin/orbcode.js +2 -0
  4. package/dist/api/client.js +141 -0
  5. package/dist/api/headers.js +14 -0
  6. package/dist/api/models.js +49 -0
  7. package/dist/api/stream.js +1 -0
  8. package/dist/auth/auth.js +172 -0
  9. package/dist/branding.js +31 -0
  10. package/dist/config/promptHistory.js +33 -0
  11. package/dist/config/settings.js +112 -0
  12. package/dist/core/agent.js +459 -0
  13. package/dist/core/events.js +1 -0
  14. package/dist/core/sessions.js +44 -0
  15. package/dist/headless.js +64 -0
  16. package/dist/index.js +84 -0
  17. package/dist/prompts/system.js +379 -0
  18. package/dist/tools/executors/executeCommand.js +58 -0
  19. package/dist/tools/executors/files.js +197 -0
  20. package/dist/tools/executors/listFiles.js +65 -0
  21. package/dist/tools/executors/searchFiles.js +104 -0
  22. package/dist/tools/executors/web.js +72 -0
  23. package/dist/tools/index.js +85 -0
  24. package/dist/tools/schemas/ask_followup_question.js +40 -0
  25. package/dist/tools/schemas/attempt_completion.js +19 -0
  26. package/dist/tools/schemas/browser_action.js +60 -0
  27. package/dist/tools/schemas/check_past_chat_memories.js +23 -0
  28. package/dist/tools/schemas/codebase_search.js +23 -0
  29. package/dist/tools/schemas/execute_command.js +31 -0
  30. package/dist/tools/schemas/fetch_instructions.js +20 -0
  31. package/dist/tools/schemas/file_edit.js +31 -0
  32. package/dist/tools/schemas/file_write.js +27 -0
  33. package/dist/tools/schemas/generate_image.js +27 -0
  34. package/dist/tools/schemas/index.js +29 -0
  35. package/dist/tools/schemas/list_code_definition_names.js +19 -0
  36. package/dist/tools/schemas/list_files.js +23 -0
  37. package/dist/tools/schemas/lsp.js +46 -0
  38. package/dist/tools/schemas/multi_file_edit.js +42 -0
  39. package/dist/tools/schemas/new_task.js +27 -0
  40. package/dist/tools/schemas/read_file.js +27 -0
  41. package/dist/tools/schemas/run_slash_command.js +23 -0
  42. package/dist/tools/schemas/search_files.js +27 -0
  43. package/dist/tools/schemas/switch_mode.js +23 -0
  44. package/dist/tools/schemas/update_todo_list.js +19 -0
  45. package/dist/tools/schemas/use_skill.js +19 -0
  46. package/dist/tools/schemas/web_fetch.js +19 -0
  47. package/dist/tools/schemas/web_search.js +19 -0
  48. package/dist/tools/types.js +7 -0
  49. package/dist/ui/App.js +569 -0
  50. package/dist/ui/LoginView.js +82 -0
  51. package/dist/ui/components/ApprovalPrompt.js +21 -0
  52. package/dist/ui/components/FollowupPrompt.js +45 -0
  53. package/dist/ui/components/Header.js +12 -0
  54. package/dist/ui/components/InputBox.js +220 -0
  55. package/dist/ui/components/ModelPicker.js +51 -0
  56. package/dist/ui/components/SessionPicker.js +52 -0
  57. package/dist/ui/components/Spinner.js +19 -0
  58. package/dist/ui/components/StatusBar.js +18 -0
  59. package/dist/ui/components/rows.js +106 -0
  60. package/dist/ui/markdown.js +64 -0
  61. package/dist/utils/diff.js +144 -0
  62. package/dist/utils/shell.js +19 -0
  63. 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,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/index.js"
@@ -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
+ };