kward 0.73.1 → 0.74.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/Gemfile.lock +2 -2
- data/doc/configuration.md +14 -1
- data/doc/mcp.md +72 -0
- data/doc/rpc.md +1 -0
- data/doc/usage.md +7 -0
- data/lib/kward/cli/commands.rb +3 -0
- data/lib/kward/cli/doctor.rb +18 -10
- data/lib/kward/cli/interactive_turn.rb +9 -3
- data/lib/kward/cli/tabs.rb +6 -9
- data/lib/kward/cli.rb +38 -1
- data/lib/kward/config_files.rb +35 -2
- data/lib/kward/mcp/client.rb +56 -0
- data/lib/kward/mcp/server_config.rb +55 -0
- data/lib/kward/mcp/stdio_transport.rb +106 -0
- data/lib/kward/prompt_interface/editor/controller.rb +64 -3
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +4 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +10 -4
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +448 -20
- data/lib/kward/prompt_interface/editor/renderer.rb +16 -0
- data/lib/kward/prompt_interface/editor/search.rb +70 -13
- data/lib/kward/prompt_interface/editor/state.rb +67 -12
- data/lib/kward/prompt_interface/editor/vibe_state.rb +12 -1
- data/lib/kward/rpc/server.rb +1 -0
- data/lib/kward/tools/mcp_tool.rb +118 -0
- data/lib/kward/tools/registry.rb +32 -2
- data/lib/kward/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2f648e16f1d5c7187e428b30ecc2d5945f067c5decf98d1db46c51e1974a7d6
|
|
4
|
+
data.tar.gz: 5acbd39bd18701659c3699a80f6a660ae5bc85c8366083ee7aa3ec184dc93706
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 92de6ca9d486d9bc95d6c36d5bffa2bc591ad18b9dbf499aba14dc349c6f6f8492869b78788e7370a38b0e90067326a0678c0e3efb1a552b12604ab8db55d15d
|
|
7
|
+
data.tar.gz: 97f901a2f5992b34d6e3a75d92137518e6b16471ae66b73455cbbccfba740a480045271cac670d82d2920ceb61a48c4ecd2b83ae3de7d97b8bdecb8b284e658c
|
data/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,26 @@ All notable changes to Kward will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.74.0] - 2026-06-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added more Vim-compatible Vibe editor bindings, including `Y`, `>>`/`<<`, `~`, `g_`, `|`, `W`/`E`/`B`, `ge`/`gE`, case operators, `gJ`, counted `%`, previous-change mark jumps, jump-list navigation, and linewise paste semantics.
|
|
12
|
+
- Added local stdio MCP server support through `mcpServers`, exposing configured MCP server tools to Kward turns.
|
|
13
|
+
- Added smartcase, live cursor movement, cancel restore, and visible match highlighting to editor search.
|
|
14
|
+
- Added Vibe editor `:e <filename>` and `:e! <filename>` commands with path tab completion.
|
|
15
|
+
- Added `--skip-config` as an emergency fallback that ignores the main config file for one run.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fixed editor undo/redo so selections do not become sticky after restoring buffer contents.
|
|
20
|
+
- Fixed busy composer slash commands so they are blocked instead of being queued or sent as in-flight steering.
|
|
21
|
+
|
|
7
22
|
## [0.73.1] - 2026-06-30
|
|
8
23
|
|
|
9
24
|
### Fixed
|
|
10
25
|
|
|
26
|
+
- Fixed Vibe editor `dG` so it deletes from the current line through the end of the file.
|
|
11
27
|
- Fixed session picker delete confirmation in terminals that send printable keys as CSI-u escape sequences.
|
|
12
28
|
|
|
13
29
|
## [0.73.0] - 2026-06-29
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
kward (0.
|
|
4
|
+
kward (0.74.0)
|
|
5
5
|
base64
|
|
6
6
|
nokogiri
|
|
7
7
|
tiktoken_ruby
|
|
@@ -146,7 +146,7 @@ CHECKSUMS
|
|
|
146
146
|
html-proofer (5.2.1) sha256=fdd958a7cbf9c3255fb96fe7cfc4e611f64e2706e469488a3326309ad007d2fd
|
|
147
147
|
io-event (1.16.2) sha256=9f9cb0a96ea5c3850a672606c65f27bc96d7621399ef6196acbfe2be0cd1279c
|
|
148
148
|
json (2.19.9) sha256=9b9025b7cdddafa38d316eca0b2358488e42d417045c1b90d216a9fefe46b79a
|
|
149
|
-
kward (0.
|
|
149
|
+
kward (0.74.0)
|
|
150
150
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
151
151
|
metrics (0.15.0) sha256=61ded5bac95118e995b1bc9ed4a5f19bc9814928a312a85b200abbdac9039072
|
|
152
152
|
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
|
data/doc/configuration.md
CHANGED
|
@@ -13,6 +13,19 @@ Small examples:
|
|
|
13
13
|
}
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"mcpServers": {
|
|
19
|
+
"safari": {
|
|
20
|
+
"command": "/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver",
|
|
21
|
+
"args": ["--mcp"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
See [MCP servers](mcp.md) for connecting local Model Context Protocol servers such as Safari's browser automation server.
|
|
28
|
+
|
|
16
29
|
```json
|
|
17
30
|
{
|
|
18
31
|
"memory": {
|
|
@@ -263,7 +276,7 @@ Modern mode uses composer-style keys: `Ctrl+S` saves, `Ctrl+Q` quits, `Ctrl+F` s
|
|
|
263
276
|
|
|
264
277
|
Emacs mode uses Emacs-style non-modal keys: `Ctrl+X Ctrl+S` saves, `Ctrl+X Ctrl+C` quits, `Ctrl+S` searches forward, `Ctrl+R` searches backward, `Ctrl+Space` sets the mark, `Ctrl+W` kills the region or previous word, `Alt+W` copies the region, `Ctrl+K` kills to end of line, `Ctrl+Y` yanks, and `Alt+Y` cycles the per-buffer kill ring after a yank.
|
|
265
278
|
|
|
266
|
-
Vibe mode opens files in normal mode and supports a compact
|
|
279
|
+
Vibe mode opens files in normal mode and supports a compact Vim-style subset: normal/insert/command modes, character, line, and block visual modes, `h/j/k/l`, word and WORD movement, counts, `d`/`y`/`c` operators, line yanks and indentation, case operators, marks, macros, jump-list navigation, linewise paste, search, undo/redo, and common `:w`, `:q`, `:q!`, `:wq`, `:x`, `:e`, substitution, and `:number` commands. Yanks also copy to the terminal clipboard when OSC 52 is supported.
|
|
267
280
|
|
|
268
281
|
## Session settings
|
|
269
282
|
|
data/doc/mcp.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# MCP servers
|
|
2
|
+
|
|
3
|
+
Kward can connect to local [Model Context Protocol](https://modelcontextprotocol.io/) servers and expose their tools to the model alongside Kward's built-in workspace tools.
|
|
4
|
+
|
|
5
|
+
This is useful when another app ships an MCP server for something Kward should be able to inspect or control. For example, Safari Technology Preview includes a Safari MCP server that lets agents inspect pages, console output, network activity, screenshots, and other browser state.
|
|
6
|
+
|
|
7
|
+
## Configure a local MCP server
|
|
8
|
+
|
|
9
|
+
Add an `mcpServers` object to your Kward config file:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"safari": {
|
|
15
|
+
"command": "/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver",
|
|
16
|
+
"args": ["--mcp"],
|
|
17
|
+
"enabled": true
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Kward starts enabled MCP servers with stdio transport, discovers their tools, and advertises those tools to the model. Tool names are prefixed with the server name so they do not collide with built-in tools or tools from another MCP server.
|
|
24
|
+
|
|
25
|
+
For the Safari MCP server, install Safari Technology Preview and enable:
|
|
26
|
+
|
|
27
|
+
1. Safari Settings > Advanced > Show features for web developers
|
|
28
|
+
2. Safari Settings > Developer > Enable remote automation and external agents
|
|
29
|
+
|
|
30
|
+
Then ask Kward something like:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
Open http://localhost:3000 in Safari and check the console for errors.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
or:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
Inspect my local app in Safari and tell me why the layout is broken.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Configuration fields
|
|
43
|
+
|
|
44
|
+
Each server supports:
|
|
45
|
+
|
|
46
|
+
- `command` — executable path to launch. Required.
|
|
47
|
+
- `args` — command arguments. Optional.
|
|
48
|
+
- `env` — environment variables to add or override for the server process. Optional.
|
|
49
|
+
- `timeout_seconds` — request timeout for MCP initialization, tool listing, and tool calls. Optional; defaults to 10 seconds.
|
|
50
|
+
- `enabled` — set to `false` to keep a server configured but disabled.
|
|
51
|
+
|
|
52
|
+
Example with an environment variable and custom timeout:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"example": {
|
|
58
|
+
"command": "/usr/local/bin/example-mcp",
|
|
59
|
+
"args": ["--stdio"],
|
|
60
|
+
"env": { "EXAMPLE_MODE": "local" },
|
|
61
|
+
"timeout_seconds": 20
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Notes and limitations
|
|
68
|
+
|
|
69
|
+
- Kward currently supports local stdio MCP servers.
|
|
70
|
+
- Kward currently exposes MCP tools. MCP resources, prompts, sampling, and Streamable HTTP are not implemented yet.
|
|
71
|
+
- MCP tool results are returned to the model as text. Structured content is included as JSON. Image, audio, and resource results are summarized as placeholders for now.
|
|
72
|
+
- MCP servers can expose sensitive local application state. Only configure servers you trust, especially browser automation servers that can read page content or screenshots.
|
data/doc/rpc.md
CHANGED
|
@@ -60,6 +60,7 @@ Detailed capability fields include:
|
|
|
60
60
|
- `auth`: Tauren auth provider format, OpenAI and Anthropic OAuth, OpenRouter API-key login, GitHub/Copilot status reporting, and provider logout for stored credentials. GitHub OAuth login is CLI-only; RPC reports `supported: false` for the GitHub provider with a reason string.
|
|
61
61
|
- `memory`: opt-in structured memory support, interactive prompt injection only, JSON/JSONL local storage, and dedicated `memory/*` methods.
|
|
62
62
|
- `commands`: supported `commands/list` capability for prompt, skill, and plugin command sources, plus plugin execution through `commands/run` or plugin slash turns.
|
|
63
|
+
- `mcp`: local stdio MCP server support through the shared `mcpServers` config. RPC exposes MCP tools to turns; MCP resources, prompts, sampling, and Streamable HTTP are explicitly unsupported for now.
|
|
63
64
|
- `startupResources`: supported startup resource listing for context, skills, prompts, and plugins.
|
|
64
65
|
- `extensionUi`: question bridge support via `ui/question` and `ui/answerQuestion`, plus plugin footer updates via `ui/footer`; other UI primitives are explicitly unsupported.
|
|
65
66
|
- `composer`: composer-only UI features. Interactive session diff totals are explicitly unsupported over RPC (`composer.sessionDiff.supported: false`) because RPC clients already receive per-tool diff results and no live composer status payload is exposed. Clipboard copy is also unsupported over RPC (`composer.copy.supported: false`) because UI clients own clipboard access.
|
data/doc/usage.md
CHANGED
|
@@ -104,6 +104,13 @@ kward --working-directory ~/code/project
|
|
|
104
104
|
kward --working-directory ~/code/project "Summarize this repository"
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
If your main config file is broken and prevents startup, use `--skip-config` to ignore it for one run:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
kward --skip-config doctor
|
|
111
|
+
kward --skip-config edit ~/.kward/config.json
|
|
112
|
+
```
|
|
113
|
+
|
|
107
114
|
## Interactive slash commands
|
|
108
115
|
|
|
109
116
|
Slash commands run local actions in the current session. Most do not send a prompt to the model; exceptions like `/git` and `/workers` orchestrate local flows that may then trigger model work.
|
data/lib/kward/cli/commands.rb
CHANGED
|
@@ -77,6 +77,7 @@ module Kward
|
|
|
77
77
|
#{option.call("--working-directory=PATH")} Run Kward from PATH
|
|
78
78
|
#{option.call("--mode=MODE")} Execution mode: auto, chat, oneshot, filter
|
|
79
79
|
#{option.call("--filter")} Shortcut for --mode filter
|
|
80
|
+
#{option.call("--skip-config")} Ignore the main config file for this run
|
|
80
81
|
#{option.call("--help")}, #{option.call("-h")} Show this help
|
|
81
82
|
#{option.call("--version")}, #{option.call("-v")} Show the installed version
|
|
82
83
|
|
|
@@ -214,6 +215,8 @@ module Kward
|
|
|
214
215
|
break
|
|
215
216
|
when "--experimental-workers"
|
|
216
217
|
@experimental_workers = true
|
|
218
|
+
when "--skip-config"
|
|
219
|
+
@skip_config = true
|
|
217
220
|
when "--filter"
|
|
218
221
|
@requested_mode = "filter"
|
|
219
222
|
when "--mode"
|
data/lib/kward/cli/doctor.rb
CHANGED
|
@@ -16,24 +16,27 @@ module Kward
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def doctor_checks
|
|
19
|
-
|
|
19
|
+
config_result = safely_read_config
|
|
20
|
+
config = config_result.is_a?(Hash) ? config_result : {}
|
|
20
21
|
[
|
|
21
22
|
doctor_config_check,
|
|
22
|
-
doctor_config_json_check(
|
|
23
|
+
doctor_config_json_check(config_result),
|
|
23
24
|
doctor_directory_check("Config directory", ConfigFiles.config_dir),
|
|
24
25
|
doctor_directory_check("Session directory", SessionStore.new(cwd: current_workspace_root).session_dir, create: true),
|
|
25
26
|
doctor_workspace_check,
|
|
26
27
|
doctor_model_check,
|
|
27
28
|
doctor_auth_check(config),
|
|
28
|
-
doctor_pan_check(
|
|
29
|
+
doctor_pan_check(config_result),
|
|
29
30
|
{ status: :ok, label: "Color", message: @color_enabled ? "enabled" : "disabled" }
|
|
30
31
|
]
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def safely_read_config
|
|
34
35
|
ConfigFiles.read_config
|
|
35
|
-
rescue
|
|
36
|
-
|
|
36
|
+
rescue ConfigFiles::ConfigError => e
|
|
37
|
+
e
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
e
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
def doctor_config_check
|
|
@@ -46,10 +49,13 @@ module Kward
|
|
|
46
49
|
{ status: :warning, label: "Config", message: "not found: #{path}" }
|
|
47
50
|
end
|
|
48
51
|
|
|
49
|
-
def doctor_config_json_check(
|
|
50
|
-
return { status: :
|
|
52
|
+
def doctor_config_json_check(config_result)
|
|
53
|
+
return { status: :ok, label: "Config JSON", message: "valid" } if config_result.is_a?(Hash)
|
|
54
|
+
if config_result.is_a?(ConfigFiles::ConfigError)
|
|
55
|
+
return { status: :error, label: "Config JSON", message: "invalid: #{config_result.detail}" }
|
|
56
|
+
end
|
|
51
57
|
|
|
52
|
-
{ status: :
|
|
58
|
+
{ status: :error, label: "Config JSON", message: "unreadable: #{config_result.message}" }
|
|
53
59
|
end
|
|
54
60
|
|
|
55
61
|
def doctor_directory_check(label, path, create: false)
|
|
@@ -96,8 +102,10 @@ module Kward
|
|
|
96
102
|
{ status: :warning, label: "Auth", message: "no saved credentials found; run `kward login`" }
|
|
97
103
|
end
|
|
98
104
|
|
|
99
|
-
def doctor_pan_check(
|
|
100
|
-
|
|
105
|
+
def doctor_pan_check(config_result)
|
|
106
|
+
return { status: :warning, label: "Pan mode", message: "skipped because config is invalid" } if config_result.is_a?(ConfigFiles::ConfigError)
|
|
107
|
+
|
|
108
|
+
pan = config_result.to_h["pan_mode"] || {}
|
|
101
109
|
if !pan["username"].to_s.empty? && !pan["password"].to_s.empty?
|
|
102
110
|
{ status: :ok, label: "Pan mode", message: "credentials configured" }
|
|
103
111
|
else
|
|
@@ -184,11 +184,13 @@ module Kward
|
|
|
184
184
|
poll_result = @prompt.poll_input
|
|
185
185
|
case poll_result
|
|
186
186
|
when String
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if slash_command_input?(poll_result)
|
|
187
|
+
if busy_queued_command?(poll_result)
|
|
190
188
|
queued_inputs << poll_result
|
|
191
189
|
@prompt.set_queued_count(queued_inputs.length) if @prompt.respond_to?(:set_queued_count)
|
|
190
|
+
elsif slash_command_input?(poll_result)
|
|
191
|
+
# Slash commands are local control actions. Running or queuing them
|
|
192
|
+
# from the busy composer is surprising because the state they act on
|
|
193
|
+
# may have changed by the time the active turn finishes.
|
|
192
194
|
elsif steering && !poll_result.strip.empty?
|
|
193
195
|
begin
|
|
194
196
|
steering.submit(poll_result)
|
|
@@ -226,6 +228,10 @@ module Kward
|
|
|
226
228
|
input.to_s.strip.start_with?("/")
|
|
227
229
|
end
|
|
228
230
|
|
|
231
|
+
def busy_queued_command?(input)
|
|
232
|
+
["/exit", "/quit", "/new"].include?(input.to_s.strip)
|
|
233
|
+
end
|
|
234
|
+
|
|
229
235
|
def handle_busy_worker_input(input, agent, queued_inputs)
|
|
230
236
|
return false unless agent
|
|
231
237
|
|
data/lib/kward/cli/tabs.rb
CHANGED
|
@@ -488,18 +488,15 @@ module Kward
|
|
|
488
488
|
end
|
|
489
489
|
|
|
490
490
|
def handle_tab_busy_input(tab, input)
|
|
491
|
-
|
|
492
|
-
if command == "/workers" || command.start_with?("/workers ") || command == "/tab" || command.start_with?("/tab ")
|
|
493
|
-
_handled, replacement_agent = handle_local_slash_command(command, tab.agent, @session_store)
|
|
494
|
-
tab.agent = replacement_agent if replacement_agent?(replacement_agent)
|
|
495
|
-
restore_busy_input_prompt
|
|
496
|
-
return
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
if slash_command_input?(input)
|
|
491
|
+
if busy_queued_command?(input)
|
|
500
492
|
tab.queued_inputs << input
|
|
501
493
|
@prompt.set_queued_count(tab.queued_inputs.length) if @prompt.respond_to?(:set_queued_count)
|
|
502
494
|
return
|
|
495
|
+
elsif slash_command_input?(input)
|
|
496
|
+
# Slash commands are local control actions. Running or queuing them
|
|
497
|
+
# from the busy composer is surprising because the state they act on
|
|
498
|
+
# may have changed by the time the active turn finishes.
|
|
499
|
+
return
|
|
503
500
|
end
|
|
504
501
|
|
|
505
502
|
if tab.steering && !input.to_s.strip.empty?
|
data/lib/kward/cli.rb
CHANGED
|
@@ -95,7 +95,7 @@ module Kward
|
|
|
95
95
|
include CLI::InteractiveTurn
|
|
96
96
|
include CLI::ToolSummaries
|
|
97
97
|
|
|
98
|
-
def initialize(argv: ARGV, stdin: STDIN, prompt: TTY::Prompt.new, client:
|
|
98
|
+
def initialize(argv: ARGV, stdin: STDIN, prompt: TTY::Prompt.new, client: nil, session_store: nil, context_usage: ContextUsage.new)
|
|
99
99
|
@argv = argv
|
|
100
100
|
@stdin = stdin
|
|
101
101
|
@prompt = prompt
|
|
@@ -109,6 +109,7 @@ module Kward
|
|
|
109
109
|
@working_directory = nil
|
|
110
110
|
@prompt_delimited = false
|
|
111
111
|
@requested_mode = "auto"
|
|
112
|
+
@skip_config = false
|
|
112
113
|
@experimental_workers = false
|
|
113
114
|
@foreground_turn_active = false
|
|
114
115
|
@pending_reasoning_config = nil
|
|
@@ -122,11 +123,41 @@ module Kward
|
|
|
122
123
|
# @return [void]
|
|
123
124
|
def run
|
|
124
125
|
@argv = extract_global_options(@argv)
|
|
126
|
+
ConfigFiles.skip_config = @skip_config
|
|
125
127
|
with_working_directory { dispatch }
|
|
128
|
+
rescue ConfigFiles::ConfigError => e
|
|
129
|
+
warn config_error_message(e)
|
|
130
|
+
exit 1
|
|
126
131
|
rescue ArgumentError => e
|
|
127
132
|
warn e.message
|
|
128
133
|
warn "Run `kward help` for available commands."
|
|
129
134
|
exit 1
|
|
135
|
+
ensure
|
|
136
|
+
ConfigFiles.skip_config = false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def ensure_client!
|
|
140
|
+
@client ||= Client.new
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def config_error_message(error)
|
|
144
|
+
<<~MESSAGE.rstrip
|
|
145
|
+
Invalid Kward config #{error.format}.
|
|
146
|
+
|
|
147
|
+
File:
|
|
148
|
+
#{error.path}
|
|
149
|
+
|
|
150
|
+
Parser error:
|
|
151
|
+
#{error.detail}
|
|
152
|
+
|
|
153
|
+
Kward cannot safely continue with this config.
|
|
154
|
+
|
|
155
|
+
Repair it with:
|
|
156
|
+
kward edit #{error.path}
|
|
157
|
+
|
|
158
|
+
Emergency fallback:
|
|
159
|
+
kward --skip-config doctor
|
|
160
|
+
MESSAGE
|
|
130
161
|
end
|
|
131
162
|
|
|
132
163
|
def dispatch
|
|
@@ -173,6 +204,7 @@ module Kward
|
|
|
173
204
|
end
|
|
174
205
|
raise ArgumentError, command_usage("doctor") unless @argv.length == 1
|
|
175
206
|
|
|
207
|
+
ensure_client!
|
|
176
208
|
print_doctor
|
|
177
209
|
return
|
|
178
210
|
end
|
|
@@ -194,6 +226,7 @@ module Kward
|
|
|
194
226
|
return
|
|
195
227
|
end
|
|
196
228
|
|
|
229
|
+
ensure_client!
|
|
197
230
|
print_sysprompt(@argv[1..] || [])
|
|
198
231
|
return
|
|
199
232
|
end
|
|
@@ -205,6 +238,7 @@ module Kward
|
|
|
205
238
|
end
|
|
206
239
|
raise ArgumentError, command_usage("rpc") unless @argv.length == 1
|
|
207
240
|
|
|
241
|
+
ensure_client!
|
|
208
242
|
Kward::RPC::Server.new(input: @stdin, output: $stdout, client: @client, experimental_workers: @experimental_workers).run
|
|
209
243
|
return
|
|
210
244
|
end
|
|
@@ -226,6 +260,7 @@ module Kward
|
|
|
226
260
|
return
|
|
227
261
|
end
|
|
228
262
|
|
|
263
|
+
ensure_client!
|
|
229
264
|
handle_openrouter_command(@argv[1..] || [])
|
|
230
265
|
return
|
|
231
266
|
end
|
|
@@ -237,6 +272,7 @@ module Kward
|
|
|
237
272
|
end
|
|
238
273
|
raise ArgumentError, command_usage("pan") unless @argv.length == 1
|
|
239
274
|
|
|
275
|
+
ensure_client!
|
|
240
276
|
PanServer.new(client: @client, working_directory: current_workspace_root).run
|
|
241
277
|
return
|
|
242
278
|
end
|
|
@@ -267,6 +303,7 @@ module Kward
|
|
|
267
303
|
end
|
|
268
304
|
|
|
269
305
|
def run_prompt_or_interactive
|
|
306
|
+
ensure_client!
|
|
270
307
|
stdin_input = read_stdin_input
|
|
271
308
|
first_prompt = one_shot_prompt_argument
|
|
272
309
|
|
data/lib/kward/config_files.rb
CHANGED
|
@@ -22,6 +22,17 @@ module Kward
|
|
|
22
22
|
# config, prompt, skill, plugin, cache, memory, and session locations instead of
|
|
23
23
|
# reconstructing `~/.kward` paths independently.
|
|
24
24
|
module ConfigFiles
|
|
25
|
+
class ConfigError < StandardError
|
|
26
|
+
attr_reader :path, :format, :detail
|
|
27
|
+
|
|
28
|
+
def initialize(path:, format:, detail:)
|
|
29
|
+
@path = path
|
|
30
|
+
@format = format
|
|
31
|
+
@detail = detail
|
|
32
|
+
super("Invalid Kward config #{format}: #{path}: #{detail}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
25
36
|
MAX_SKILL_FILE_BYTES = 100_000
|
|
26
37
|
MAX_PROMPT_FILE_BYTES = 32 * 1024
|
|
27
38
|
DEFAULT_OVERLAY_SETTINGS = { "alignment" => "center", "width" => "maximum" }.freeze
|
|
@@ -45,8 +56,18 @@ module Kward
|
|
|
45
56
|
end
|
|
46
57
|
end
|
|
47
58
|
|
|
59
|
+
@skip_config = false
|
|
60
|
+
|
|
48
61
|
module_function
|
|
49
62
|
|
|
63
|
+
def skip_config=(value)
|
|
64
|
+
@skip_config = value
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def skip_config?
|
|
68
|
+
@skip_config == true
|
|
69
|
+
end
|
|
70
|
+
|
|
50
71
|
# Directory that contains Kward's user config and adjacent prompt/skill
|
|
51
72
|
# data. Defaults to `~/.kward`, or the directory of `KWARD_CONFIG_PATH`.
|
|
52
73
|
#
|
|
@@ -94,6 +115,7 @@ module Kward
|
|
|
94
115
|
"auto_resume" => false
|
|
95
116
|
},
|
|
96
117
|
"enforce_workspace_agents_file" => false,
|
|
118
|
+
"mcpServers" => {},
|
|
97
119
|
"tools" => {
|
|
98
120
|
"workspace_guardrails" => true
|
|
99
121
|
}
|
|
@@ -103,6 +125,7 @@ module Kward
|
|
|
103
125
|
# Performs ensure default config for configuration file and path handling.
|
|
104
126
|
def ensure_default_config!(path = config_path)
|
|
105
127
|
path = File.expand_path(path)
|
|
128
|
+
return false if skip_config? && path == config_path
|
|
106
129
|
return false if File.exist?(path)
|
|
107
130
|
|
|
108
131
|
write_config(default_config, path)
|
|
@@ -156,11 +179,12 @@ module Kward
|
|
|
156
179
|
# @return [Hash] parsed config object
|
|
157
180
|
def read_config(path = config_path)
|
|
158
181
|
path = File.expand_path(path)
|
|
182
|
+
return {} if skip_config? && path == config_path
|
|
159
183
|
return {} unless File.exist?(path)
|
|
160
184
|
|
|
161
185
|
JSON.parse(File.read(path))
|
|
162
|
-
rescue JSON::ParserError
|
|
163
|
-
raise
|
|
186
|
+
rescue JSON::ParserError => e
|
|
187
|
+
raise ConfigError.new(path: path, format: "JSON", detail: e.message)
|
|
164
188
|
end
|
|
165
189
|
|
|
166
190
|
# Writes config JSON using private file permissions.
|
|
@@ -168,6 +192,9 @@ module Kward
|
|
|
168
192
|
# @param config [Hash] config object to persist
|
|
169
193
|
# @param path [String] config file path
|
|
170
194
|
def write_config(config, path = config_path)
|
|
195
|
+
path = File.expand_path(path)
|
|
196
|
+
raise "Cannot write Kward config while --skip-config is active: #{path}" if skip_config? && path == config_path
|
|
197
|
+
|
|
171
198
|
PrivateFile.write_json(path, config)
|
|
172
199
|
end
|
|
173
200
|
|
|
@@ -361,6 +388,12 @@ module Kward
|
|
|
361
388
|
value.is_a?(Hash) ? value : {}
|
|
362
389
|
end
|
|
363
390
|
|
|
391
|
+
# Returns configured MCP stdio servers, or an empty config when absent.
|
|
392
|
+
def mcp_servers(config = read_config)
|
|
393
|
+
value = config["mcpServers"] || config.dig("mcp", "servers")
|
|
394
|
+
value.is_a?(Hash) ? value : {}
|
|
395
|
+
end
|
|
396
|
+
|
|
364
397
|
# Validates and persists terminal overlay settings.
|
|
365
398
|
def update_overlay_settings(values)
|
|
366
399
|
raise "Overlay settings must be an object" unless values.is_a?(Hash)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require_relative "stdio_transport"
|
|
2
|
+
|
|
3
|
+
# Namespace for the Kward CLI agent runtime.
|
|
4
|
+
module Kward
|
|
5
|
+
# Model Context Protocol client support.
|
|
6
|
+
module MCP
|
|
7
|
+
PROTOCOL_VERSION = "2025-11-25"
|
|
8
|
+
|
|
9
|
+
# Minimal MCP client for discovering and invoking server tools.
|
|
10
|
+
class Client
|
|
11
|
+
attr_reader :name
|
|
12
|
+
|
|
13
|
+
def initialize(name:, transport:)
|
|
14
|
+
@name = name.to_s
|
|
15
|
+
@transport = transport
|
|
16
|
+
@initialized = false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize!
|
|
20
|
+
return if @initialized
|
|
21
|
+
|
|
22
|
+
@transport.request("initialize", initialize_params)
|
|
23
|
+
@transport.notify("notifications/initialized")
|
|
24
|
+
@initialized = true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def list_tools
|
|
28
|
+
initialize!
|
|
29
|
+
result = @transport.request("tools/list", {})
|
|
30
|
+
Array(result["tools"] || result[:tools])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call_tool(name, arguments = {})
|
|
34
|
+
initialize!
|
|
35
|
+
@transport.request("tools/call", { name: name, arguments: arguments || {} })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def close
|
|
39
|
+
@transport.close
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def initialize_params
|
|
45
|
+
{
|
|
46
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
47
|
+
capabilities: {},
|
|
48
|
+
clientInfo: {
|
|
49
|
+
name: "kward",
|
|
50
|
+
version: Kward.const_defined?(:VERSION) ? Kward::VERSION : "0.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require_relative "../config_files"
|
|
2
|
+
require_relative "client"
|
|
3
|
+
require_relative "stdio_transport"
|
|
4
|
+
|
|
5
|
+
# Namespace for the Kward CLI agent runtime.
|
|
6
|
+
module Kward
|
|
7
|
+
# Model Context Protocol client support.
|
|
8
|
+
module MCP
|
|
9
|
+
# Builds MCP clients from Kward configuration.
|
|
10
|
+
module ServerConfig
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def clients_from_config(config)
|
|
14
|
+
servers = ConfigFiles.mcp_servers(config)
|
|
15
|
+
|
|
16
|
+
servers.filter_map do |name, settings|
|
|
17
|
+
client_from_settings(name, settings)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def client_from_settings(name, settings)
|
|
22
|
+
return nil unless settings.is_a?(Hash)
|
|
23
|
+
return nil if settings["enabled"] == false
|
|
24
|
+
|
|
25
|
+
command = settings["command"].to_s
|
|
26
|
+
return nil if command.empty?
|
|
27
|
+
|
|
28
|
+
Client.new(
|
|
29
|
+
name: name,
|
|
30
|
+
transport: StdioTransport.new(
|
|
31
|
+
command: command,
|
|
32
|
+
args: settings["args"] || [],
|
|
33
|
+
env: normalized_env(settings["env"]),
|
|
34
|
+
timeout_seconds: positive_number(settings["timeout_seconds"] || settings["timeoutSeconds"])
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def normalized_env(value)
|
|
40
|
+
return {} unless value.is_a?(Hash)
|
|
41
|
+
|
|
42
|
+
value.each_with_object({}) do |(key, item), env|
|
|
43
|
+
env[key.to_s] = item.to_s
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def positive_number(value)
|
|
48
|
+
number = value.to_f
|
|
49
|
+
number.positive? ? number : nil
|
|
50
|
+
rescue StandardError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|