@knightcodeai/cli 0.20.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/bundle/chunks/anthropic-messages-EY2CGNFR.js +6 -0
  3. package/dist/bundle/chunks/azure-openai-responses-KGI5F27P.js +2 -0
  4. package/dist/bundle/chunks/bedrock-converse-stream.js +8 -5
  5. package/dist/bundle/chunks/chunk-424HQSAU.js +10 -0
  6. package/dist/bundle/chunks/{chunk-DFRYMT3C.js → chunk-5O4LFJS7.js} +3 -3
  7. package/dist/bundle/chunks/chunk-BLHRVYHG.js +9 -0
  8. package/dist/bundle/chunks/{chunk-B7A4GIBK.js → chunk-EJS73Z3L.js} +1 -1
  9. package/dist/bundle/chunks/chunk-F22TYE3W.js +7 -0
  10. package/dist/bundle/chunks/{chunk-L6QGGIZG.js → chunk-FSAHEI7P.js} +4 -4
  11. package/dist/bundle/chunks/chunk-K6AEXKBA.js +30 -0
  12. package/dist/bundle/chunks/chunk-O2HKRIDR.js +16 -0
  13. package/dist/bundle/chunks/{chunk-JMJS643J.js → chunk-OLW2IS5G.js} +2 -2
  14. package/dist/bundle/chunks/chunk-QEVQ7EIS.js +2 -0
  15. package/dist/bundle/chunks/chunk-RRNUW65U.js +4 -0
  16. package/dist/bundle/chunks/{chunk-MZ5UVOTP.js → chunk-TOEFTX4C.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-6KWPS3LA.js → chunk-TOGJI36I.js} +141 -129
  18. package/dist/bundle/chunks/{chunk-N5M3XNPN.js → chunk-VLP4ZJF6.js} +1 -1
  19. package/dist/bundle/chunks/chunk-XDEOVQPJ.js +108 -0
  20. package/dist/bundle/chunks/chunk-YDO6TXKD.js +2 -0
  21. package/dist/bundle/chunks/{cli-72YA573P.js → cli-CIRIEG66.js} +1 -1
  22. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-AWHNPEAT.js → cloudflare-workers-ai-system-one-H3D4RYQQ.js} +1 -1
  23. package/dist/bundle/chunks/{easter-egg-3d-XXPCDCZV.js → easter-egg-3d-YLN6ACWS.js} +1 -1
  24. package/dist/bundle/chunks/{execute-KWIUMPDE.js → execute-P6HIB6ET.js} +1 -1
  25. package/dist/bundle/chunks/{google-generative-ai-HPLZ2ABU.js → google-generative-ai-C3FLXY47.js} +1 -1
  26. package/dist/bundle/chunks/{google-vertex-E3UTTTIG.js → google-vertex-AXIPR47C.js} +1 -1
  27. package/dist/bundle/chunks/knightcode-messages-LTLKDRGW.js +8 -0
  28. package/dist/bundle/chunks/{llama-cpp-classify-FEWC5ZIH.js → llama-cpp-classify-J2EJVDIZ.js} +1 -1
  29. package/dist/bundle/chunks/mistral-conversations-N2UBIKKR.js +5 -0
  30. package/dist/bundle/chunks/{openai-codex-responses-BCLSSOJ6.js → openai-codex-responses-REOHLPAA.js} +1 -1
  31. package/dist/bundle/chunks/{openai-completions-GTNAEXX6.js → openai-completions-XONAGXPC.js} +1 -1
  32. package/dist/bundle/chunks/{openai-decisions-CSQTW3S4.js → openai-decisions-ACB7F6SS.js} +1 -1
  33. package/dist/bundle/chunks/{openai-responses-ILFXLMJW.js → openai-responses-OHJS7C73.js} +1 -1
  34. package/dist/bundle/chunks/{openrouter-images-J66HXAMB.js → openrouter-images-SMNBEOPV.js} +1 -1
  35. package/dist/bundle/chunks/{runtime-OW5JKWBE.js → runtime-WNZGA67H.js} +1 -1
  36. package/dist/bundle/chunks/{typesafe-system-one-QA5VXBEZ.js → typesafe-system-one-KB2F2YGV.js} +1 -1
  37. package/dist/bundle/chunks/{virtual-modules-5DLTLPP7.js → virtual-modules-EU3BUTGN.js} +1 -1
  38. package/dist/bundle/cli-runtime.js +1 -1
  39. package/docs/app-server.md +102 -6
  40. package/docs/app-server.schema.json +493 -0
  41. package/docs/cli.md +3 -3
  42. package/docs/custom-provider.md +2 -0
  43. package/docs/desktop.md +83 -4
  44. package/docs/environment-variables.md +1 -0
  45. package/docs/extensions.md +3 -1
  46. package/docs/keybindings.md +1 -0
  47. package/docs/mcp.md +16 -2
  48. package/docs/message-types.md +1 -1
  49. package/docs/packages.md +2 -0
  50. package/docs/plan-mode.md +4 -0
  51. package/docs/settings.md +3 -3
  52. package/docs/slash-commands.md +2 -0
  53. package/docs/usage.md +23 -0
  54. package/examples/extensions/custom-provider-anthropic/index.ts +27 -31
  55. package/examples/extensions/todo.ts +3 -0
  56. package/examples/mcp-app/server.mjs +177 -0
  57. package/package.json +1 -1
  58. package/dist/bundle/chunks/anthropic-messages-5ZM7Z2XQ.js +0 -6
  59. package/dist/bundle/chunks/azure-openai-responses-OUS4VJNA.js +0 -2
  60. package/dist/bundle/chunks/chunk-4CRI7BMY.js +0 -2
  61. package/dist/bundle/chunks/chunk-6GRVIFTN.js +0 -11
  62. package/dist/bundle/chunks/chunk-6X7IXVQH.js +0 -30
  63. package/dist/bundle/chunks/chunk-BWGDAXFH.js +0 -108
  64. package/dist/bundle/chunks/chunk-FA32X2UJ.js +0 -7
  65. package/dist/bundle/chunks/chunk-IIWY7U5S.js +0 -7
  66. package/dist/bundle/chunks/chunk-J63X3FRT.js +0 -16
  67. package/dist/bundle/chunks/chunk-RKNFSQUG.js +0 -2
  68. package/dist/bundle/chunks/chunk-UU4A4NV5.js +0 -2
  69. package/dist/bundle/chunks/knightcode-messages-57CV36UV.js +0 -8
  70. package/dist/bundle/chunks/mistral-conversations-KT4C4XTZ.js +0 -5
package/docs/desktop.md CHANGED
@@ -7,8 +7,40 @@ configuration lives in the files the terminal reads (`settings.json`, `mcp.json`
7
7
  `tools.json`); the app's Settings edit those same files, so a change made in either place
8
8
  shows in both.
9
9
 
10
- The app is in development and not yet published. Phase S1 (threads, transcript, composer,
11
- approvals) is usable from a source checkout.
10
+ Installers come from the GitHub releases tagged `desktop-v<version>`; none is published yet,
11
+ so for now the app runs from a source checkout.
12
+
13
+ ## Installing
14
+
15
+ Each installer carries its own `knightcode` binary as the app server, so nothing needs to be
16
+ on `PATH`. None is signed by a recognised publisher yet:
17
+
18
+ - Windows: run `KnightCode-<version>.Setup.exe`. It installs for your user only, into
19
+ `%LOCALAPPDATA%\KnightCodeDesktop`, with Start menu and desktop shortcuts; remove it from
20
+ Settings › Apps. SmartScreen says "Windows protected your PC" the first time: choose
21
+ More info, then Run anyway.
22
+ - macOS: open the `.dmg` for your Mac (`arm64` for Apple silicon, `x64` for Intel) and drag
23
+ KnightCode to Applications. The app is signed ad hoc, not notarized, so the first launch
24
+ is refused: open System Settings › Privacy & Security and choose Open Anyway, or run
25
+ `xattr -dr com.apple.quarantine /Applications/KnightCode.app` once.
26
+ - Linux: install the `.deb` (`sudo apt install ./knightcode-desktop_<version>_amd64.deb`) or
27
+ the `.rpm` (`sudo dnf install ./knightcode-desktop-<version>-1.x86_64.rpm`).
28
+
29
+ Check a download against the release's `SHA256SUMS`.
30
+
31
+ ## Updates
32
+
33
+ - Windows: the app checks at launch and every 4 hours, and downloads a newer version in
34
+ the background. An Update button then shows at the right of the title bar, and Settings ›
35
+ About offers Restart to update; if threads are running it asks first, since quitting
36
+ interrupts them. Help › Check for Updates… checks now.
37
+ - macOS: the app checks the same way and shows Update when a newer version exists, which
38
+ opens the release page to download it (it cannot install itself while it is signed ad
39
+ hoc). KnightCode › Check for Updates… checks now.
40
+ - Linux: install the newer `.deb` or `.rpm` over the old one.
41
+
42
+ Settings › About shows the app's and the app server's versions and when the app last
43
+ checked.
12
44
 
13
45
  ## Running from source
14
46
 
@@ -25,7 +57,22 @@ quoting, so a path containing spaces (`C:\Program Files\...`) cannot be named; p
25
57
  on `PATH` and give the bare name. A packaged build ignores it and runs only its bundled binary.
26
58
 
27
59
  Server output goes to `app-server.log` in the app's logs folder (`%APPDATA%\KnightCode\logs`
28
- on Windows, `~/Library/Logs/KnightCode` on macOS, `~/.config/KnightCode/logs` on Linux).
60
+ on Windows, `~/Library/Logs/KnightCode` on macOS, `~/.config/KnightCode/logs` on Linux). At
61
+ 5 MiB it moves to `app-server.log.1`, the older ones to `.2` up to `.4`, so at most five files
62
+ are kept. Settings › About › Show logs opens the folder.
63
+
64
+ To build the installers for this machine, stage the app server and make them; the
65
+ installers land in `apps/desktop/out/make/`:
66
+
67
+ ```sh
68
+ bun run apps/desktop/scripts/stage-server.ts # builds knightcode for this platform
69
+ cd apps/desktop && bun run make
70
+ ```
71
+
72
+ The end-to-end tests launch the production build that packaging leaves in
73
+ `apps/desktop/.vite/`, against an app server whose model is scripted
74
+ (`packages/cli/test/fixtures/faux-app-server.ts`): run `bun run package`, then
75
+ `bun run test:e2e`.
29
76
 
30
77
  ## What works
31
78
 
@@ -64,6 +111,32 @@ on Windows, `~/Library/Logs/KnightCode` on macOS, `~/.config/KnightCode/logs` on
64
111
  added, as `knightcode config` does. Built-in extensions are always on and not listed.
65
112
 
66
113
  Changes apply to threads that start or resume afterwards.
114
+ - Side panel: Changes shows the thread's diff; the separators between hunks expand to
115
+ the unchanged lines around them. A file named in a tool row or in Changes opens in a
116
+ read-only tab beside it, at the line the tool read or changed. A tab opened in passing
117
+ is italic and the next one replaces it; double-click it to keep it. Each thread keeps
118
+ its own tabs, at most 12 files.
119
+ - Open in: a file tab's Open button sends the file, at its line, to an editor, terminal
120
+ or IDE found on your machine, or to the system's default app or file manager. The app
121
+ remembers the last choice, per project and overall.
122
+ - Terminal: Ctrl+\` opens a terminal in the thread's folder, in a region under the chat and
123
+ the side panel; Ctrl+J shows or hides it, and its top edge resizes it. The + menu offers
124
+ the shells found (PowerShell, Windows PowerShell, Command Prompt, Git Bash and WSL on
125
+ Windows; your login shell elsewhere), and the last one picked is the default. Terminals
126
+ belong to their thread: they keep running when the window reloads or another thread
127
+ opens, and end when you close them, archive or delete the thread, or quit. Copy needs a
128
+ selection (Ctrl+C, Ctrl+Shift+C or Ctrl+Insert); paste with Ctrl+V, Ctrl+Shift+V or
129
+ Shift+Insert (on macOS, Cmd+C and Cmd+V). Links in the output open in your browser.
130
+ - Preview: the side panel's + (Ctrl+T) opens a page in a preview tab, http and https only
131
+ (`localhost:3000` is enough). A link to a local server in a message opens there too.
132
+ Each thread keeps its own cookies and storage, cleared when the thread is deleted; pages
133
+ get no permissions, and downloads ask where to save. Annotate (Ctrl+Shift+.) lets you
134
+ drag over part of the page to attach it to the message you are writing, with the page's
135
+ address.
136
+ - MCP Apps: a tool from an [MCP server](mcp.md#mcp-apps) that has an app shows it in the
137
+ thread, under the turn's work. The app can call its own server's tools (asking you
138
+ first for one that may change things), send a follow-up message once you confirm it,
139
+ and open full screen in the side panel.
67
140
  - Archived threads: search, unarchive, or delete for good.
68
141
  - Command palette (Ctrl+K): actions and threads in one search.
69
142
  - Quitting while threads run asks first.
@@ -105,10 +178,16 @@ once. The protocol side is in [app-server.md](app-server.md#dialogs-and-extensio
105
178
  | Toggle the sidebar | Ctrl+B |
106
179
  | Back / forward | Alt+Left / Alt+Right |
107
180
  | Archive the thread | Ctrl+Shift+A |
181
+ | Close the side panel's file tab | Ctrl+Shift+W |
182
+ | Open a terminal / show or hide them | Ctrl+\` / Ctrl+J |
183
+ | Next / previous terminal | Ctrl+PageDown / Ctrl+PageUp |
184
+ | Open a preview / focus its address bar | Ctrl+T / Ctrl+L |
185
+ | Annotate the preview | Ctrl+Shift+. |
108
186
  | Send | Enter |
109
187
  | New line | Shift+Enter |
110
188
  | Steer the running turn | Ctrl+Enter |
111
189
  | Stop the running turn | Escape (empty composer) |
112
190
  | Allow / deny an approval | Enter / Escape (card focused) |
113
191
 
114
- On macOS, Ctrl is Cmd. Settings › Keyboard shortcuts lists every binding.
192
+ On macOS, Ctrl is Cmd, except in Ctrl+\` and Ctrl+PageDown/PageUp, which stay Control.
193
+ Settings › Keyboard shortcuts lists every binding.
@@ -90,6 +90,7 @@ These variables are read by KnightCode itself:
90
90
  | `KNIGHTCODE_CACHE_RETENTION` | Set to `long` for extended provider prompt caching where supported |
91
91
  | `KNIGHTCODE_SHARE_VIEWER_URL` | Override the base URL used by `/share` |
92
92
  | `KNIGHTCODE_BUG_ENDPOINT` | Override the origin `/bug` uploads go to; default is `https://remote.knightcode.dev` |
93
+ | `KNIGHTCODE_CLEAR_ON_SHRINK` | Set to `1` to clear empty rows when rendered content shrinks; the `terminal.clearOnShrink` setting takes precedence. See [Settings](settings.md) |
93
94
  | `KNIGHTCODE_HARDWARE_CURSOR` | Set to `1` to show the hardware cursor; see [Terminal setup](terminal-setup.md) |
94
95
  | `KNIGHTCODE_CLIPBOARD_OSC52` | Set to `1` to also send copied text to the terminal with OSC 52 after the local clipboard write, as KnightCode does over SSH. Use it when a remote client reaches KnightCode without SSH variables, such as a multiplexer attached from another machine or a remote IDE daemon |
95
96
  | `KNIGHTCODE_HYPERLINKS` | Override OSC 8 hyperlink detection with `1`, `0`, or `auto` |
@@ -262,6 +262,8 @@ A tool can run other tools with `ctx.executeTool(name, args, { signal, onUpdate
262
262
 
263
263
  See [`hello.ts`](../examples/extensions/hello.ts), [`todo.ts`](../examples/extensions/todo.ts), [`dynamic-tools.ts`](../examples/extensions/dynamic-tools.ts), and [`truncated-tool.ts`](../examples/extensions/truncated-tool.ts).
264
264
 
265
+ A tool registered under a built-in name replaces the built-in tool. Re-registering a copy of `createBashToolDefinition(cwd)` or `createPowerShellToolDefinition(cwd)` with object spread, for example to change its rendering, keeps [background commands](usage.md#background-commands): the session runs them with its own background manager as long as the copy keeps the built-in `execute` and `parameters`. A copy that replaces `execute`, such as one that routes commands to a sandbox or a remote host, runs commands in the foreground only, and `task_stop` is not declared for it.
266
+
265
267
  ### Tool exposure
266
268
 
267
269
  `exposure` controls how the model reaches a tool. "Callable" means callable from other tools through `ctx.executeTool()` (`ctx.tools`), as the `codemode` tool's scripts do:
@@ -315,7 +317,7 @@ knightcode.unregisterMcpServer("jira");
315
317
 
316
318
  Servers registered while the extension loads connect when the session starts, together with the `mcp.json` servers; servers registered later connect right away, and `knightcode.unregisterMcpServer()` closes the connection and makes the server's tools unreachable. Registrations are not saved: register again on every load, for example based on the extension's own settings. A server in `mcp.json` with the same name takes precedence, and `/mcp` shows the override. Registering the same name again replaces the extension's earlier registration; names registered by another extension, invalid names, and invalid configs throw.
317
319
 
318
- The built-in MCP support connects registered servers. When nothing does, because another extension replaced it (see [MCP](mcp.md#other-mcp-extensions)), each registration is reported as an extension error. Other MCP extensions can connect registered servers too: read them with `knightcode.getMcpServers()` on `session_start` and handle the `mcp_servers_change` event for later changes.
320
+ The built-in MCP support connects registered servers. When nothing does, because another extension replaced it (see [MCP](mcp.md#replace-the-built-in-mcp-support)), each registration is reported as an extension error. Other MCP extensions can connect registered servers too: read them with `knightcode.getMcpServers()` on `session_start` and handle the `mcp_servers_change` event for later changes.
319
321
 
320
322
  <a id="extensioncontext"></a>
321
323
  <a id="extensioncommandcontext"></a>
@@ -164,6 +164,7 @@ On native Windows, `app.suspend` has no default because Windows terminals do not
164
164
  | Keybinding id | Default | Description |
165
165
  |--------|---------|-------------|
166
166
  | `app.tools.expand` | `ctrl+o` | Collapse or expand tool output |
167
+ | `app.tools.background` | `ctrl+b` | Move running shell commands to the [background](usage.md#background-commands). Only while a command runs; otherwise the key reaches the editor, where `ctrl+b` moves the cursor left |
167
168
  | `app.message.copy` | `ctrl+x` | Copy the selected message in `/tree`; otherwise copy the active fullscreen text selection when `fullscreenCopyOnSelect` is disabled, falling back to the last assistant message. On OAuth sign-in screens, copy the sign-in URL |
168
169
  | `app.message.followUp` | `alt+enter` (`ctrl+q` on Windows and WSL) | Queue follow-up message |
169
170
  | `app.message.dequeue` | `alt+up` (`alt+q` on Windows and WSL) | Restore queued messages to editor |
package/docs/mcp.md CHANGED
@@ -243,13 +243,27 @@ When a connected server offers [resources](https://modelcontextprotocol.io/speci
243
243
 
244
244
  These tools reach every enabled, non-hidden server with resources. Their exposure is the widest exposure among those servers: `direct`, then `codemode` or `deferred`. Resource links in tool results identify `read_mcp_resource` and the server.
245
245
 
246
- Resources for MCP Apps, identified by `ui://` URIs or `text/html;profile=mcp-app`, are omitted because KnightCode does not render them. Resource icons are also omitted.
246
+ Resources for MCP Apps, identified by `ui://` URIs or `text/html;profile=mcp-app`, are omitted from these tools: the desktop app draws them (see [MCP Apps](#mcp-apps)). Resource icons are also omitted.
247
247
 
248
248
  Reading and listing resources is retried once after a transient HTTP error (408, 429, or 5xx). Tool calls are not retried because the server may already have performed them.
249
249
 
250
+ ## MCP Apps
251
+
252
+ A server can give a tool an interactive view with the MCP Apps extension (`io.modelcontextprotocol/ui`, Stable 2026-01-26): the tool names a `ui://` resource in `_meta.ui.resourceUri`, and the resource is an HTML document of type `text/html;profile=mcp-app`. The [desktop app](desktop.md) draws it in the tool's row. The terminal draws no apps, so KnightCode tells servers it supports them only in the desktop, and elsewhere servers answer with their text fallback.
253
+
254
+ In the desktop:
255
+
256
+ - A tool with an app is declared to the model whatever the server's exposure, and it cannot be called from codemode scripts: a call drawn as an app must be the model's own, which the session keeps. A tool you configured as `hidden` stays hidden and draws no app.
257
+ - A tool whose `_meta.ui.visibility` leaves out `"model"` is not declared to the model, whatever its exposure and whether or not it has an app of its own. An app can still call it.
258
+ - An app calls only tools of its own server whose visibility includes `"app"`. In Full access its calls run. In Ask and Auto-edit, a call to a tool not annotated `readOnlyHint` asks you first, naming the server and the tool. An app's calls do not go through the session's tool pipeline, so extension `tool_call` and `tool_result` handlers, permission gates included, do not see them.
259
+ - An app can send a follow-up message to the thread after you confirm it, open http and https links in your browser, and ask to be shown full screen in the side panel. It cannot update the model's context.
260
+ - The app runs in a sandboxed frame on its own origin, with no permissions (no camera, microphone, location or clipboard). It reaches the network only through the domains its resource declares in `_meta.ui.csp`; a declaration that is not a list of host sources is refused and the app does not load.
261
+
262
+ [`examples/mcp-app/server.mjs`](../examples/mcp-app/server.mjs) is a small stdio server with a counter app: one tool that shows the app, one only the app may call, and one read-only tool.
263
+
250
264
  ## Permissions
251
265
 
252
- Every MCP call passes through KnightCode's tool pipeline. Extension `tool_call` and `tool_result` handlers, including permission gates, therefore apply to MCP tools. Calls made from codemode scripts carry the codemode call ID as `parentToolCallId`.
266
+ Every MCP call the model makes passes through KnightCode's tool pipeline. Extension `tool_call` and `tool_result` handlers, including permission gates, therefore apply to MCP tools; an [MCP App](#mcp-apps)'s own calls are the exception. Calls made from codemode scripts carry the codemode call ID as `parentToolCallId`.
253
267
 
254
268
  `knightcode.getAllTools()` reports the annotations declared by each server: `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`. Permission extensions can use these hints to decide which calls require confirmation (see [Tool exposure](extensions.md#tool-exposure)). Resource tools are marked read-only.
255
269
 
@@ -140,7 +140,7 @@ interface AssistantMessage {
140
140
  }
141
141
  ```
142
142
 
143
- `responseModel` records a concrete provider response model when it differs from the requested model. `responseId`, `providerThinkingLevel`, `thinkingLevel`, `diagnostics`, and `rawStopReason` preserve provider or runtime details.
143
+ `responseModel` records the concrete model reported by the provider, when the provider reports one. It may equal the requested model. `responseId`, `providerThinkingLevel`, `thinkingLevel`, `diagnostics`, and `rawStopReason` preserve provider or runtime details.
144
144
 
145
145
  `"pending"` is used for a partial assistant message while it streams. The completed message in `message_end` has a terminal stop reason, and KnightCode does not persist `"pending"` assistant messages in session JSONL.
146
146
 
package/docs/packages.md CHANGED
@@ -16,6 +16,8 @@ knightcode install ./local-package
16
16
 
17
17
  `knightcode list` shows configured packages. Use `knightcode remove <source>` to remove one and `knightcode update --extensions` to reconcile package installations. See [Command Line](cli.md#package-commands) for every package command and option.
18
18
 
19
+ If a configured package fails to install at startup, for example because npm cannot reach the registry, KnightCode warns about it, skips that package's resources, and keeps running. KnightCode tries to install it again on the next start or `/reload`. `knightcode install` and `knightcode update --extensions` still report the failure and exit non-zero.
20
+
19
21
  Personal installs are written to `~/.knightcode/agent/settings.json`. Add `--local` or `-l` to write the package declaration to `.knightcode/settings.json`. KnightCode reads declarations from that file only after project trust is granted.
20
22
 
21
23
  Project packages are installed and loaded only after project trust is resolved. Packages can execute extension code and can include skills that instruct the model to run programs. Review third-party package source before installing it. Review project package declarations before granting project trust.
package/docs/plan-mode.md CHANGED
@@ -69,6 +69,10 @@ to the highlighted choice; on an implement choice it is sent with the
69
69
  approval, and on Keep planning it is sent as feedback. Escape keeps planning. Review does not open automatically on resume. Session replacement, tree navigation, or a new run cancels an open
70
70
  picker. An action rechecks the shown revision before changing state.
71
71
 
72
+ The desktop app opens review by itself too, asking "Implement this plan?":
73
+ Yes, implement this plan; Implement in a new thread; or Exit planning.
74
+ A fresh session is named after the plan's first line of prose.
75
+
72
76
  RPC reports a submission notification; `/plan` opens its review selection.
73
77
  JSON mode adds no extra output:
74
78
  the submission tool-call events contain the Markdown.
package/docs/settings.md CHANGED
@@ -107,7 +107,7 @@ See [Compaction Reference](compaction.md) for trigger, summarization, and valida
107
107
  | `showHardwareCursor` | boolean | `false` | Use the terminal cursor instead of KnightCode's drawn cursor. KnightCode still positions it for input methods. |
108
108
  | `terminal.showImages` | boolean | `true` | Display inline images when supported. |
109
109
  | `terminal.imageWidthCells` | number | `60` | Preferred inline image width in terminal cells. |
110
- | `terminal.clearOnShrink` | boolean | `false` | Clear empty rows when rendered content shrinks. |
110
+ | `terminal.clearOnShrink` | boolean | `false` | Clear empty rows when rendered content shrinks. When unset, `KNIGHTCODE_CLEAR_ON_SHRINK=1` enables it. |
111
111
  | `terminal.showTerminalProgress` | boolean | `false` | Show OSC 9;4 progress in the terminal tab. |
112
112
  | `terminal.hyperlinks` | `boolean \| "auto"` | `"auto"` | Override OSC 8 hyperlink detection. |
113
113
  | `terminal.images` | `"kitty" \| "iterm2" \| "auto" \| false` | `"auto"` | Override inline-image protocol detection. |
@@ -178,8 +178,8 @@ handed off by accident, and nothing is created through a symlink or junction.
178
178
  |---|---|---|---|
179
179
  | `transport` | `"auto" \| "sse" \| "websocket" \| "websocket-cached"` | `"auto"` | Preferred transport for AI providers that support multiple transports. |
180
180
  | `httpProxy` | string | None | Proxy URL applied as `HTTP_PROXY` and `HTTPS_PROXY` for KnightCode-managed HTTP clients. **Can only be set in agent-directory settings.** |
181
- | `httpIdleTimeoutMs` | number | `300000` | HTTP header and body idle timeout in milliseconds. Set to `0` to disable. |
182
- | `websocketConnectTimeoutMs` | number | `15000` | WebSocket connection timeout in milliseconds. Set to `0` to disable. |
181
+ | `httpIdleTimeoutMs` | number \| `"disabled"` | `300000` | HTTP header and body idle timeout in milliseconds. Set to `0` or `"disabled"` to disable. |
182
+ | `websocketConnectTimeoutMs` | number \| `"disabled"` | `15000` | WebSocket connection timeout in milliseconds. Set to `0` or `"disabled"` to disable. |
183
183
  | `retry.enabled` | boolean | `true` | Enable automatic agent-level retry for transient failures. |
184
184
  | `retry.maxRetries` | number | `3` | Maximum agent-level retry attempts. |
185
185
  | `retry.baseDelayMs` | number | `2000` | Initial exponential-backoff delay in milliseconds. |
@@ -64,8 +64,10 @@ Review a session before exporting or sharing it. Sessions can contain prompts, t
64
64
  | Command | Description |
65
65
  |---|---|
66
66
  | `/trust` | Save a project trust decision for future KnightCode processes |
67
+ | `/mcp` | Manage MCP servers: sign in, reconnect, enable or disable, and change exposure |
67
68
  | `/reload` | Reload keybindings, extensions, skills, templates, themes, and context files |
68
69
  | `/hotkeys` | Show active keyboard shortcuts |
70
+ | `/tasks` | List [background commands](usage.md#background-commands), show their output, and stop them |
69
71
  | `/changelog` | Show changelog entries |
70
72
  | `/quit` | Quit KnightCode |
71
73
 
package/docs/usage.md CHANGED
@@ -41,6 +41,7 @@ You can send more input while KnightCode is working:
41
41
  | Adjust the current task | Type a message and press `Enter` |
42
42
  | Add work after the current task | Type a message and press `Alt+Enter` |
43
43
  | Return queued messages to the editor | Press `Alt+Up` |
44
+ | Let a long command run on while KnightCode continues | Press `Ctrl+B` (see [Background commands](#background-commands)) |
44
45
  | Stop the current task | Press `Escape` |
45
46
 
46
47
  A message sent with `Enter` waits until the current response and its tool calls finish, then guides the next response. A follow-up sent with `Alt+Enter` waits until KnightCode finishes the current task. Aborting returns queued messages to the editor.
@@ -83,6 +84,28 @@ Prefix a command with `!` to run it and include its output in the conversation:
83
84
 
84
85
  Use `!!` when you want to run a command without sending its output to the model.
85
86
 
87
+ ## Background commands
88
+
89
+ KnightCode can start a command in the background, and decides per command whether to. It does so for work that takes long or never exits, such as a dev server, a file watcher, or a long build or test run. The tool call returns at once and the command keeps running while KnightCode works, across turns. When the command exits, KnightCode is told how it ended, with the last lines of its output, and carries on from there; you see the same notice in the transcript.
90
+
91
+ Press `Ctrl+B` while a command runs in the foreground to move it to the background. The same process keeps running; nothing starts again. The key works only while a command runs; otherwise `Ctrl+B` moves the cursor as usual.
92
+
93
+ While commands run, a pill above the footer counts them: `1 shell`, `3 shells`. Press `Down` in an empty editor to select it and `Enter` to open the **Background tasks** list, or click it; `Esc` or `Up` returns to the editor. `/tasks` opens the same list. With a single command it opens on that command's details.
94
+
95
+ | Key | In the list | In a command's details |
96
+ |-----|-------------|------------------------|
97
+ | `Up`/`Down` | Choose a command | |
98
+ | `Enter` | Show its details | Close |
99
+ | `x` | Stop the chosen command | Stop it |
100
+ | `Left` | Close | Back to the list |
101
+ | `Esc` | Close | Close |
102
+
103
+ A command's details show its status and exit code, runtime, command, and the last 10 lines of its output as it arrives. Its output is also written to a file, and KnightCode reads that file when it needs the output sooner. If writing that file fails, for example on a full disk, the file is incomplete and KnightCode is told so. KnightCode stops a command with the `task_stop` tool, which is on whenever `bash` or `powershell` is.
104
+
105
+ A background command ends when it exits, when KnightCode or you stop it, or when the session ends: quitting, `/new`, `/resume`, and `/fork` stop it, while `/reload` keeps it. Resuming or forking a session does not start its commands again; with your next message KnightCode is told that they were stopped. A `timeout` given with the command still stops it in the background. A command that writes more than 1 GiB of output is stopped, and at most 32 run at once.
106
+
107
+ A background command has no input: one that asks a question reads end-of-file. If you press `Escape` to stop the current task, a command that ends afterwards is recorded in the conversation without starting KnightCode again; the next message you send re-arms that. In print and JSON modes KnightCode does not exit while a command it started in the background is running, so it can act on how the command ended.
108
+
86
109
  ## Web Tools
87
110
 
88
111
  Two tools give the agent read access to the web. Both ship disabled; turn them on with `/tools`.
@@ -42,7 +42,7 @@ import {
42
42
  type TextContent,
43
43
  type ThinkingContent,
44
44
  type Tool,
45
- type ToolCall,
45
+ ToolCallArgumentsStream,
46
46
  type ToolResultMessage,
47
47
  type TranscriptContext,
48
48
  } from "@knightcode/ai";
@@ -454,8 +454,9 @@ function streamCustomAnthropic(
454
454
  const anthropicStream = client.messages.stream({ ...params }, { signal: options?.signal });
455
455
  stream.push({ type: "start", partial: output });
456
456
 
457
- type Block = (ThinkingContent | TextContent | (ToolCall & { partialJson: string })) & { index: number };
458
- const blocks = output.content as Block[];
457
+ // Stream state per Anthropic block index, kept off the content blocks so it never reaches partial messages.
458
+ // ToolCallArgumentsStream reads each argument delta once and shows the arguments parsed so far.
459
+ const openBlocks = new Map<number, { contentIndex: number; arguments?: ToolCallArgumentsStream }>();
459
460
 
460
461
  for await (const event of anthropicStream) {
461
462
  if (event.type === "message_start") {
@@ -468,15 +469,12 @@ function streamCustomAnthropic(
468
469
  calculateCost(model, output.usage);
469
470
  } else if (event.type === "content_block_start") {
470
471
  if (event.content_block.type === "text") {
471
- output.content.push({ type: "text", text: "", index: event.index } as any);
472
+ output.content.push({ type: "text", text: "" });
473
+ openBlocks.set(event.index, { contentIndex: output.content.length - 1 });
472
474
  stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
473
475
  } else if (event.content_block.type === "thinking") {
474
- output.content.push({
475
- type: "thinking",
476
- thinking: "",
477
- thinkingSignature: "",
478
- index: event.index,
479
- } as any);
476
+ output.content.push({ type: "thinking", thinking: "", thinkingSignature: "" });
477
+ openBlocks.set(event.index, { contentIndex: output.content.length - 1 });
480
478
  stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
481
479
  } else if (event.content_block.type === "tool_use") {
482
480
  output.content.push({
@@ -484,15 +482,18 @@ function streamCustomAnthropic(
484
482
  id: event.content_block.id,
485
483
  name: isOAuth ? fromClaudeCodeName(event.content_block.name, tools) : event.content_block.name,
486
484
  arguments: {},
487
- partialJson: "",
488
- index: event.index,
489
- } as any);
485
+ });
486
+ openBlocks.set(event.index, {
487
+ contentIndex: output.content.length - 1,
488
+ arguments: new ToolCallArgumentsStream(),
489
+ });
490
490
  stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
491
491
  }
492
492
  } else if (event.type === "content_block_delta") {
493
- const index = blocks.findIndex((b) => b.index === event.index);
494
- const block = blocks[index];
495
- if (!block) continue;
493
+ const open = openBlocks.get(event.index);
494
+ if (!open) continue;
495
+ const index = open.contentIndex;
496
+ const block = output.content[index]!;
496
497
 
497
498
  if (event.delta.type === "text_delta" && block.type === "text") {
498
499
  block.text += event.delta.text;
@@ -505,11 +506,8 @@ function streamCustomAnthropic(
505
506
  delta: event.delta.thinking,
506
507
  partial: output,
507
508
  });
508
- } else if (event.delta.type === "input_json_delta" && block.type === "toolCall") {
509
- (block as any).partialJson += event.delta.partial_json;
510
- try {
511
- block.arguments = JSON.parse((block as any).partialJson);
512
- } catch {}
509
+ } else if (event.delta.type === "input_json_delta" && block.type === "toolCall" && open.arguments) {
510
+ block.arguments = open.arguments.append(event.delta.partial_json);
513
511
  stream.push({
514
512
  type: "toolcall_delta",
515
513
  contentIndex: index,
@@ -520,20 +518,19 @@ function streamCustomAnthropic(
520
518
  block.thinkingSignature = (block.thinkingSignature || "") + (event.delta as any).signature;
521
519
  }
522
520
  } else if (event.type === "content_block_stop") {
523
- const index = blocks.findIndex((b) => b.index === event.index);
524
- const block = blocks[index];
525
- if (!block) continue;
521
+ const open = openBlocks.get(event.index);
522
+ if (!open) continue;
523
+ openBlocks.delete(event.index);
524
+ const index = open.contentIndex;
525
+ const block = output.content[index]!;
526
526
 
527
- delete (block as any).index;
528
527
  if (block.type === "text") {
529
528
  stream.push({ type: "text_end", contentIndex: index, content: block.text, partial: output });
530
529
  } else if (block.type === "thinking") {
531
530
  stream.push({ type: "thinking_end", contentIndex: index, content: block.thinking, partial: output });
532
- } else if (block.type === "toolCall") {
533
- try {
534
- block.arguments = JSON.parse((block as any).partialJson);
535
- } catch {}
536
- delete (block as any).partialJson;
531
+ } else if (block.type === "toolCall" && open.arguments) {
532
+ // One full parse of the complete text; malformed arguments are reported in `argumentsError`.
533
+ Object.assign(block, open.arguments.finish());
537
534
  stream.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: output });
538
535
  }
539
536
  } else if (event.type === "message_delta") {
@@ -563,7 +560,6 @@ function streamCustomAnthropic(
563
560
  stream.push({ type: "done", reason: output.stopReason, message: output });
564
561
  stream.end();
565
562
  } catch (error) {
566
- for (const block of output.content) delete (block as any).index;
567
563
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
568
564
  output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
569
565
  stream.push({ type: "error", reason: output.stopReason, error: output });
@@ -273,6 +273,9 @@ export default function (knightcode: ExtensionAPI) {
273
273
 
274
274
  case "clear":
275
275
  return new Text(theme.fg("success", "✓ ") + theme.fg("muted", "Cleared all todos"), 0, 0);
276
+
277
+ default:
278
+ return new Text(result.content[0]?.type === "text" ? theme.fg("error", result.content[0].text) : "", 0, 0);
276
279
  }
277
280
  },
278
281
  });
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * A stdio MCP server with one MCP App: a counter. It needs no dependencies (newline-delimited JSON-RPC over stdio).
4
+ *
5
+ * - `show_counter` (model and app): shows the counter; its `_meta.ui.resourceUri` names the app, `ui://counter/app.html`.
6
+ * - `increment` (app only, `_meta.ui.visibility: ["app"]`): changes the count, so outside Full access the desktop
7
+ * asks the user before it runs.
8
+ * - `read_count` (read-only): what the count is; an app may call it without asking.
9
+ *
10
+ * Add it to `~/.knightcode/agent/mcp.json`:
11
+ *
12
+ * { "mcpServers": { "counter": { "command": "node", "args": ["/path/to/server.mjs"], "exposure": "direct" } } }
13
+ *
14
+ * In the desktop app, ask KnightCode to show the counter: the tool's row draws the app. In the terminal, which draws
15
+ * no apps, the tool answers with its text.
16
+ */
17
+
18
+ import { createInterface } from "node:readline";
19
+
20
+ const APP_URI = "ui://counter/app.html";
21
+ const APP_MIME = "text/html;profile=mcp-app";
22
+ let count = 0;
23
+
24
+ const APP_HTML = `<!doctype html>
25
+ <meta charset="utf-8">
26
+ <style>
27
+ body { margin: 0; padding: 12px 14px; font: 14px var(--font-sans, system-ui); color: var(--color-text-primary, #111);
28
+ background: var(--color-background-primary, transparent); }
29
+ .row { display: flex; align-items: center; gap: 10px; }
30
+ output { font: 600 22px var(--font-mono, monospace); min-width: 3ch; }
31
+ button { font: inherit; padding: 4px 12px; border-radius: var(--border-radius-md, 8px);
32
+ border: 1px solid var(--color-border-primary, #ccc); background: var(--color-background-secondary, #f4f4f4);
33
+ color: inherit; cursor: pointer; }
34
+ #status { margin-top: 8px; color: var(--color-text-secondary, #666); font-size: 12px; }
35
+ </style>
36
+ <div class="row">
37
+ <span>Count</span><output id="count">…</output>
38
+ <button id="add">Add one</button>
39
+ <button id="tell">Tell KnightCode</button>
40
+ </div>
41
+ <div id="status"></div>
42
+ <script>
43
+ const pending = new Map();
44
+ let next = 0;
45
+ const request = (method, params) =>
46
+ new Promise((resolve, reject) => {
47
+ const id = ++next;
48
+ pending.set(id, { resolve, reject });
49
+ parent.postMessage({ jsonrpc: "2.0", id, method, params }, "*");
50
+ });
51
+ const notify = (method, params) => parent.postMessage({ jsonrpc: "2.0", method, params }, "*");
52
+ const show = (n) => (document.getElementById("count").textContent = String(n));
53
+ const status = (text) => (document.getElementById("status").textContent = text);
54
+ const theme = (context) => {
55
+ for (const [name, value] of Object.entries(context?.styles?.variables ?? {}))
56
+ if (value) document.documentElement.style.setProperty(name, value);
57
+ };
58
+ window.addEventListener("message", (event) => {
59
+ const message = event.data;
60
+ if (!message || message.jsonrpc !== "2.0") return;
61
+ if (message.id !== undefined && !message.method) {
62
+ const waiter = pending.get(message.id);
63
+ pending.delete(message.id);
64
+ if (message.error) waiter?.reject(new Error(message.error.message));
65
+ else waiter?.resolve(message.result);
66
+ return;
67
+ }
68
+ if (message.method === "ui/notifications/tool-result") show(message.params?.structuredContent?.count ?? "?");
69
+ else if (message.method === "ui/notifications/tool-cancelled") status("The call was cancelled");
70
+ else if (message.method === "ui/notifications/host-context-changed") theme(message.params);
71
+ else if (message.method === "ui/resource-teardown") parent.postMessage({ jsonrpc: "2.0", id: message.id, result: {} }, "*");
72
+ else if (message.method === "ping") parent.postMessage({ jsonrpc: "2.0", id: message.id, result: {} }, "*");
73
+ });
74
+ document.getElementById("add").onclick = async () => {
75
+ try {
76
+ const result = await request("tools/call", { name: "increment", arguments: { by: 1 } });
77
+ show(result.structuredContent.count);
78
+ status("");
79
+ } catch (error) {
80
+ status(error.message);
81
+ }
82
+ };
83
+ document.getElementById("tell").onclick = async () => {
84
+ const now = document.getElementById("count").textContent;
85
+ try {
86
+ await request("ui/message", { role: "user", content: [{ type: "text", text: "The counter app shows " + now + "." }] });
87
+ status("Sent");
88
+ } catch (error) {
89
+ status(error.message);
90
+ }
91
+ };
92
+ (async () => {
93
+ const init = await request("ui/initialize", {
94
+ protocolVersion: "2026-01-26",
95
+ appInfo: { name: "counter", version: "1.0.0" },
96
+ appCapabilities: { availableDisplayModes: ["inline"] },
97
+ });
98
+ theme(init.hostContext);
99
+ notify("ui/notifications/initialized", {});
100
+ notify("ui/notifications/size-changed", { height: document.documentElement.scrollHeight });
101
+ })();
102
+ </script>`;
103
+
104
+ const tools = [
105
+ {
106
+ name: "show_counter",
107
+ description: "Show the counter app with the current count.",
108
+ inputSchema: { type: "object", properties: {} },
109
+ annotations: { readOnlyHint: true },
110
+ _meta: { ui: { resourceUri: APP_URI } },
111
+ },
112
+ {
113
+ name: "increment",
114
+ description: "Add to the counter.",
115
+ inputSchema: { type: "object", properties: { by: { type: "number" } } },
116
+ _meta: { ui: { visibility: ["app"] } },
117
+ },
118
+ {
119
+ name: "read_count",
120
+ description: "Read the counter.",
121
+ inputSchema: { type: "object", properties: {} },
122
+ annotations: { readOnlyHint: true },
123
+ },
124
+ ];
125
+
126
+ const countResult = (text) => ({ content: [{ type: "text", text }], structuredContent: { count } });
127
+
128
+ function answer(method, params) {
129
+ switch (method) {
130
+ case "initialize":
131
+ return {
132
+ protocolVersion: params?.protocolVersion ?? "2025-11-25",
133
+ capabilities: { tools: {}, resources: {} },
134
+ serverInfo: { name: "counter-app", version: "1.0.0" },
135
+ };
136
+ case "ping":
137
+ return {};
138
+ case "tools/list":
139
+ return { tools };
140
+ case "tools/call":
141
+ if (params?.name === "show_counter") return countResult(`The counter is at ${count}.`);
142
+ if (params?.name === "increment") {
143
+ count += Number(params.arguments?.by ?? 1);
144
+ return countResult(`The counter is now ${count}.`);
145
+ }
146
+ if (params?.name === "read_count") return countResult(String(count));
147
+ throw Object.assign(new Error(`no tool ${params?.name}`), { code: -32602 });
148
+ case "resources/list":
149
+ return { resources: [{ uri: APP_URI, name: "Counter app", mimeType: APP_MIME }] };
150
+ case "resources/read":
151
+ if (params?.uri !== APP_URI) throw Object.assign(new Error(`no resource ${params?.uri}`), { code: -32002 });
152
+ return {
153
+ contents: [
154
+ { uri: APP_URI, mimeType: APP_MIME, text: APP_HTML, _meta: { ui: { prefersBorder: true, csp: {} } } },
155
+ ],
156
+ };
157
+ default:
158
+ throw Object.assign(new Error(`method not found: ${method}`), { code: -32601 });
159
+ }
160
+ }
161
+
162
+ const write = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
163
+ createInterface({ input: process.stdin }).on("line", (line) => {
164
+ let message;
165
+ try {
166
+ message = JSON.parse(line);
167
+ } catch {
168
+ return;
169
+ }
170
+ // Notifications (no id) need no answer.
171
+ if (message.id === undefined || message.id === null) return;
172
+ try {
173
+ write({ jsonrpc: "2.0", id: message.id, result: answer(message.method, message.params) });
174
+ } catch (error) {
175
+ write({ jsonrpc: "2.0", id: message.id, error: { code: error.code ?? -32603, message: error.message } });
176
+ }
177
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knightcodeai/cli",
3
- "version": "0.20.0",
3
+ "version": "0.21.1",
4
4
  "description": "KnightCode — a local, BYOK terminal coding agent powered by OpenRouter.",
5
5
  "type": "module",
6
6
  "repository": {