@visual-z/xeno 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +143 -33
  3. package/docs/install.md +18 -6
  4. package/docs/providers.md +147 -0
  5. package/package.json +8 -2
  6. package/src/apps/cli.ts +42 -14
  7. package/src/apps/commands.ts +32 -4
  8. package/src/apps/completions.ts +9 -6
  9. package/src/apps/export-cli.ts +24 -0
  10. package/src/apps/extension-ui.ts +330 -0
  11. package/src/apps/input.ts +16 -1
  12. package/src/apps/model-catalog.ts +42 -0
  13. package/src/apps/project-trust.ts +49 -0
  14. package/src/apps/render.ts +1 -0
  15. package/src/apps/resource-browser.ts +75 -0
  16. package/src/apps/resources.ts +25 -0
  17. package/src/apps/session-browser.ts +113 -0
  18. package/src/apps/session-picker.ts +55 -0
  19. package/src/apps/shell.ts +1177 -378
  20. package/src/apps/startup.ts +400 -268
  21. package/src/core/action-defaults.ts +306 -0
  22. package/src/core/attachment-input.ts +17 -0
  23. package/src/core/catalog-merge.ts +19 -0
  24. package/src/core/event-validation.ts +19 -2
  25. package/src/core/events.ts +45 -2
  26. package/src/core/export.ts +6 -2
  27. package/src/core/extension-flags.ts +14 -0
  28. package/src/core/images.ts +46 -0
  29. package/src/core/json-event.ts +2 -0
  30. package/src/core/keymap.ts +43 -119
  31. package/src/core/model-replay.ts +22 -0
  32. package/src/core/model-scope.ts +53 -0
  33. package/src/core/packages.ts +47 -0
  34. package/src/core/project.ts +21 -3
  35. package/src/core/prompts.ts +33 -5
  36. package/src/core/queue.ts +18 -0
  37. package/src/core/resource-selection.ts +36 -0
  38. package/src/{runtime → core}/resume.ts +15 -22
  39. package/src/core/session-archive.ts +4 -0
  40. package/src/core/session-browser.ts +82 -0
  41. package/src/core/session-entry.ts +56 -0
  42. package/src/core/session-position.ts +26 -0
  43. package/src/core/settings.ts +169 -154
  44. package/src/core/theme.ts +15 -6
  45. package/src/core/thinking.ts +26 -0
  46. package/src/core/tokens.ts +2 -0
  47. package/src/core/transcript.ts +15 -5
  48. package/src/runtime/auth.ts +116 -11
  49. package/src/runtime/catalog.ts +54 -151
  50. package/src/runtime/clipboard.ts +114 -0
  51. package/src/runtime/extension-loader.ts +37 -0
  52. package/src/runtime/extension-ui.ts +55 -0
  53. package/src/runtime/extensions.ts +273 -0
  54. package/src/runtime/images.ts +76 -0
  55. package/src/runtime/loop.ts +213 -83
  56. package/src/runtime/model-config.ts +232 -0
  57. package/src/runtime/model-data.LICENSE +27 -0
  58. package/src/runtime/model-data.ts +21 -0
  59. package/src/runtime/model-replay.ts +10 -0
  60. package/src/runtime/model-scope.ts +20 -0
  61. package/src/runtime/model-sources.ts +151 -0
  62. package/src/runtime/model.ts +74 -11
  63. package/src/runtime/models-fetch.ts +6 -104
  64. package/src/runtime/oauth.ts +4 -3
  65. package/src/runtime/openai-responses.ts +12 -4
  66. package/src/runtime/openai.ts +13 -4
  67. package/src/runtime/package-manager.ts +125 -0
  68. package/src/runtime/pi-provider.ts +235 -0
  69. package/src/runtime/progress.ts +26 -0
  70. package/src/runtime/provider-access.ts +21 -0
  71. package/src/runtime/provider-protocols.ts +29 -0
  72. package/src/runtime/provider-request.ts +73 -0
  73. package/src/runtime/provider.ts +26 -6
  74. package/src/runtime/rich-text.ts +36 -0
  75. package/src/runtime/tools.ts +41 -5
  76. package/src/store/command.ts +7 -1
  77. package/src/store/context-files.ts +1 -1
  78. package/src/store/log.ts +65 -0
  79. package/src/store/models.ts +28 -9
  80. package/src/store/observations.ts +4 -2
  81. package/src/store/packages.ts +98 -0
  82. package/src/store/recovery.ts +8 -0
  83. package/src/store/resource-index.ts +170 -0
  84. package/src/store/resources.ts +75 -142
  85. package/src/store/session-archive.ts +162 -0
  86. package/src/store/session-browser.ts +68 -0
  87. package/src/store/session-position.ts +27 -0
  88. package/src/store/sessions.ts +87 -66
  89. package/src/store/trust.ts +13 -0
  90. package/src/tui/chat-viewport.ts +47 -0
  91. package/src/tui/editor.ts +62 -15
  92. package/src/tui/host.ts +50 -9
  93. package/src/tui/input.ts +8 -2
  94. package/src/tui/select.ts +24 -6
  95. package/src/tui/settings.ts +4 -0
  96. package/src/tui/system-theme.ts +700 -0
  97. package/src/tui/transcript.ts +63 -11
  98. package/src/tui/tree.ts +161 -95
  99. package/src/vendor/pi-rich/LICENSE.upstream +21 -0
  100. package/src/vendor/pi-rich/UPSTREAM.json +17 -0
  101. package/src/vendor/pi-rich/clipboard-command.ts +44 -0
  102. package/src/vendor/pi-rich/exif-orientation.ts +182 -0
  103. package/src/vendor/pi-rich/html.ts +51 -0
  104. package/src/vendor/pi-rich/image-convert.ts +49 -0
  105. package/src/vendor/pi-rich/image-resize-core.ts +164 -0
  106. package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
  107. package/src/vendor/pi-rich/image-resize.ts +123 -0
  108. package/src/vendor/pi-rich/mime.ts +116 -0
  109. package/src/vendor/pi-rich/photon.ts +139 -0
  110. package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
  111. package/src/vendor/pi-tui/UPSTREAM.json +50 -0
  112. package/src/vendor/pi-tui/VENDOR.md +7 -5
  113. package/src/vendor/pi-tui/autocomplete.ts +35 -10
  114. package/src/vendor/pi-tui/colors.ts +367 -0
  115. package/src/vendor/pi-tui/components/box.ts +8 -9
  116. package/src/vendor/pi-tui/components/editor.ts +4 -2
  117. package/src/vendor/pi-tui/components/markdown.ts +8 -2
  118. package/src/vendor/pi-tui/index.ts +31 -1
  119. package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
  120. package/src/vendor/pi-tui/native/clipboard.h +208 -0
  121. package/src/vendor/pi-tui/native/darwin/README.md +22 -0
  122. package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
  123. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  124. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  125. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
  126. package/src/vendor/pi-tui/native/linux/README.md +23 -0
  127. package/src/vendor/pi-tui/native/linux/build.sh +65 -0
  128. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  129. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  130. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
  131. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
  132. package/src/vendor/pi-tui/native/napi.h +91 -0
  133. package/src/vendor/pi-tui/native/win32/README.md +31 -0
  134. package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
  135. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  136. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  137. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
  138. package/src/vendor/pi-tui/native-platform.ts +2 -0
  139. package/src/vendor/pi-tui/oklab.ts +233 -0
  140. package/src/vendor/pi-tui/terminal-colors.ts +26 -8
  141. package/src/vendor/pi-tui/terminal-image.ts +43 -8
  142. package/src/vendor/pi-tui/terminal.ts +16 -9
  143. package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
  144. package/src/vendor/pi-tui/tui.ts +112 -75
  145. package/src/vendor/pi-tui/utils.ts +89 -49
  146. package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # xeno changelog
2
+
3
+ ## 0.2.0 — 2026-09-30
4
+
5
+ - Restore saved model identity on session entry; cross-model history moves through a mechanical checkpoint.
6
+ - Align Pi terminal input, full-screen navigation, model controls, rich content, session browsing and resource reload.
7
+ - Add local extensions, custom UI components and explicit npm/git/local package management.
8
+ - Export portable JSONL archives, import independent sessions, and write local terminal diagnostics.
9
+ - Ship pinned native helpers, unify clipboard writes, and add typed extension CLI flags, model scopes, named sessions and offline export.
10
+ - Persist thinking visibility, manage model defaults, and scope cancellable authentication to its active application.
11
+ - Combine Pi and Models.dev into an offline catalog of 226 providers and 7,501 models, with ten native protocols and explicit exclusions.
12
+ - Support flat, Pi and OpenCode provider configuration, private credentials and headers, local endpoints, and a separate atomic refresh cache.
13
+ - Checkpoint changed wire profiles from the log, preserve native signatures, and render cumulative arguments for interleaved tool calls.
14
+
15
+ Implementation and validation records are in the task cards under `docs/tasks/`.
16
+
17
+ ## 0.1.4
18
+
19
+ Earlier implementation and validation records are available in `docs/PROGRESS.md`.
package/README.md CHANGED
@@ -17,7 +17,7 @@ is ever summarized by a model, and nothing is ever rewritten in place.
17
17
  messages[] frozen system prefix + handoff + this node's events
18
18
  ```
19
19
 
20
- Version **0.1.4**. The on-disk session format will still change; `0.x` is that
20
+ Version **0.2.0**. The on-disk session format will still change; `0.x` is that
21
21
  statement. Published as `@visual-z/xeno`; the license is `UNLICENSED` (readable and installable, no reuse granted).
22
22
 
23
23
  ## Install
@@ -70,8 +70,9 @@ views. Its budget-limited result and negative cases are recorded in the same rep
70
70
  neither segmentation nor the experimental adaptive rule has established a quality advantage.
71
71
  `bun run scripts/compare-harness.ts smoke --output /absolute/new/smoke-directory`
72
72
  also exercises xeno-windowed, xeno-continuous and an installed Pi CLI with shared
73
- confined tools, without paid model calls. Pi runs on Node through an isolated local
74
- tool bridge; no Pi SDK is added to the project. Its `plan` / `run` protocol freezes
73
+ confined tools, without paid model calls. The comparison Pi CLI runs on Node through
74
+ an isolated local tool bridge; xeno's providers use the pinned Pi AI runtime described below.
75
+ Its `plan` / `run` protocol freezes
75
76
  source, model, task order and spending policy, including summary calls. An explicitly
76
77
  authorized `--budget unlimited` on both `plan` and `run` records costs without a local
77
78
  dollar ceiling; upstream errors stop the trial, with HTTP 402 marked as awaiting top-up.
@@ -82,15 +83,21 @@ This checks a controlled configuration, not stock Pi or an official leaderboard.
82
83
  ```
83
84
  xeno [-m <model>] [--provider <name>] [--api-key <key>]
84
85
  [-w <workspace>] [--log <dir>] [--max-turns N]
85
- [--trigger N --hard N] [--list-models] [--fresh]
86
+ [--trigger N --hard N] [--list-models [search]] [--fresh]
87
+ [-c|--continue] [-r|--resume [id]] [--session path|id]
88
+ [--fork path|id] [--session-dir dir] [--session-id id] [--no-session]
89
+ [--models patterns] [--thinking level] [--mode text|json]
90
+ [--export session [output.html]] [--offline] [--version]
86
91
  [models --refresh]
87
92
  [--system-prompt <text-or-file>] [--append-system-prompt <text>]
88
93
  [--no-context-files|-nc]
89
94
  [--tools names] [--exclude-tools names] [--no-tools]
90
95
  [--allow-tools names] [--approve|--no-approve]
91
- [--theme <name|path>]
92
- [--skill <path>] [--no-skills]
93
- [--prompt-template <path>] [--no-prompt-templates]
96
+ [--theme <name|path>] [--use-theme <name>] [--no-themes]
97
+ [--tui-mode regular|fullscreen]
98
+ [--skill <path>] [--no-skills|-ns]
99
+ [--prompt-template <path>] [--no-prompt-templates|-np]
100
+ [--extension|-e <path>] [--no-extensions|-ne]
94
101
  [-p <prompt>] [prompt...]
95
102
  ```
96
103
 
@@ -98,29 +105,53 @@ xeno [-m <model>] [--provider <name>] [--api-key <key>]
98
105
  - A prompt, or `-p` / `--print`: one shot, then exit. `-p` puts only the
99
106
  assistant's final text on stdout; the banner, usage, thinking, and tool
100
107
  lines go to stderr. Piped stdin is appended after the command-line prompt.
101
- - A workspace that already holds a session is resumed. `--fresh` opens a
102
- second session beside it. Nothing is deleted.
108
+ - Each invocation starts a new session, including `-p`. `--fresh` remains
109
+ an explicit alias. Nothing is deleted.
110
+ - `-c` continues the newest session, or starts one if none exists. `-r`
111
+ opens a session picker; cancelling leaves saved sessions untouched.
112
+ `--session <path|id>` and `-r <id>` open a specific session.
113
+ A portable JSONL file creates an independent imported session in the current workspace.
114
+ - Resume restores that session's model before consulting defaults. An
115
+ explicit `--model` switches through a checkpoint, preserving the original
116
+ log without replaying another model's thinking. Missing or ambiguous saved
117
+ models are reported; restore their catalog entry before resuming.
103
118
  - A leftover `.xeno/log` is not migrated; continue it with `--log .xeno/log`.
119
+ - `--models 'deepseek/*:low,anthropic/*'` resolves ordered model scopes; an
120
+ explicit empty scope disables cycling. `--model id:level` sets thinking,
121
+ with an explicit `--thinking` taking precedence.
122
+ - `--session-id id` creates or resumes that exact local ID. `--export` writes
123
+ offline HTML from a saved session or portable archive without starting an agent.
124
+ - `--theme` can be repeated for explicit files/directories; `--use-theme`
125
+ selects a theme. `--no-themes` disables automatic discovery.
126
+ - Extensions can register typed native CLI flags, including in `--help`.
127
+ Normal startup stays offline; `--offline` also blocks remote preparation.
104
128
 
105
129
  Slash commands that exist in the session (from `/help`):
106
130
 
107
131
  ```
108
132
  /model [id] search models, or switch (cuts a node)
109
- /models refresh fetch models.dev into ~/.xeno/models.json
133
+ /models refresh refresh ~/.xeno/models-cache.json; preserve models.json
110
134
  /login [provider] choose a login method, or store an API key
111
135
  /login claude-pro|codex|github-copilot
112
136
  subscription login (first-party client credentials)
113
137
  /logout [provider] remove a stored credential
138
+ /scoped-models enable, disable and order models for cycling
114
139
  /new start a new session beside this one
115
140
  /resume [id] search sessions, or switch to one
116
141
  /name <name> name this session
117
142
  /session id, path, events, usage
118
143
  /back [n [files|context]] [confirm]
119
- /tree search the session tree; navigate context at an exchange boundary
144
+ /tree search, filter, fold, label and navigate exact session events
120
145
  /fork [n] choose an exchange to copy into a new session
121
146
  /clone copy the active branch into a new session
122
- /trust [here|parent|never]
123
- /settings [key v] theme, thinking, statusBar
147
+ /export [file] offline HTML, or a portable archive with a .jsonl filename
148
+ /import <file> validate and open an archive as an independent session
149
+ /debug [file] write local terminal and message diagnostics
150
+ /changelog show bundled release notes
151
+ /trust [once|here|parent|never]
152
+ /reload refresh local resources at a durable context checkpoint
153
+ /config search resources and edit global or project overrides
154
+ /settings [key v] theme, thinking, queues, viewport and editor settings
124
155
  /hotkeys bindings in effect
125
156
  /usage /state /help /quit
126
157
  ```
@@ -128,22 +159,53 @@ Slash commands that exist in the session (from `/help`):
128
159
  Type `/` for commands, templates and skills. The editor completes command
129
160
  arguments and paths. `@file` or `@"file name"` attaches a project file anywhere
130
161
  outside code literals; choosing a file keeps the prompt in the editor.
131
- At line start, `!cmd` runs in the workspace and sends stdout/stderr/exit to the model; `!!cmd` runs without
132
- sending output. During a turn, Enter queues steering (delivered after the
133
- current tools finish); Alt+Enter queues a follow-up for after the turn.
162
+ `!cmd` runs in the workspace, streams output, and records it for the next model
163
+ request. `!!cmd` records human history while excluding its output from model
164
+ context. Neither starts a model request or requires a model credential.
165
+ Escape cancels the command and its process group. During a turn, Enter queues
166
+ steering after the current tools finish; Alt+Enter queues a follow-up after the
167
+ turn, or submits immediately while idle. Alt+Up returns the entire queue to the
168
+ editor. Settings expose independent `steeringMode` and `followUpMode` values
169
+ of `all` or `one-at-a-time`.
134
170
  Escape interrupts; pressing Escape twice on an empty idle editor opens the tree.
135
171
  Ctrl+C clears the draft, then a quick second press exits; Ctrl+D exits an empty
136
172
  editor. Ctrl+O and Ctrl+T toggle tools and thinking throughout the history.
137
-
138
- `xeno --list-models` prints the built-in catalog (12 providers, 23 models)
139
- and whether a credential resolved. `xeno models --refresh` writes
140
- `~/.xeno/models.json`; startup never fetches.
173
+ Ctrl+T persists `hideThinkingBlock`. `/settings` also manages new-session model
174
+ defaults and per-model thinking overrides; clearing an override restores the
175
+ project or global setting. `showCacheMissNotices` displays the existing prefix
176
+ diagnostics without changing usage accounting or issuing extra model requests.
177
+
178
+ `/scoped-models` changes the current session immediately; Ctrl+S saves the range.
179
+
180
+ `-r` and `/resume` share a session browser: Tab changes workspace scope, Ctrl+N
181
+ filters named sessions, Ctrl+S cycles threaded/recent/relevance ordering, Ctrl+P
182
+ shows paths, and Ctrl+R renames. Search supports `re:<pattern>` and quoted phrases.
183
+ Ctrl+D asks before moving an inactive session into its sibling `.trash` directory;
184
+ the original files remain recoverable by moving that directory back. All-workspace
185
+ discovery uses registered session roots, with no 200-session result cap.
186
+ The tree supports Ctrl+O filter cycling, Alt+Left/Right branch folding/navigation,
187
+ Shift+L labels, Shift+T label timestamps, and Ctrl+X copy. These actions can be rebound.
188
+ Enter toggles a model, Ctrl+A/Ctrl+X enables/disables filtered models, Ctrl+P
189
+ toggles a provider, and Alt+Up/Down orders enabled models. An explicit empty
190
+ range disables cycling. `/thinking` exposes off/minimal/low/medium/high/xhigh/max
191
+ with model capability checks; Ctrl+S saves a default for that provider/model.
192
+ Automatic model changes clamp effort to a supported level.
193
+
194
+ `xeno --list-models [search]` searches the offline union of Pi and OpenCode's
195
+ Models.dev directory: **226 providers and 7,501 model entries** in the pinned
196
+ snapshot. Use `provider/model` when several providers expose the same ID.
197
+ `xeno models --refresh` atomically updates `~/.xeno/models-cache.json`;
198
+ startup never fetches and the user's `~/.xeno/models.json` remains untouched.
199
+ Flat catalogs, Pi `providers` and OpenCode `provider` configuration are supported.
200
+ See [provider configuration](docs/providers.md) for local servers, model overrides,
201
+ credentials, native cloud protocols, and explicit coverage limits.
141
202
 
142
203
  ## Credentials
143
204
 
144
- Order: `--api-key`, then `~/.xeno/auth.json`, then the provider's environment
145
- variable. A stored entry owns that provider — the environment is not consulted
146
- after a stored value fails.
205
+ Order: `--api-key`, `~/.xeno/auth.json`, `models.json` provider/model credentials,
206
+ then the provider's environment variables. A failed explicit source does not
207
+ fall through to another account. `/login codex` uses the `openai-codex` protocol;
208
+ ordinary OpenAI API models use `openai`.
147
209
 
148
210
  Interactive sessions can start with no key and wait for `/login`. `-p` and
149
211
  non-tty runs exit 1 and name `--api-key` or the variable; they do not mention
@@ -221,12 +283,13 @@ Shipped, not planned:
221
283
  an action/observation cycle repeats, or the request budget is nearly exhausted.
222
284
  Existing messages, the system prefix, and tool definitions stay frozen. Model
223
285
  notes remain annotations and cannot manufacture verified completion.
224
- - Two wire adapters (`anthropic-messages`, `openai-completions`). Both
225
- stream; deltas are UI events and are not written to the log.
226
- - A built-in catalog of **12** API-key providers and **23** models
227
- (anthropic, openai, google, deepseek, xai, groq, cerebras, mistral,
228
- openrouter, zai, moonshotai, together). `~/.xeno/models.json` overrides
229
- by id. Startup does not hit the network.
286
+ - Ten native wire protocols through a pinned Pi runtime adapter, alongside the
287
+ three existing verified transports. Streaming text, thinking, tools and opaque
288
+ signatures project from the same append-only log; deltas remain UI events.
289
+ - An offline provider/model union with **226 providers** and **7,501 entries**,
290
+ user overrides by provider/model, and a separate explicit-refresh cache.
291
+ Native protocol fixtures cover requests and responses; the directory is not
292
+ a claim that every listed remote account/model was tested live.
230
293
  - Sessions, resume, `/new` / `/resume` / `/name` / `/session`, `/back`
231
294
  (files, context, or both), `/tree`, read-only `branch` / `resolve`, and
232
295
  write branches in their own git worktree.
@@ -238,14 +301,46 @@ Shipped, not planned:
238
301
  Markdown, searchable pickers and settings. Durable history is a pure log
239
302
  projection across handoff boundaries. Includes tool diffs, historical folds,
240
303
  Pi dark/light palettes, `keybindings.json`, `/settings` and `/hotkeys`.
304
+
241
305
  - Context files: `AGENTS.md` / `CLAUDE.md` and the override chain, plus
242
306
  `--system-prompt` / `--append-system-prompt` / `-nc`.
243
307
  - Skills (`~/.xeno/skills/`, `/skill:name`) and prompt templates
244
308
  (`~/.xeno/prompts/`, `/name`). Untrusted projects do not load the
245
309
  project-level copies.
310
+ - `/reload` refreshes local models, settings, keybindings, context files, skills,
311
+ templates and themes without losing the draft or active model. A changed system
312
+ prefix checkpoints first; unchanged resources do not create a new segment.
313
+ `/config` and `xeno config` share searchable resource provenance and enablement.
314
+ Relative resource paths resolve from the settings file that declares them;
315
+ project overrides cycle through inherit, load and skip. CLI paths remain explicit.
316
+ `/trust` opens a selector; `once` grants no persistent trust. Global
317
+ `defaultProjectTrust` cannot be overridden by project settings.
318
+ Templates support Pi's `$1`, `$@`, `$ARGUMENTS`, defaults and slices as well as
319
+ the existing `{{name}}` syntax. Arguments are substituted once.
320
+ - Local TS/JS extensions can register commands, shortcuts, tools and lifecycle
321
+ hooks through Pi's factory protocol. Discovery, project trust and `/reload`
322
+ use the same resource inventory. Custom state and messages remain append-only
323
+ log events. See [extension boundaries and usage](docs/extensions.md).
324
+ - Packages can come from local paths, npm or git: `xeno install <source>`,
325
+ `remove`, `update --extensions` and `list`. `--local` selects project scope;
326
+ `-e` tries a package for one invocation. Manifests, per-kind filters and trust
327
+ feed the same resource inventory. Normal startup stays offline.
328
+ See [package management](docs/packages.md).
246
329
  - `/login` for API keys and for Claude Pro/Max, Codex, and GitHub
247
330
  Copilot subscription flows. See Known gaps.
248
331
 
332
+ Images can be attached with `@path`, a dropped image path, or Ctrl+V (files,
333
+ image, then text). `/attachments` removes draft images; queue recall and history
334
+ forks retain them. The log stores immutable blob references. Provider adapters
335
+ load verified bytes for Anthropic, Chat Completions and Responses; text-only
336
+ models receive a visible placeholder. Custom models declare `input: ["text", "image"]`
337
+ in `models.json`. `terminal.showImages`, `terminal.imageWidthCells`,
338
+ `images.autoResize`, `images.blockImages` and `markdown.mermaid` are configurable
339
+ through `/settings`. Pi syntax highlighting and Mermaid use the same rendering
340
+ path for saved replies and live output. Image tokens are estimates until provider
341
+ usage is available. Enabling image blocking checkpoints an existing image context;
342
+ unblocking affects new submissions, preserving historical request prefixes.
343
+
249
344
  ## Known gaps
250
345
 
251
346
  - **No sandbox.** See Isolation. That is a decision, not a missing feature.
@@ -258,7 +353,8 @@ Shipped, not planned:
258
353
  catalog comment on `gpt-5.5-pro` notes a `context_over_200k` tier that
259
354
  is not applied. `/usage` reports `{kind:"partial"}` when cache prices
260
355
  are missing; it does not treat a missing cache price as zero.
261
- - **No packages or extensions.** Skills and templates are files on disk.
356
+ - **Package installation is separate from resource loading.** Local extensions,
357
+ skills and templates load from explicit, trusted filesystem sources.
262
358
  There is no installer.
263
359
  - **Bun only.** Node has not been tested.
264
360
  - **`/back` does not restore symlinks.**
@@ -270,8 +366,22 @@ Shipped, not planned:
270
366
  `docs/TUI_ARCHITECTURE.md` describes the active interaction path and verification.
271
367
  `docs/PI_PARITY.md` records deliberate differences and remaining Pi features.
272
368
  Pi's editor supports undo, not redo; an old `editor.redo` binding is diagnosed
273
- as unsupported rather than silently accepted. Ctrl+- and the existing Ctrl+Z
274
- alias invoke Pi undo.
369
+ as unsupported rather than silently accepted. Ctrl+- invokes Pi undo.
370
+ Ctrl+Z suspends the CLI on POSIX; `fg` restores the terminal and unsent draft.
371
+
372
+ `/settings tuiMode fullscreen` uses Pi's alternate-screen viewport, transcript
373
+ search (Ctrl+Shift+F), prompt navigation (Ctrl+Up/Down), mouse scrolling and text
374
+ selection. `/settings fullscreenExitOutput resume-hint` leaves a resume command
375
+ when exiting; `transcript` prints the conversation. Switching modes preserves
376
+ the editor, history and undo. Scrollbar, wheel speed, copy-on-select, editor
377
+ padding, autocomplete rows, cursor, output padding and double-Escape behavior
378
+ are configurable in `/settings`.
379
+
380
+ `/settings theme` previews built-in and custom themes from the config directory's
381
+ `themes/`. Escape restores the prior theme. Custom files reload after valid edits;
382
+ `auto` follows terminal light/dark changes and `system` derives Pi's palette from
383
+ terminal color replies. Pi's namespaced action IDs work in `keybindings.json`
384
+ alongside existing xeno names; an empty array disables an action in its own scope.
275
385
 
276
386
  ## Why this shape
277
387
 
@@ -460,7 +570,7 @@ Every path below was checked with `test -f` on this checkout.
460
570
  | `store/context-files.ts` | AGENTS.md discovery |
461
571
  | **`src/runtime/`** | impure but thin; the only layer allowed npm packages |
462
572
  | `runtime/provider.ts` / `openai.ts` | the two wire formats |
463
- | `runtime/catalog.ts` / `model.ts` | 12 providers, 23 models |
573
+ | `runtime/catalog.ts` / `model-config.ts` / `model-sources.ts` | Pi + Models.dev union, offline overlays and private provider configuration |
464
574
  | `runtime/tools.ts` / `history-tools.ts` | confined tools, complete query results and history retrieval |
465
575
  | `runtime/benchmark.ts` | actual Session trials, bounded spending and cache diagnostics |
466
576
  | `runtime/loop.ts` | orchestration; every judgement delegates to a pure function in core |
package/docs/install.md CHANGED
@@ -30,8 +30,16 @@ cd xeno
30
30
  bun install
31
31
  ```
32
32
 
33
- `bun install` pulls the one runtime dependency (`@anthropic-ai/sdk`)
34
- and the TypeScript dev tooling.
33
+ `bun install` pulls the provider SDK, Pi text layout/Markdown dependencies,
34
+ syntax highlighting, Mermaid, Photon image processing, and TypeScript tooling.
35
+ The exact versions are in `package.json` and `bun.lock`.
36
+
37
+ Pi native helpers for macOS, Linux X11 and Windows (arm64/x64) ship with the
38
+ package. They are pinned upstream builds with matching source and hashes;
39
+ startup does not download or compile them. Missing platform support uses the
40
+ clipboard command fallbacks. The packaged macOS helper was load-tested on
41
+ this machine; Linux/Windows payloads and routing are checked without claiming
42
+ native device acceptance.
35
43
 
36
44
  Run the CLI without linking:
37
45
 
@@ -55,8 +63,10 @@ xeno --list-models
55
63
  `bun link` writes a shim that executes the `bin` field
56
64
  (`src/apps/cli.ts`) with Bun. It does not copy the tree.
57
65
 
58
- A workspace that already has a session is resumed. `--fresh` starts a
59
- new session beside it; nothing is deleted.
66
+ Each invocation starts a new session. `xeno -c` continues the newest one;
67
+ `xeno -r` opens a picker. `--fresh` is a compatibility alias for the default,
68
+ and nothing is deleted. Resume restores the saved model. An explicit
69
+ `--model` switches at a checkpoint while keeping the original log intact.
60
70
 
61
71
  ## Credentials
62
72
 
@@ -64,7 +74,8 @@ Resolution order, first match wins:
64
74
 
65
75
  1. `--api-key`
66
76
  2. `~/.xeno/auth.json` (created `0600` by `/login`)
67
- 3. the provider's environment variable
77
+ 3. the model or provider `apiKey` in `~/.xeno/models.json`
78
+ 4. the provider's environment variable
68
79
 
69
80
  A stored entry owns that provider. The environment is not a fallback
70
81
  after a stored key fails.
@@ -111,7 +122,8 @@ In **your home directory** (`~/.xeno/`, or `$XENO_CONFIG_DIR`):
111
122
  auth.json credentials (0600)
112
123
  settings.json theme, thinking, defaults
113
124
  trust.json which directories may load project settings
114
- models.json optional catalog overlay (written by `xeno models --refresh`)
125
+ models.json optional user provider/model configuration
126
+ models-cache.json generated catalog cache (written by `xeno models --refresh`)
115
127
  keybindings.json optional
116
128
  prompts/ skills/ optional
117
129
  ```
@@ -0,0 +1,147 @@
1
+ # 供应商、模型目录与自定义配置
2
+
3
+ xeno 内置 Pi 0.99.1 和 OpenCode 所用 Models.dev 的固定源并集:226 个供应商、7,501 个模型条目。同一个模型在不同供应商下分别计数。目录保留能力与已知价格;未提供的价格显示未知,不用零补全。
4
+
5
+ 来源固定为 Pi `1b347794e2a630e4359f2584f4eea388145d0ddf`、OpenCode `2fa3363c924c5c3e367b84a87ae478296a0ed59b`,Models.dev JSON SHA256 `7345de7358c471e78ac756aed556cd804677cd0d8748b71edc3931225440158f`。SDK 固定为 `@earendil-works/pi-ai@0.99.1`;引入它是为了复用维护中的原生协议和签名处理,避免在 xeno 重写十套 SDK。SDK 仅在 runtime;原有 TUI vendor 的版本与升级方式独立。
6
+
7
+ ## 目录和选择
8
+
9
+ ```sh
10
+ xeno --list-models siliconflow
11
+ xeno --provider deepseek --model deepseek-flash
12
+ xeno --model google/gemini-2.5-flash
13
+ xeno models --refresh
14
+ ```
15
+
16
+ 按 `provider/model` 识别模型。完整标识优先于其他供应商包含斜杠的原始 ID;裸 ID 多义时给出候选,不按目录顺序猜。旧会话保存了供应商时精确恢复;只存 ID 的旧会话需要目录中的唯一匹配。
17
+
18
+ 目录优先级:内置 → `~/.xeno/models-cache.json` → `~/.xeno/models.json`。启动、列举、选择、恢复和 `/reload` 共用同一解析入口。缓存更新模型元数据时保留原有已验证模型的 wire dialect;用户配置仍可明确覆盖它。刷新成功原子替换缓存,失败保留原缓存,始终不重写用户配置。`XENO_CONFIG_DIR` 可以替换这个目录。启动不联网;`--offline` 禁止显式刷新。
19
+
20
+ 编辑配置后执行 `/reload`。当前模型的端点、API、wire ID 或回放格式变化会记录 checkpoint;纯凭据轮换不切段。禁用当前模型后,下一条输入保留为草稿,需要重新选择模型。供应商错误或配置错误不会使请求转到另一个账户。
21
+
22
+ 新的 assistant 事件同时记录回放配置的摘要,恢复时从日志比较,配置别名不变也能识别上游协议变化。摘要不包含凭据或端点原文。没有摘要的旧原生配置在首次接入时先建立 checkpoint;旧的默认传输保持原恢复行为。
23
+
24
+ ## 本地无认证端点(Pi 分组)
25
+
26
+ 以下内容保存为 `~/.xeno/models.json`;端口、模型 ID 与本地服务保持一致。
27
+
28
+ ```json
29
+ {
30
+ "providers": {
31
+ "local": {
32
+ "api": "openai-completions",
33
+ "baseUrl": "http://127.0.0.1:1234/v1",
34
+ "auth": false,
35
+ "models": [{
36
+ "id": "my-local-model",
37
+ "name": "Local model",
38
+ "contextWindow": 32768,
39
+ "maxTokens": 4096,
40
+ "compat": {
41
+ "supportsStore": false,
42
+ "supportsDeveloperRole": false,
43
+ "maxTokensField": "max_tokens"
44
+ },
45
+ "samplingParams": { "top_k": 40, "min_p": 0.05 }
46
+ }]
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ `xeno --model local/my-local-model` 使用该条目。`auth: false` 或 `authHeader: false` 关闭自动认证;SDK 构造所需的内部占位 key 不会发到服务器。`authHeader: "x-custom-key"` 可把最终解析的 key 放进指定头。Google 原生 SDK 拥有认证流程,不支持这两种头替换方式;本地兼容服务使用 OpenAI 接口。
53
+
54
+ ## OpenCode 分组、别名与私有配置
55
+
56
+ ```json
57
+ {
58
+ "provider": {
59
+ "company": {
60
+ "npm": "@ai-sdk/openai-compatible",
61
+ "options": {
62
+ "baseURL": "https://gateway.example.com/v1",
63
+ "apiKey": "{env:COMPANY_API_KEY}",
64
+ "temperature": 0.2
65
+ },
66
+ "headers": { "x-tenant": "${COMPANY_TENANT}" },
67
+ "models": {
68
+ "coding": {
69
+ "id": "upstream-model-id",
70
+ "limit": { "context": 128000, "output": 8192 },
71
+ "reasoning": true,
72
+ "thinkingLevelMap": { "off": null, "minimal": "low", "low": "low", "medium": "medium", "high": "high", "xhigh": null, "max": null },
73
+ "compat": { "supportsStore": false, "supportsDeveloperRole": false, "maxTokensField": "max_tokens" },
74
+ "body": { "top_p": 0.9 }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ 选择 `company/coding`,线上请求使用 `upstream-model-id`。`npm` 仅用于识别已接通的协议,不会安装或执行任意供应商包;也可以直接指定 `api`。OpenCode 的完整应用配置、插件、任意 SDK 选项不在这个解析器的兼容范围。
83
+
84
+ 凭据优先级为 CLI `--api-key` → `auth.json` → 模型/供应商 `apiKey` → 供应商环境变量。供应商 `apiKey` 可被模型覆盖;头和 scoped env 按字段合并。指定来源失败就报错,不向下兜底。
85
+
86
+ `apiKey`、`headers`、`env` 和 `baseUrl` 支持字面字符串、`$VAR` / `${VAR}` / `{env:VAR}`,以及以 `!` 开头的本地命令。命令只有在凭据解析或实际请求需要时执行,不在目录解析时执行。`$$` 和 `$!` 表示字面开头字符。保留 xeno 原有规则:裸 `MY_KEY` 是字面 key,引用变量要写 `$MY_KEY`。只把自己信任的命令写入用户配置;目录缓存不含用户秘密。
87
+
88
+ `env` 只作用于该供应商,不更改全局进程环境。变量顺序不影响结果,循环引用拒绝。密钥、私有请求头、env 值与请求 options 保存在私有配置中,不随模型对象进入模型列表或日志。
89
+
90
+ ## 覆盖与启停
91
+
92
+ ```json
93
+ {
94
+ "providers": {
95
+ "deepseek": {
96
+ "apiKey": "$DEEPSEEK_API_KEY",
97
+ "modelOverrides": {
98
+ "deepseek-flash": { "maxTokens": 8192, "headers": { "x-route": "coding" } }
99
+ },
100
+ "allowlist": ["deepseek-*"],
101
+ "denylist": ["*-deprecated"]
102
+ }
103
+ },
104
+ "disabled_providers": ["unused-provider"]
105
+ }
106
+ ```
107
+
108
+ `modelOverrides` 只修改已有条目;`models` 可以新增。保留旧的 `[{provider,id,api,...}]` 或 `{ "models": [...] }` 扁平格式。支持 provider/model 的 `disabled: true` / `enabled: false`、全局 `enabled_providers` / `disabled_providers`,以及 `allowlist` / `denylist`(同义 `whitelist` / `blacklist`),模式支持 `*` 和 `?`,deny 优先。无效同级模型给出字段路径,有效同级项继续可用;损坏的显式覆盖不会继续使用原来的凭据。
109
+
110
+ ## 原生协议和云认证
111
+
112
+ | api | 接口与认证 |
113
+ |---|---|
114
+ | `anthropic-messages` | Anthropic、支持 Messages 的网关;API key 或已有 Claude OAuth |
115
+ | `openai-completions` | Chat Completions 兼容端点 |
116
+ | `openai-responses` | OpenAI Responses;每次发完整投影,`store: false` |
117
+ | `openai-codex-responses` | ChatGPT 订阅端点;`/login codex` 或 `OPENAI_CODEX_ACCESS_TOKEN` |
118
+ | `azure-openai-responses` | Azure key + resource/base URL;支持 `AZURE_RESOURCE_NAME` / `AZURE_API_KEY` 别名 |
119
+ | `google-generative-ai` | Gemini 原生 API,Google/Gemini API key |
120
+ | `google-vertex` | Vertex API key,或 `auth: "ambient"` + ADC;项目和位置必需 |
121
+ | `bedrock-converse-stream` | AWS bearer token、profile、IAM/容器/身份链;`auth: "ambient"` 显式使用 SDK 凭据链 |
122
+ | `mistral-conversations` | Pi 的 Mistral 原生适配 |
123
+ | `pi-messages` | Pi Messages 兼容服务器 |
124
+
125
+ Vertex 可配置 scoped `env`:`GOOGLE_APPLICATION_CREDENTIALS`、`GOOGLE_CLOUD_PROJECT`、`GOOGLE_CLOUD_LOCATION`;也接受 OpenCode 的 `GOOGLE_VERTEX_PROJECT` / `GOOGLE_VERTEX_LOCATION`。Bedrock 使用 `AWS_PROFILE`、`AWS_REGION` 等 SDK 环境设置;resource/project/location 不会被当成 API key。需要端点模板的服务必须提供对应变量,例如 Cloudflare 的 account/gateway。
126
+
127
+ 旧 `auth.json` 中存于 `openai` 的 Codex OAuth 可由 `openai-codex` 兼容读取;显式原生条目优先,普通 OpenAI API key 不共享到订阅协议。新的 `/login codex` 写入 `openai-codex`。其他 Pi 专属订阅登录流程不冒充已接入;可按协议配置 key/token。
128
+
129
+ ## 可配置参数与边界
130
+
131
+ - `compat` / `nativeCompat` 传递 Pi 协议兼容能力;`thinkingLevelMap` 明确本地思考档到上游值的映射。
132
+ - `options` 支持 `temperature`、`samplingParams`、`cacheRetention`、`timeoutMs`、`maxRetryDelayMs`、`metadata`;具体协议只消费自己支持的参数。云 region/profile 通过 `env`。不认识的 option 明确报错。
133
+ - `samplingParams` 用于 OpenAI 兼容协议的采样扩展;`body` / `extraBody` 是实际原生请求体的额外字段。模型、system、消息、工具、stream 与服务端会话字段由投影/适配器负责,不能被这些扩展覆盖。
134
+ - D18 保留 xeno 客户端身份;不会自动带 Pi/Claude Code/Copilot 的产品身份或 attribution 头,也不会插入 Claude Code 的身份提示。订阅服务可能因此拒绝第三方客户端;此处不承诺所有订阅账户可用。
135
+ - Google/Vertex SDK 自行管理 HTTP,不使用自定义 fetch;Bedrock 自行管理 AWS HTTP 与重试,外层不再叠加重试。取消与终止状态仍统一交回 xeno。
136
+
137
+ ## 覆盖、排除与验证依据
138
+
139
+ `src/runtime/model-data.ts` 同时保存完整来源元数据和 1,373 条排除记录。明确无工具能力或非文本输出的模型不进入 agent 可选目录;还需要专用 SDK 的条目保留排除原因,例如 GitLab、WatsonX、Vertex Anthropic、SAP、Merge Gateway、v0、Qvac。可用兼容服务仍可由用户指定 API/端点接入,不能仅新增一个名字就算支持了专用协议。
140
+
141
+ 除了两份上游源码,Cohere、Venice、AIHubMix 的接入分别核对了 [Cohere compatibility API](https://docs.cohere.com/docs/compatibility-api)、[Venice API 官方资料](https://github.com/veniceai/api-docs/blob/main/agents.md)、[AIHubMix 快速开始](https://docs.aihubmix.com/en/quick-start)。Cohere 的 reasoning effort 按官方能力映射为 none/high;不发送其未支持的 store 字段。
142
+
143
+ 上游配置依据:[Pi models](https://github.com/earendil-works/pi/blob/1b347794e2a630e4359f2584f4eea388145d0ddf/packages/coding-agent/docs/models.md)、[OpenCode provider](https://github.com/anomalyco/opencode/blob/2fa3363c924c5c3e367b84a87ae478296a0ed59b/packages/opencode/src/provider/provider.ts)、[Models.dev](https://models.dev/)。
144
+
145
+ 本地验证覆盖十种协议的真实受控 HTTP(包含 AWS 二进制 EventStream)、签名/工具结果往返、错误重试/取消、实际 CLI JSON 输出、实际 PTY 重载/切换。没有逐一调用付费远程模型,目录条目数量不等于账户可用性或模型质量证明。
146
+
147
+ 重生成时先下载并保存 Models.dev JSON,再运行 `bun scripts/update-model-catalog.ts <snapshot.json>`,审查来源摘要、条目/排除变化及测试固定计数。应用启动不执行生成器。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@visual-z/xeno",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "Segmented append-only agent log with pure context and handoff projections",
@@ -10,7 +10,9 @@
10
10
  "files": [
11
11
  "src",
12
12
  "README.md",
13
- "docs/install.md"
13
+ "CHANGELOG.md",
14
+ "docs/install.md",
15
+ "docs/providers.md"
14
16
  ],
15
17
  "engines": {
16
18
  "bun": ">=1.1.0"
@@ -34,7 +36,11 @@
34
36
  },
35
37
  "dependencies": {
36
38
  "@anthropic-ai/sdk": "^0.127.0",
39
+ "@earendil-works/pi-ai": "0.99.1",
40
+ "@silvia-odwyer/photon-node": "0.3.4",
37
41
  "get-east-asian-width": "1.6.0",
42
+ "grok-mermaid": "0.2.3",
43
+ "highlight.js": "10.7.3",
38
44
  "marked": "18.0.11"
39
45
  },
40
46
  "publishConfig": {
package/src/apps/cli.ts CHANGED
@@ -13,25 +13,53 @@
13
13
  */
14
14
 
15
15
  import {
16
- parseArgs, startup, readPipedStdin, mergePrintPrompt, silenceTtyEarly,
16
+ parseArgs, startup, readPipedStdin, mergePrintPrompt, silenceTtyEarly, printCliHelp,
17
17
  } from "./startup.ts";
18
18
  import { runPrompt, runRepl } from "./shell.ts";
19
+ import { runPackageCommand } from "../runtime/package-manager.ts";
20
+ import { exportHtmlFile } from "./export-cli.ts";
19
21
 
20
- const args = parseArgs(process.argv.slice(2));
22
+ const args = parseArgs(process.argv.slice(2), { deferExtensions: true });
21
23
  // Before startup reads auth, scans context files and prints the header:
22
24
  // in cooked mode the tty would echo anything typed during that window
23
25
  // straight into the banner.
24
- const restoreTty = silenceTtyEarly({ interactive: !args.print && !args.prompt });
25
- if (args.print) {
26
- // With a prompt already given, stdin is optional context: wait briefly for it.
27
- args.prompt = mergePrintPrompt(args.prompt,
28
- await readPipedStdin(process.stdin, args.prompt.length > 0 ? { firstByteMs: 3_000 } : {}));
29
- }
30
- const app = await startup(args);
31
- if (args.prompt) {
26
+ const restoreTty = silenceTtyEarly({ interactive: !args.packageCommand && !args.help && !args.version && !args.exportFile && !args.print && !args.prompt });
27
+ process.once("exit", restoreTty);
28
+ try {
29
+ if (args.help && (args.packageCommand || args.configCommand || args.exportFile || args.version || args.listModels || args.modelsCommand)) {
30
+ printCliHelp();
31
+ } else if (args.version) {
32
+ if (args.extensionArgv) throw new Error("Unknown flags for --version");
33
+ const pkg = await Bun.file(new URL("../../package.json", import.meta.url)).json() as { version: string };
34
+ console.log(pkg.version);
35
+ } else if (args.exportFile) {
36
+ if (args.extensionArgv) throw new Error("Unknown flags for offline export");
37
+ console.log(`Exported ${await exportHtmlFile(args.exportFile, args.prompt || undefined, args.workspace)}`);
38
+ } else if (args.packageCommand) {
39
+ if (args.extensionArgv) throw new Error("Unknown package command flag");
40
+ if ((args.offline || process.env["XENO_OFFLINE"] === "1") && (args.packageCommand.operation === "update" || args.packageCommand.operation === "install" && args.packageCommand.sources.some(source => /^(npm:|git:|git\+|https?:\/\/|ssh:\/\/|git@)/.test(source)))) throw new Error("--offline prevents remote package installation or updates");
41
+ const controller = new AbortController(), cancel = () => controller.abort();
42
+ process.once("SIGINT", cancel); process.once("SIGTERM", cancel);
43
+ try { await runPackageCommand(args.packageCommand, { workspace: args.workspace, signal: controller.signal, ...(args.approve === undefined ? {} : { trusted: args.approve }) }); }
44
+ catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = controller.signal.aborted ? 130 : 1; }
45
+ finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
46
+ } else {
47
+ const app = await startup(args);
48
+ if (args.print) {
49
+ // With a prompt already given, stdin is optional context: wait briefly for it.
50
+ args.prompt = mergePrintPrompt(args.prompt,
51
+ await readPipedStdin(process.stdin, args.prompt.length > 0 ? { firstByteMs: 3_000 } : {}));
52
+ }
53
+ if (args.prompt) {
54
+ restoreTty();
55
+ await runPrompt(app, args.prompt);
56
+ } else {
57
+ await runRepl(app, restoreTty);
58
+ }
59
+ }
60
+ } catch (error) {
61
+ console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1;
62
+ } finally {
63
+ process.off("exit", restoreTty);
32
64
  restoreTty();
33
- await runPrompt(app, args.prompt);
34
- } else {
35
- try { await runRepl(app); }
36
- finally { restoreTty(); }
37
65
  }