kward 0.71.0 → 0.72.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 +41 -1
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/doc/agent-tools.md +15 -6
- data/doc/authentication.md +22 -1
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +106 -3
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +16 -3
- data/doc/editor.md +394 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +122 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +72 -1
- data/doc/releasing.md +37 -9
- data/doc/rpc.md +74 -4
- data/doc/session-management.md +35 -1
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +53 -7
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +51 -12
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +33 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +32 -1
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +218 -9
- data/lib/kward/cli/slash_commands.rb +415 -2
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +158 -26
- data/lib/kward/config_files.rb +123 -1
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +12 -4
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/composer_controller.rb +69 -1
- data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
- data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
- data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
- data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
- data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
- data/lib/kward/prompt_interface/editor/search.rb +76 -0
- data/lib/kward/prompt_interface/editor/selections.rb +120 -0
- data/lib/kward/prompt_interface/editor/state.rb +1249 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
- data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
- data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
- data/lib/kward/prompt_interface/file_overlay.rb +211 -0
- data/lib/kward/prompt_interface/git_prompt.rb +299 -0
- data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
- data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
- data/lib/kward/prompt_interface/interactive/state.rb +62 -0
- data/lib/kward/prompt_interface/key_handler.rb +387 -35
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/question_prompt.rb +98 -50
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +16 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
- data/lib/kward/prompt_interface.rb +286 -8
- data/lib/kward/prompts/commands.rb +5 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/server.rb +42 -3
- data/lib/kward/rpc/session_manager.rb +35 -47
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/session_store.rb +44 -0
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +202 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +62 -16
- data/lib/kward/tools/tool_call.rb +10 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +68 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/store.rb +72 -0
- data/lib/kward/workers/tool_policy.rb +23 -0
- data/lib/kward/workers/worker.rb +82 -0
- data/lib/kward/workers/write_lock.rb +38 -0
- data/lib/kward/workers.rb +7 -0
- data/lib/kward/workspace.rb +110 -24
- data/templates/default/fulldoc/html/css/kward.css +107 -36
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +4 -2
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +53 -1
data/doc/usage.md
CHANGED
|
@@ -12,7 +12,7 @@ cd ~/code/project
|
|
|
12
12
|
kward
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
When running from source, replace `kward` with `ruby lib/main.rb`.
|
|
15
|
+
When running from source, replace `kward` with `ruby lib/main.rb`. See [Getting started](getting-started.md) for install and sign-in.
|
|
16
16
|
|
|
17
17
|
## Common workflows
|
|
18
18
|
|
|
@@ -42,6 +42,14 @@ For larger reviews, use interactive mode so Kward can inspect related files:
|
|
|
42
42
|
Review the current git diff. If something looks risky, inspect the relevant files before recommending changes.
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
To review and commit changes interactively, use `/git`:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
/git
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
It opens a staging overlay where you can stage or unstage files, preview diffs, and write a commit message without leaving Kward.
|
|
52
|
+
|
|
45
53
|
### Make a small code change
|
|
46
54
|
|
|
47
55
|
```text
|
|
@@ -64,6 +72,14 @@ Or run a shell command yourself from the composer by prefixing it with `!`:
|
|
|
64
72
|
!git status --short
|
|
65
73
|
```
|
|
66
74
|
|
|
75
|
+
For several commands, enter the embedded Kward shell:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
/shell
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`/shell` opens `ekwsh`, a Kward-native command mode. Kward keeps the tab bar, composer editing, and transcript rendering while each command runs through your configured shell. Built-ins such as `cd`, `pwd`, `export`, `unset`, `alias`, `clear`, and `exit` maintain shell-mode state between commands. Plain Tab completes built-in command names, configured aliases, executables from `$PATH`, and file paths from the shell's current directory; `cd` completion suggests directories only. `ekwsh` preserves safe ANSI SGR color/style output while stripping terminal-control sequences that could corrupt the TUI, and sets conservative color-friendly environment defaults such as `CLICOLOR=1`, `COLORTERM=truecolor`, and `TERM=xterm-256color` when needed. It does not force color by default; set `FORCE_COLOR`, `CLICOLOR_FORCE`, or command-specific flags such as `--color=always` if you want more aggressive color output. You can set global shell env vars and aliases in `~/.kward/ekwsh.yml`; see [Configuration](configuration.md). It is intended for command output, not full-screen interactive programs such as editors or pagers.
|
|
82
|
+
|
|
67
83
|
## Shell commands
|
|
68
84
|
|
|
69
85
|
Useful shell commands:
|
|
@@ -72,10 +88,12 @@ Useful shell commands:
|
|
|
72
88
|
kward # start interactive chat
|
|
73
89
|
kward "Explain this project" # ask one question and exit
|
|
74
90
|
kward help # show commands and examples
|
|
91
|
+
kward version # show the installed version
|
|
75
92
|
kward doctor # check local setup
|
|
76
93
|
kward login # sign in or save credentials
|
|
77
94
|
kward auth status # show credential status without secrets
|
|
78
95
|
kward sysprompt # inspect assembled instructions
|
|
96
|
+
kward stats tokens # export local token telemetry as CSV
|
|
79
97
|
kward rpc # start the experimental RPC backend
|
|
80
98
|
```
|
|
81
99
|
|
|
@@ -88,15 +106,26 @@ kward --working-directory ~/code/project "Summarize this repository"
|
|
|
88
106
|
|
|
89
107
|
## Interactive slash commands
|
|
90
108
|
|
|
91
|
-
|
|
109
|
+
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.
|
|
92
110
|
|
|
93
111
|
| Command | Use it when you want to... |
|
|
94
112
|
| --- | --- |
|
|
95
113
|
| `/login` | sign in or save provider credentials. |
|
|
96
114
|
| `/model` | choose the active model. |
|
|
97
115
|
| `/reasoning` | choose reasoning effort. |
|
|
116
|
+
| `/git` | review uncommitted changes, stage files, and commit. |
|
|
117
|
+
| `/files` | browse project files in a nested tree and open them in the editor. |
|
|
118
|
+
| `/shell` | run workspace commands in the embedded Kward shell. |
|
|
119
|
+
| `/settings` | configure prompt overlays. |
|
|
98
120
|
| `/status` | see session, model, and context status. |
|
|
99
|
-
| `/new` | start a fresh session. |
|
|
121
|
+
| `/new` | start a fresh session in the current tab. |
|
|
122
|
+
| `/tab 2` | switch to tab 2. |
|
|
123
|
+
| `/tab move 1` | move the active tab to position 1. |
|
|
124
|
+
| `/tab move left` | move the active tab one position left. |
|
|
125
|
+
| `/tab move right` | move the active tab one position right. |
|
|
126
|
+
| `/tab close` | close the active tab. |
|
|
127
|
+
| `/tab new` | open a new tab. |
|
|
128
|
+
| `/tab name <label>` | rename the active tab label. |
|
|
100
129
|
| `/sessions` | open the saved sessions picker or continue a previous session by path. |
|
|
101
130
|
| `/resume` | alias for `/sessions`. |
|
|
102
131
|
| `/name <name>` | name or clear the current session. |
|
|
@@ -107,13 +136,23 @@ Use slash commands for local actions that should not go to the model:
|
|
|
107
136
|
| `/copy last` | copy the latest assistant answer. |
|
|
108
137
|
| `/copy transcript` | copy the transcript as Markdown. |
|
|
109
138
|
| `/export notes.md` | write the transcript to a Markdown file. |
|
|
110
|
-
| `/compact [
|
|
139
|
+
| `/compact [instructions]` | summarize older context so a long chat can continue. |
|
|
111
140
|
| `/memory ...` | manage opt-in memory. |
|
|
112
141
|
| `/redraw` | fix terminal drawing after resize or glitches. |
|
|
142
|
+
| `/reload` | reload installed plugins. |
|
|
143
|
+
| `/workers` | open the experimental worker pipeline (`new`, `do <task>`, or `list`). |
|
|
113
144
|
| `/exit` | leave Kward. |
|
|
114
145
|
|
|
115
146
|
Prompt templates and plugins can add more slash commands.
|
|
116
147
|
|
|
148
|
+
## Prompt history
|
|
149
|
+
|
|
150
|
+
In interactive mode, Kward keeps prompt history per workspace under `~/.kward/history/`. Press Up/Down to recall previous prompts across restarts.
|
|
151
|
+
|
|
152
|
+
Press `Ctrl-R` to search history. Type a fuzzy query in the composer, use Up/Down to choose a result from the overlay, then press Enter to place it back in the composer for editing or resubmission. Press Esc or Ctrl-C to cancel the search and restore the draft.
|
|
153
|
+
|
|
154
|
+
`$path` editor-open commands are also saved after a file opens successfully, using the resolved workspace-relative path.
|
|
155
|
+
|
|
117
156
|
## Sessions
|
|
118
157
|
|
|
119
158
|
Interactive chats are saved as workspace-scoped sessions under:
|
|
@@ -149,11 +188,18 @@ One-shot prompts are best for short tasks that do not need session history:
|
|
|
149
188
|
|
|
150
189
|
```bash
|
|
151
190
|
kward "What does this repository do?"
|
|
191
|
+
cat error.log | kward
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
When stdin and a prompt are both present, Kward runs in filter mode: stdin is the input, the prompt is the instruction, and stdout contains only the transformed result. You can also force this with `--filter` or `--mode filter`.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
152
197
|
git diff | kward "Review this diff"
|
|
153
|
-
|
|
198
|
+
echo "Hello" | kward --filter "Translate to German"
|
|
199
|
+
kward --mode filter "Indent this JSON" < unindented.json
|
|
154
200
|
```
|
|
155
201
|
|
|
156
|
-
Use `--` when your prompt starts with something that could be parsed as a command or option:
|
|
202
|
+
Use `--mode chat`, `--mode oneshot`, or `--mode filter` to override automatic mode detection. Use `--` when your prompt starts with something that could be parsed as a command or option:
|
|
157
203
|
|
|
158
204
|
```bash
|
|
159
205
|
kward -- explain --working-directory
|
|
@@ -181,7 +227,7 @@ Important guardrails:
|
|
|
181
227
|
|
|
182
228
|
## Images
|
|
183
229
|
|
|
184
|
-
If the active model supports images, Kward can attach image paths, Markdown image links, `file://` URLs, or image data URLs pasted into the composer.
|
|
230
|
+
If the active model supports images, Kward can attach image paths, Markdown image links, `file://` URLs, or image data URLs pasted into the composer. Supported formats are GIF, JPEG, PNG, and WebP, up to 20 MB per image.
|
|
185
231
|
|
|
186
232
|
Use this for tasks such as:
|
|
187
233
|
|
data/doc/web-search.md
CHANGED
|
@@ -36,16 +36,18 @@ Kward should search first, then fetch important pages before relying on them.
|
|
|
36
36
|
|
|
37
37
|
## Network behavior
|
|
38
38
|
|
|
39
|
-
Web tools are advertised to the model by default. Queries and fetched URLs are sent over the network to the selected provider or target host.
|
|
39
|
+
Web tools are advertised to the model by default. Queries and fetched URLs are sent over the network to the selected provider or target host. See [Configuration](configuration.md) for the full `web_search` config reference including API key storage and the `provider` config setting.
|
|
40
40
|
|
|
41
41
|
In automatic mode, provider fallback is:
|
|
42
42
|
|
|
43
43
|
1. Exa API when `EXA_API_KEY` is configured, otherwise keyless Exa MCP.
|
|
44
|
-
2. Perplexity API when configured and model-provider fallback is allowed.
|
|
45
|
-
3. Gemini API with Google Search grounding when configured and model-provider fallback is allowed.
|
|
44
|
+
2. Perplexity API when `PERPLEXITY_API_KEY` is configured and model-provider fallback is allowed.
|
|
45
|
+
3. Gemini API with Google Search grounding when `GEMINI_API_KEY` is configured and model-provider fallback is allowed.
|
|
46
46
|
4. DuckDuckGo HTML search, then bundled public SearXNG instances.
|
|
47
47
|
|
|
48
|
-
You do not need an API key for basic web search, but keys can improve limits or provider choice.
|
|
48
|
+
You do not need an API key for basic web search, but keys can improve limits or provider choice. The default provider can also be set in config as `web_search.provider`; the tool argument overrides it for a single call.
|
|
49
|
+
|
|
50
|
+
Search output is capped at 8 KB total, with excerpts up to 300 characters and answer text up to 2,000 characters. HTTP requests use a 10-second timeout.
|
|
49
51
|
|
|
50
52
|
## Disable web tools
|
|
51
53
|
|
|
@@ -83,6 +85,10 @@ Arguments:
|
|
|
83
85
|
- `max_bytes`: default 16384, capped at 131072.
|
|
84
86
|
- `extract`: optional `auto`, `text`, or `markdown`.
|
|
85
87
|
|
|
88
|
+
In `auto` mode (default), Kward detects HTML content and extracts readable text by stripping scripts, styles, navigation, and forms, preserving headings, paragraphs, lists, code blocks, and blockquotes. Non-HTML content is returned as cleaned text. Use `markdown` to format extracted headings and code blocks as Markdown, or `text` for plain text without formatting.
|
|
89
|
+
|
|
90
|
+
Fetches follow up to 5 redirects and use a 10-second HTTP timeout.
|
|
91
|
+
|
|
86
92
|
### `fetch_raw`
|
|
87
93
|
|
|
88
94
|
Reads a specific HTTP or HTTPS resource without readability extraction. Use it for JSON, YAML, XML, RSS, OpenAPI specs, and plain text.
|
|
@@ -92,3 +98,5 @@ Arguments:
|
|
|
92
98
|
- `url`
|
|
93
99
|
- `max_bytes`: default 16384, capped at 131072.
|
|
94
100
|
- `accept`: optional HTTP `Accept` header.
|
|
101
|
+
|
|
102
|
+
Fetches follow up to 5 redirects and use a 10-second HTTP timeout.
|
data/doc/workspace-tools.md
CHANGED
|
@@ -12,14 +12,14 @@ Kward normally chooses these tools itself. You do not need to know their exact n
|
|
|
12
12
|
|
|
13
13
|
## Guardrails
|
|
14
14
|
|
|
15
|
-
Workspace tools use the active workspace as their boundary. File paths are workspace-relative by default, and file tools are guarded so Kward does not edit arbitrary unread files.
|
|
15
|
+
Workspace tools use the active workspace as their boundary. File paths are workspace-relative by default, and file tools are guarded so Kward does not edit arbitrary unread files. Guardrails can be disabled with the `tools.workspace_guardrails` setting — see [Configuration](configuration.md). When disabled, file tools can access paths outside the workspace, but shell commands are unaffected since they already run as your OS user.
|
|
16
16
|
|
|
17
17
|
Important behavior:
|
|
18
18
|
|
|
19
19
|
- Existing files must be read in the current conversation before `write_file` or `edit_file` can change them.
|
|
20
|
-
- Reads are bounded to avoid pulling very large files into context by accident.
|
|
20
|
+
- Reads are bounded to avoid pulling very large files into context by accident. Files larger than 256 KB cannot be read or edited. Read output is capped at 50 KB or 2,000 lines, whichever comes first.
|
|
21
21
|
- Edits use exact text replacement, so accidental partial or fuzzy changes fail instead of guessing.
|
|
22
|
-
- Shell commands run as your operating-system user from the workspace. They are powerful and should be treated like commands you run yourself.
|
|
22
|
+
- Shell commands run as your operating-system user from the workspace. They are powerful and should be treated like commands you run yourself. Command output is capped at 128 KB.
|
|
23
23
|
|
|
24
24
|
## Reading the workspace
|
|
25
25
|
|
|
@@ -33,25 +33,55 @@ Arguments:
|
|
|
33
33
|
|
|
34
34
|
### `read_file`
|
|
35
35
|
|
|
36
|
-
Reads a workspace text file. Output is capped, and Kward can continue with line offsets when it needs more detail.
|
|
36
|
+
Reads a workspace text file. Output is capped, and Kward can continue with line offsets when it needs more detail. The tool also supports explicit context modes so Kward can start cheap and widen only when needed.
|
|
37
37
|
|
|
38
38
|
Arguments:
|
|
39
39
|
|
|
40
40
|
- `path`: workspace-relative file path.
|
|
41
41
|
- `offset`: optional 1-indexed start line.
|
|
42
42
|
- `limit`: optional maximum number of lines.
|
|
43
|
+
- `mode`: optional context mode:
|
|
44
|
+
- `preview`: read a short preview slice, defaulting to 120 lines when `limit` is omitted.
|
|
45
|
+
- `outline`: return only the source declaration outline.
|
|
46
|
+
- `range`: read the requested `offset`/`limit` slice.
|
|
47
|
+
- `full`: read from `offset` until Kward's read caps stop the response.
|
|
48
|
+
- `max_bytes`: optional per-call byte budget, capped by Kward's workspace read limit.
|
|
43
49
|
|
|
44
50
|
A successful read marks the resolved file path as read for the conversation, allowing later edits to that file.
|
|
45
51
|
|
|
52
|
+
When called without `offset`, `limit`, or `mode` on a file that exceeds 2,000 lines or 50 KB, Kward returns a structure outline (classes, modules, methods) plus the first 120 lines instead of truncating blindly. This helps identify relevant entry points before requesting specific line ranges.
|
|
53
|
+
|
|
54
|
+
Binary files (detected by null bytes) return an error instead of content.
|
|
55
|
+
|
|
56
|
+
### `context_budget_stats`
|
|
57
|
+
|
|
58
|
+
Returns approximate context-budget savings for the current active conversation since it was opened in this process. The report compares each raw tool result size with the model-facing result after compaction or duplicate replacement, includes per-tool totals, and estimates saved tokens using roughly four bytes per token.
|
|
59
|
+
|
|
60
|
+
Arguments: none.
|
|
61
|
+
|
|
62
|
+
These numbers are intentionally approximate runtime stats. They are useful for seeing whether Kward's budgeting is helping the current conversation, not for billing, and they are not reconstructed when a saved session is resumed.
|
|
63
|
+
|
|
64
|
+
### `context_for_task`
|
|
65
|
+
|
|
66
|
+
Builds a compact, task-shaped context bundle from likely workspace files. It ranks files by task terms, includes source outlines, and adds short matching excerpts while respecting an approximate byte budget. This is intentionally lightweight: it does not maintain a persistent index or semantic graph.
|
|
67
|
+
|
|
68
|
+
Arguments:
|
|
69
|
+
|
|
70
|
+
- `task`: required task or question to gather context for.
|
|
71
|
+
- `paths`: optional files or directories to focus. Defaults to the workspace root.
|
|
72
|
+
- `budget`: optional approximate byte budget, default 4,000 and capped at 20,000.
|
|
73
|
+
|
|
74
|
+
Use this when Kward needs orientation for a bug, review, implementation, or explanation before deciding which exact file ranges to read.
|
|
75
|
+
|
|
46
76
|
### `summarize_file_structure`
|
|
47
77
|
|
|
48
|
-
Returns a compact outline of
|
|
78
|
+
Returns a compact outline of recognizable declarations in a source file, including line ranges and declaration kind where Kward can infer them. Kward uses it when a file may be too large to read fully at first.
|
|
49
79
|
|
|
50
80
|
Arguments:
|
|
51
81
|
|
|
52
82
|
- `path`: workspace-relative source file path.
|
|
53
83
|
|
|
54
|
-
This tool saves tokens by letting Kward identify relevant entry points before requesting exact line ranges with `read_file`.
|
|
84
|
+
This tool saves tokens by letting Kward identify relevant entry points before requesting exact line ranges with `read_file`. The outline is capped at 80 entries and recognizes common Ruby, JavaScript/TypeScript, Go, Rust, Java, and C#-style declarations with lightweight pattern matching rather than a full parser. Binary files (detected by null bytes) return an error instead of content.
|
|
55
85
|
|
|
56
86
|
## Changing files
|
|
57
87
|
|
|
@@ -66,6 +96,8 @@ Arguments:
|
|
|
66
96
|
|
|
67
97
|
Use full writes when replacing generated content or creating a new file. For small edits to existing files, Kward should usually prefer `edit_file`.
|
|
68
98
|
|
|
99
|
+
The result includes a unified diff of the changes, capped at 8 KB with a summary of additions and deletions when truncated.
|
|
100
|
+
|
|
69
101
|
### `edit_file`
|
|
70
102
|
|
|
71
103
|
Applies one or more exact replacements to a file that has already been read.
|
|
@@ -77,7 +109,9 @@ Arguments:
|
|
|
77
109
|
- `old_text`: unique exact text to replace.
|
|
78
110
|
- `new_text`: replacement text.
|
|
79
111
|
|
|
80
|
-
Each `old_text` must match exactly once, and replacements must not overlap. This keeps edits deterministic and avoids broad fuzzy rewriting.
|
|
112
|
+
Each `old_text` must match exactly once, and replacements must not overlap. This keeps edits deterministic and avoids broad fuzzy rewriting. Empty `old_text`, multiple matches, and no-op edits (where the result is identical) are all rejected with an error.
|
|
113
|
+
|
|
114
|
+
The result includes a unified diff of the changes, capped at 8 KB with a summary of additions and deletions when truncated.
|
|
81
115
|
|
|
82
116
|
## Running commands
|
|
83
117
|
|
|
@@ -90,16 +124,21 @@ Arguments:
|
|
|
90
124
|
- `command`: command to run.
|
|
91
125
|
- `timeout_seconds`: optional timeout, default 30 seconds.
|
|
92
126
|
|
|
93
|
-
Kward uses shell commands for tests, linters, build checks, and simple repository inspection. Command output is bounded and may be compacted before it is sent back into model context, while the original output remains available in the session record.
|
|
127
|
+
Kward uses shell commands for tests, linters, build checks, and simple repository inspection. Command output is bounded at 128 KB and may be compacted before it is sent back into model context, while the original output remains available in the session record.
|
|
128
|
+
|
|
129
|
+
The output format is `Exit status: N` followed by `STDOUT:` and `STDERR:` sections. On timeout, Kward sends SIGTERM then SIGKILL after 0.2 seconds and returns a timeout error. Commands support cooperative cancellation when the session is cancelled.
|
|
94
130
|
|
|
95
131
|
## Token behavior
|
|
96
132
|
|
|
97
133
|
Workspace tools are intentionally incremental:
|
|
98
134
|
|
|
99
135
|
1. list directories to find likely files,
|
|
100
|
-
2.
|
|
101
|
-
3. read focused line ranges
|
|
102
|
-
4.
|
|
103
|
-
5.
|
|
136
|
+
2. use `read_file` with `mode: "outline"` or `summarize_file_structure` before reading everything,
|
|
137
|
+
3. read focused line ranges with `mode: "range"`, `offset`, `limit`, and optional `max_bytes`,
|
|
138
|
+
4. widen to `mode: "full"` only when focused context is insufficient,
|
|
139
|
+
5. make exact edits,
|
|
140
|
+
6. run focused verification commands.
|
|
104
141
|
|
|
105
142
|
This keeps the model's context window focused on relevant evidence instead of flooding it with entire repositories or long command output.
|
|
143
|
+
|
|
144
|
+
For details on how tool outputs are compacted, deduplicated, and retrieved, see [Agent tools](agent-tools.md).
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Space Invaders — a full classic arcade game built as a Kward interactive
|
|
4
|
+
# plugin command.
|
|
5
|
+
#
|
|
6
|
+
# Install: copy this file to ~/.kward/plugins/ and run `/invaders` in the
|
|
7
|
+
# Kward TUI.
|
|
8
|
+
#
|
|
9
|
+
# Controls:
|
|
10
|
+
# ←/→ Move
|
|
11
|
+
# Space Fire
|
|
12
|
+
# Q / Esc Quit
|
|
13
|
+
# Ctrl+C Force quit (handled by Kward)
|
|
14
|
+
#
|
|
15
|
+
# The game renders colored sprites and particle-burst explosions inside the
|
|
16
|
+
# composer canvas region using the interactive mode API.
|
|
17
|
+
|
|
18
|
+
Kward.plugin do |plugin|
|
|
19
|
+
plugin.interactive_command "invaders", rows: 18, fps: 30, description: "Space Invaders arcade game" do |ui, ctx|
|
|
20
|
+
game = SpaceInvadersGame.new(width: ui.width, height: ui.height)
|
|
21
|
+
ui.on_tick { |ui| game.tick(ui) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# rubocop:disable Metrics/ClassLength, Metrics/MethodLength
|
|
26
|
+
class SpaceInvadersGame
|
|
27
|
+
PLAYER_CHAR = "▲"
|
|
28
|
+
PLAYER_COLOR = :cyan
|
|
29
|
+
BULLET_CHAR = "│"
|
|
30
|
+
BULLET_COLOR = :yellow
|
|
31
|
+
BOMB_CHAR = "V"
|
|
32
|
+
BOMB_COLOR = :magenta
|
|
33
|
+
SCORE_LABEL = "Score"
|
|
34
|
+
LIVES_LABEL = "Lives"
|
|
35
|
+
|
|
36
|
+
INVADER_ROWS = [
|
|
37
|
+
{ char: "M", color: :red, points: 30 },
|
|
38
|
+
{ char: "M", color: :red, points: 30 },
|
|
39
|
+
{ char: "W", color: :yellow, points: 20 },
|
|
40
|
+
{ char: "W", color: :yellow, points: 20 },
|
|
41
|
+
{ char: "A", color: :green, points: 10 }
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
44
|
+
PARTICLE_CHARS = %w[* + . o].freeze
|
|
45
|
+
PARTICLE_COLORS = %i[red yellow].freeze
|
|
46
|
+
PARTICLE_LIFE = 6
|
|
47
|
+
|
|
48
|
+
MOVE_EVERY = 12
|
|
49
|
+
BULLET_SPEED = 2
|
|
50
|
+
BOMB_SPEED = 1
|
|
51
|
+
MAX_BULLETS = 3
|
|
52
|
+
FIRE_COOLDOWN = 8
|
|
53
|
+
BOMB_CHANCE = 0.015
|
|
54
|
+
|
|
55
|
+
def initialize(width:, height:)
|
|
56
|
+
@width = width
|
|
57
|
+
@height = height
|
|
58
|
+
@phase = :playing
|
|
59
|
+
@tick_count = 0
|
|
60
|
+
@score = 0
|
|
61
|
+
@lives = 3
|
|
62
|
+
@wave = 1
|
|
63
|
+
@fire_cooldown = 0
|
|
64
|
+
@invader_dir = 1
|
|
65
|
+
@invaders = []
|
|
66
|
+
@bullets = []
|
|
67
|
+
@bombs = []
|
|
68
|
+
@explosions = []
|
|
69
|
+
@message_timer = 0
|
|
70
|
+
spawn_invaders
|
|
71
|
+
place_player
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def tick(ui)
|
|
75
|
+
drain_keys(ui)
|
|
76
|
+
case @phase
|
|
77
|
+
when :playing
|
|
78
|
+
update_playing
|
|
79
|
+
render_playing(ui)
|
|
80
|
+
when :game_over, :won
|
|
81
|
+
update_message_phase
|
|
82
|
+
render_message(ui)
|
|
83
|
+
end
|
|
84
|
+
ui.render
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def drain_keys(ui)
|
|
90
|
+
while (key = ui.poll_key)
|
|
91
|
+
case key
|
|
92
|
+
when :escape, "q", "Q"
|
|
93
|
+
ui.exit
|
|
94
|
+
when :left
|
|
95
|
+
@player_col -= 1 if @phase == :playing
|
|
96
|
+
when :right
|
|
97
|
+
@player_col += 1 if @phase == :playing
|
|
98
|
+
when :space
|
|
99
|
+
fire_bullet if @phase == :playing
|
|
100
|
+
when :return, :enter
|
|
101
|
+
restart if @phase != :playing
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def place_player
|
|
107
|
+
@player_col = @width / 2
|
|
108
|
+
@player_row = @height - 2
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# ---- Invader spawning ----
|
|
112
|
+
|
|
113
|
+
def spawn_invaders
|
|
114
|
+
@invaders = []
|
|
115
|
+
rows = INVADER_ROWS.length
|
|
116
|
+
cols = [(@width - 4) / 4, 6].min
|
|
117
|
+
cols = [cols, 3].max
|
|
118
|
+
start_row = 2
|
|
119
|
+
start_col = (@width - cols * 4) / 2
|
|
120
|
+
start_col = [start_col, 1].max
|
|
121
|
+
|
|
122
|
+
rows.times do |row|
|
|
123
|
+
config = INVADER_ROWS[row]
|
|
124
|
+
cols.times do |col|
|
|
125
|
+
@invaders << {
|
|
126
|
+
row: start_row + row,
|
|
127
|
+
col: start_col + col * 4,
|
|
128
|
+
char: config[:char],
|
|
129
|
+
color: config[:color],
|
|
130
|
+
points: config[:points],
|
|
131
|
+
alive: true
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
@invader_dir = 1
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# ---- Playing phase updates ----
|
|
139
|
+
|
|
140
|
+
def update_playing
|
|
141
|
+
@tick_count += 1
|
|
142
|
+
@fire_cooldown = [@fire_cooldown - 1, 0].max if @fire_cooldown.positive?
|
|
143
|
+
|
|
144
|
+
move_invaders if (@tick_count % move_interval).zero?
|
|
145
|
+
move_bullets
|
|
146
|
+
move_bombs
|
|
147
|
+
maybe_drop_bomb
|
|
148
|
+
update_explosions
|
|
149
|
+
check_collisions
|
|
150
|
+
check_win_lose
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def move_interval
|
|
154
|
+
alive = @invaders.count { |inv| inv[:alive] }
|
|
155
|
+
total = @invaders.length
|
|
156
|
+
return MOVE_EVERY if total.zero?
|
|
157
|
+
|
|
158
|
+
speedup = ((total - alive) * MOVE_EVERY) / (total * 2)
|
|
159
|
+
[MOVE_EVERY - speedup, 2].max
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def move_invaders
|
|
163
|
+
living = @invaders.select { |inv| inv[:alive] }
|
|
164
|
+
return if living.empty?
|
|
165
|
+
|
|
166
|
+
min_col = living.map { |inv| inv[:col] }.min
|
|
167
|
+
max_col = living.map { |inv| inv[:col] }.max
|
|
168
|
+
|
|
169
|
+
if @invader_dir.positive? && max_col >= @width - 2
|
|
170
|
+
@invader_dir = -1
|
|
171
|
+
@invaders.each { |inv| inv[:row] += 1 if inv[:alive] }
|
|
172
|
+
elsif @invader_dir.negative? && min_col <= 1
|
|
173
|
+
@invader_dir = 1
|
|
174
|
+
@invaders.each { |inv| inv[:row] += 1 if inv[:alive] }
|
|
175
|
+
else
|
|
176
|
+
@invaders.each { |inv| inv[:col] += @invader_dir if inv[:alive] }
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def fire_bullet
|
|
181
|
+
return if @bullets.length >= MAX_BULLETS
|
|
182
|
+
return if @fire_cooldown.positive?
|
|
183
|
+
|
|
184
|
+
@bullets << { row: @player_row - 1, col: @player_col }
|
|
185
|
+
@fire_cooldown = FIRE_COOLDOWN
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def move_bullets
|
|
189
|
+
@bullets.each { |b| b[:row] -= BULLET_SPEED }
|
|
190
|
+
@bullets.reject! { |b| b[:row] < 0 }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def move_bombs
|
|
194
|
+
@bombs.each { |b| b[:row] += BOMB_SPEED }
|
|
195
|
+
@bombs.reject! { |b| b[:row] >= @height }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def maybe_drop_bomb
|
|
199
|
+
return if rand > BOMB_CHANCE
|
|
200
|
+
|
|
201
|
+
living = @invaders.select { |inv| inv[:alive] }
|
|
202
|
+
return if living.empty?
|
|
203
|
+
|
|
204
|
+
# Drop from a random bottom-most invader per column
|
|
205
|
+
columns = living.group_by { |inv| inv[:col] }
|
|
206
|
+
col, invaders = columns.min_by { rand }
|
|
207
|
+
return unless invaders
|
|
208
|
+
|
|
209
|
+
bottom = invaders.max_by { |inv| inv[:row] }
|
|
210
|
+
@bombs << { row: bottom[:row] + 1, col: bottom[:col] } if bottom
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# ---- Explosions ----
|
|
214
|
+
|
|
215
|
+
def spawn_explosion(row, col, color = :red)
|
|
216
|
+
PARTICLE_LIFE.times do |i|
|
|
217
|
+
angle = (i * 360 / PARTICLE_LIFE) * Math::PI / 180
|
|
218
|
+
dx = (Math.cos(angle) * (i + 1)).round
|
|
219
|
+
dy = (Math.sin(angle) * (i + 1)).round
|
|
220
|
+
@explosions << {
|
|
221
|
+
row: row + dy,
|
|
222
|
+
col: col + dx,
|
|
223
|
+
char: PARTICLE_CHARS[i % PARTICLE_CHARS.length],
|
|
224
|
+
color: PARTICLE_COLORS[i % PARTICLE_COLORS.length],
|
|
225
|
+
life: PARTICLE_LIFE
|
|
226
|
+
}
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def update_explosions
|
|
231
|
+
@explosions.each { |e| e[:life] -= 1 }
|
|
232
|
+
@explosions.reject! { |e| e[:life] <= 0 }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# ---- Collisions ----
|
|
236
|
+
|
|
237
|
+
def check_collisions
|
|
238
|
+
check_bullet_invader_hits
|
|
239
|
+
check_bomb_player_hits
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def check_bullet_invader_hits
|
|
243
|
+
@bullets.reject! do |bullet|
|
|
244
|
+
hit = @invaders.find { |inv| inv[:alive] && inv[:row] == bullet[:row] && inv[:col] == bullet[:col] }
|
|
245
|
+
next false unless hit
|
|
246
|
+
|
|
247
|
+
hit[:alive] = false
|
|
248
|
+
@score += hit[:points]
|
|
249
|
+
spawn_explosion(hit[:row], hit[:col], hit[:color])
|
|
250
|
+
true
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def check_bomb_player_hits
|
|
255
|
+
@bombs.reject! do |bomb|
|
|
256
|
+
hit = bomb[:row] == @player_row && bomb[:col] == @player_col
|
|
257
|
+
next false unless hit
|
|
258
|
+
|
|
259
|
+
@lives -= 1
|
|
260
|
+
spawn_explosion(@player_row, @player_col, :cyan)
|
|
261
|
+
@phase = :game_over if @lives <= 0
|
|
262
|
+
true
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def check_win_lose
|
|
267
|
+
return unless @phase == :playing
|
|
268
|
+
|
|
269
|
+
@phase = :won if @invaders.none? { |inv| inv[:alive] }
|
|
270
|
+
|
|
271
|
+
return unless @invaders.any? { |inv| inv[:alive] && inv[:row] >= @player_row }
|
|
272
|
+
|
|
273
|
+
@phase = :game_over
|
|
274
|
+
spawn_explosion(@player_row, @player_col, :cyan)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# ---- Message phase ----
|
|
278
|
+
|
|
279
|
+
def update_message_phase
|
|
280
|
+
@message_timer += 1
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def restart
|
|
284
|
+
@score = 0
|
|
285
|
+
@lives = 3
|
|
286
|
+
@wave = 1
|
|
287
|
+
@tick_count = 0
|
|
288
|
+
@bullets.clear
|
|
289
|
+
@bombs.clear
|
|
290
|
+
@explosions.clear
|
|
291
|
+
@fire_cooldown = 0
|
|
292
|
+
@invader_dir = 1
|
|
293
|
+
spawn_invaders
|
|
294
|
+
place_player
|
|
295
|
+
@phase = :playing
|
|
296
|
+
@message_timer = 0
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# ---- Rendering ----
|
|
300
|
+
|
|
301
|
+
def render_playing(ui)
|
|
302
|
+
ui.clear_frame
|
|
303
|
+
|
|
304
|
+
render_hud(ui)
|
|
305
|
+
render_invaders(ui)
|
|
306
|
+
render_bullets(ui)
|
|
307
|
+
render_bombs(ui)
|
|
308
|
+
render_explosions(ui)
|
|
309
|
+
ui.put(@player_row, @player_col, PLAYER_CHAR, PLAYER_COLOR)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def render_hud(ui)
|
|
313
|
+
score_text = "#{SCORE_LABEL}: #{@score}"
|
|
314
|
+
lives_text = "#{LIVES_LABEL}: #{@lives}"
|
|
315
|
+
wave_text = "Wave: #{@wave}"
|
|
316
|
+
|
|
317
|
+
score_text.chars.each_with_index do |char, i|
|
|
318
|
+
ui.put(0, i, char, :green)
|
|
319
|
+
end
|
|
320
|
+
lives_col = @width - lives_text.length
|
|
321
|
+
lives_text.chars.each_with_index do |char, i|
|
|
322
|
+
ui.put(0, lives_col + i, char, :cyan)
|
|
323
|
+
end
|
|
324
|
+
wave_col = (@width - wave_text.length) / 2
|
|
325
|
+
wave_text.chars.each_with_index do |char, i|
|
|
326
|
+
ui.put(0, wave_col + i, char, :gray)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def render_invaders(ui)
|
|
331
|
+
@invaders.each do |inv|
|
|
332
|
+
next unless inv[:alive]
|
|
333
|
+
|
|
334
|
+
ui.put(inv[:row], inv[:col], inv[:char], inv[:color])
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def render_bullets(ui)
|
|
339
|
+
@bullets.each { |b| ui.put(b[:row], b[:col], BULLET_CHAR, BULLET_COLOR) }
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def render_bombs(ui)
|
|
343
|
+
@bombs.each { |b| ui.put(b[:row], b[:col], BOMB_CHAR, BOMB_COLOR) }
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def render_explosions(ui)
|
|
347
|
+
@explosions.each do |e|
|
|
348
|
+
ui.put(e[:row], e[:col], e[:char], e[:color])
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def render_message(ui)
|
|
353
|
+
ui.clear_frame
|
|
354
|
+
render_invaders(ui)
|
|
355
|
+
render_explosions(ui)
|
|
356
|
+
|
|
357
|
+
if @phase == :won
|
|
358
|
+
message = "YOU WIN!"
|
|
359
|
+
color = :green
|
|
360
|
+
else
|
|
361
|
+
message = "GAME OVER"
|
|
362
|
+
color = :red
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
center_text(ui, @height / 2, message, color)
|
|
366
|
+
center_text(ui, @height / 2 + 2, "Score: #{@score}", :yellow)
|
|
367
|
+
center_text(ui, @height / 2 + 4, "Enter: restart Q: quit", :gray)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def center_text(ui, row, text, color)
|
|
371
|
+
col = (@width - text.length) / 2
|
|
372
|
+
text.chars.each_with_index do |char, i|
|
|
373
|
+
ui.put(row, col + i, char, color)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
# rubocop:enable Metrics/ClassLength, Metrics/MethodLength
|