kward 0.83.0 → 0.85.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 +102 -15
- data/CONTRIBUTING.md +74 -0
- data/Gemfile.lock +8 -2
- data/README.md +21 -1
- data/Rakefile +46 -2
- data/SECURITY.md +31 -0
- data/doc/agent-tools.md +13 -1
- data/doc/api.md +21 -2
- data/doc/composer.md +2 -2
- data/doc/configuration.md +95 -25
- data/doc/editor.md +28 -13
- data/doc/extensibility.md +2 -1
- data/doc/files.md +8 -4
- data/doc/getting-started.md +3 -0
- data/doc/git.md +3 -1
- data/doc/pan.md +25 -15
- data/doc/permissions.md +4 -4
- data/doc/platform-support.md +48 -0
- data/doc/plugins.md +464 -15
- data/doc/rpc.md +154 -16
- data/doc/sandboxing.md +11 -5
- data/doc/security.md +10 -3
- data/doc/session-management.md +5 -4
- data/doc/shell.md +62 -45
- data/doc/tabs.md +6 -2
- data/doc/transports.md +15 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +9 -6
- data/doc/workspace-tools.md +9 -0
- data/examples/plugins/space_invaders.rb +1 -1
- data/examples/plugins/stardate_footer.rb +2 -2
- data/examples/plugins/telegram/plugin.rb +1 -1
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +30 -14
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/compaction.rb +9 -3
- data/lib/kward/cli/doctor.rb +39 -17
- data/lib/kward/cli/hook_commands.rb +22 -12
- data/lib/kward/cli/interactive_turn.rb +48 -7
- data/lib/kward/cli/plugins.rb +81 -12
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +52 -5
- data/lib/kward/cli/rendering.rb +18 -9
- data/lib/kward/cli/runtime_helpers.rb +228 -71
- data/lib/kward/cli/sessions.rb +9 -5
- data/lib/kward/cli/settings/menus.rb +745 -0
- data/lib/kward/cli/settings/model.rb +327 -0
- data/lib/kward/cli/settings.rb +6 -1055
- data/lib/kward/cli/slash_commands.rb +56 -17
- data/lib/kward/cli/tabs.rb +244 -33
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +14 -7
- data/lib/kward/cli/worktrees.rb +65 -2
- data/lib/kward/cli.rb +70 -30
- data/lib/kward/compactor.rb +18 -7
- data/lib/kward/config/core.rb +389 -0
- data/lib/kward/config/extensions.rb +96 -0
- data/lib/kward/config/prompts.rb +313 -0
- data/lib/kward/config/settings.rb +250 -0
- data/lib/kward/config_files.rb +14 -994
- data/lib/kward/conversation.rb +31 -2
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +36 -24
- data/lib/kward/model/copilot_models.rb +2 -2
- data/lib/kward/model/model_info.rb +20 -3
- data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
- data/lib/kward/model/payloads.rb +12 -3
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/model/typesafe_client.rb +78 -0
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +33 -10
- data/lib/kward/permissions/policy.rb +6 -2
- data/lib/kward/plugin_registry.rb +2 -659
- data/lib/kward/plugins/actions.rb +453 -0
- data/lib/kward/plugins/chat_contract.rb +121 -0
- data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
- data/lib/kward/plugins/host.rb +232 -0
- data/lib/kward/plugins/registry.rb +1190 -0
- data/lib/kward/plugins/resources.rb +206 -0
- data/lib/kward/plugins/turn_request.rb +36 -0
- data/lib/kward/plugins/ui.rb +219 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
- data/lib/kward/prompt_interface/editor/controller.rb +30 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/markdown_code_block.rb +136 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
- data/lib/kward/prompt_interface/editor/renderer.rb +108 -6
- data/lib/kward/prompt_interface/editor/runner.rb +362 -0
- data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
- data/lib/kward/prompt_interface/editor/scratchpad_languages.rb +74 -0
- data/lib/kward/prompt_interface/editor/scratchpad_runner.rb +182 -0
- data/lib/kward/prompt_interface/editor/state.rb +11 -11
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
- data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
- data/lib/kward/prompt_interface/file_overlay.rb +71 -15
- data/lib/kward/prompt_interface/key_handler.rb +67 -0
- data/lib/kward/prompt_interface/layout.rb +1 -1
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
- data/lib/kward/prompt_interface/project_browser.rb +415 -14
- data/lib/kward/prompt_interface/runtime_state.rb +56 -2
- data/lib/kward/prompt_interface/screen.rb +11 -4
- data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
- data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
- data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
- data/lib/kward/prompt_interface.rb +151 -27
- data/lib/kward/prompts/commands.rb +3 -2
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
- data/lib/kward/pty/detached_run.rb +44 -0
- data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +104 -30
- data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
- data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
- data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +52 -0
- data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
- data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
- data/lib/kward/rpc/prompt_bridge.rb +25 -0
- data/lib/kward/rpc/server.rb +91 -12
- data/lib/kward/rpc/session_manager.rb +147 -44
- data/lib/kward/rpc/session_tree_rows.rb +2 -2
- data/lib/kward/rpc/tool_metadata.rb +1 -1
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/sandbox/command_runner.rb +1 -1
- data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
- data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
- data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
- data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
- data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
- data/lib/kward/{ekwsh.rb → shell/kwsh.rb} +38 -19
- data/lib/kward/shell/kwshrc.rb +233 -0
- data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +121 -28
- data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +2 -0
- data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
- data/lib/kward/skills/trust_store.rb +1 -1
- data/lib/kward/tabs/driver.rb +194 -0
- data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
- data/lib/kward/{ansi.rb → terminal/ansi.rb} +110 -10
- data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
- data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
- data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +12 -0
- data/lib/kward/terminal/text.rb +121 -0
- data/lib/kward/text_matcher.rb +18 -0
- data/lib/kward/tools/base.rb +18 -0
- data/lib/kward/tools/context_for_task.rb +15 -6
- data/lib/kward/tools/edit_file.rb +9 -6
- data/lib/kward/tools/git_commit.rb +13 -7
- data/lib/kward/tools/list_directory.rb +4 -4
- data/lib/kward/tools/open_editor.rb +41 -0
- data/lib/kward/tools/plugin_tool.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +1 -1
- data/lib/kward/tools/read_file.rb +7 -6
- data/lib/kward/tools/registry.rb +109 -17
- data/lib/kward/tools/run_shell_command.rb +10 -8
- data/lib/kward/tools/search/code.rb +1 -1
- data/lib/kward/tools/summarize_file_structure.rb +5 -5
- data/lib/kward/tools/tool_call.rb +3 -1
- data/lib/kward/tools/typesafe_evaluate.rb +81 -0
- data/lib/kward/tools/workspace_targets.rb +58 -0
- data/lib/kward/tools/write_file.rb +9 -6
- data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
- data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
- data/lib/kward/transport/contracts.rb +200 -0
- data/lib/kward/transport/gateway.rb +79 -35
- data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
- data/lib/kward/transport.rb +1 -200
- data/lib/kward/version.rb +1 -1
- data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
- data/lib/kward/{project_files.rb → workspace/files.rb} +2 -2
- data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
- data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
- data/templates/default/kward_navigation.rb +1 -0
- data/templates/default/layout/html/footer.erb +10 -0
- data/templates/default/layout/html/headers.erb +23 -0
- data/templates/default/layout/html/layout.erb +2 -2
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +94 -47
- data/lib/kward/scratchpad_runner.rb +0 -56
- data/lib/kward/tab_driver.rb +0 -90
- /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
- /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
- /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
- /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
- /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
- /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
- /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
- /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
- /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
- /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
data/doc/transports.md
CHANGED
|
@@ -104,6 +104,13 @@ Plugin-chat drivers may accept a `context:` keyword on `submit`. Transport turns
|
|
|
104
104
|
provide the authenticated actor there. Existing drivers that do not accept the
|
|
105
105
|
keyword continue to work, but cannot use actor-specific context.
|
|
106
106
|
|
|
107
|
+
The plugin-chat factory host reports `surface: :transport`, the stable chat type
|
|
108
|
+
ID and scope key, immutable plugin configuration, scoped durable storage, secret
|
|
109
|
+
lookup, logging, declared chat capabilities, and managed cleanup. Storage is
|
|
110
|
+
isolated by plugin, chat type, and external conversation scope. Versioned chat
|
|
111
|
+
contracts also reject attachments not declared by that chat type before calling
|
|
112
|
+
the driver.
|
|
113
|
+
|
|
107
114
|
Plugin-chat transport IDs, turn events, transcript storage, and authorization
|
|
108
115
|
remain separate from normal workspace sessions. A plugin's `singleton: :global`
|
|
109
116
|
setting also means that all transport conversations share that one plugin
|
|
@@ -119,6 +126,14 @@ API.
|
|
|
119
126
|
Transports that cannot support interactive approvals must use an explicit
|
|
120
127
|
configured fallback policy; they must not leave an agent turn waiting forever.
|
|
121
128
|
|
|
129
|
+
Structured plugin UI requests use the same transport-neutral interaction path.
|
|
130
|
+
Plugin `question`, `select`, `confirm`, and `input` calls arrive with matching
|
|
131
|
+
interaction kinds, prompts, choices, and metadata, and answers are routed back to the
|
|
132
|
+
waiting plugin handler. An adapter may support only a subset, but it must cancel
|
|
133
|
+
or answer unsupported requests rather than leaving the turn blocked. Plugin
|
|
134
|
+
notifications and progress remain local to the RPC/TUI UI bridge for now and
|
|
135
|
+
are not transport interaction requests.
|
|
136
|
+
|
|
122
137
|
## Storage and routing
|
|
123
138
|
|
|
124
139
|
Transport plugins receive namespaced durable storage for state such as:
|
data/doc/troubleshooting.md
CHANGED
|
@@ -10,14 +10,24 @@ This page covers common issues and how to diagnose them. When something is not w
|
|
|
10
10
|
kward doctor
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
It checks that your config file is readable and valid JSON, that the config and session directories are writable, that the workspace exists, which provider and model are active, and which credentials are configured.
|
|
13
|
+
It checks that your config file is readable and valid JSON, that the config and session directories are writable, that the workspace exists, which provider and model are active, and which credentials are configured. Core check failures produce a nonzero exit status, so the command can also guard setup scripts. Optional features such as Pan are reported separately and do not make an otherwise healthy setup fail.
|
|
14
14
|
|
|
15
|
-
`kward auth status` gives a focused view of credentials only, also without printing secrets:
|
|
15
|
+
`kward auth status` gives a focused view of configured credentials only, also without printing secrets:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
kward auth status
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
Use `kward auth status --all` when you also want to see every unconfigured provider.
|
|
22
|
+
|
|
23
|
+
Kward keeps normal command failures concise. To include a Ruby backtrace while diagnosing an unexpected failure, rerun the command with debug errors enabled:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
KWARD_DEBUG=1 kward "Repeat the failing task"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Backtraces can include local paths and implementation details. Review them before sharing them publicly.
|
|
30
|
+
|
|
21
31
|
## Authentication errors and token expiration
|
|
22
32
|
|
|
23
33
|
OAuth tokens expire. Kward refreshes access tokens automatically when a refresh token is available, but if the refresh token is missing or expired, requests fail with a provider-specific error:
|
data/doc/usage.md
CHANGED
|
@@ -66,7 +66,7 @@ Inside interactive mode, ask Kward to run a command:
|
|
|
66
66
|
Run the focused test for the CLI status command.
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
Or run an interactive PTY command yourself from the composer by prefixing it with `!`. Aliases configured in `
|
|
69
|
+
Or run an interactive PTY command yourself from the composer by prefixing it with `!`. Aliases configured in `kwshrc` are expanded here too:
|
|
70
70
|
|
|
71
71
|
```text
|
|
72
72
|
!git status --short
|
|
@@ -78,7 +78,7 @@ For several commands, enter the embedded Kward shell:
|
|
|
78
78
|
/shell
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
`/shell` opens `
|
|
81
|
+
`/shell` opens `kwsh`, a Kward-native command mode with one persistent local shell process. It preserves state such as the current directory, environment variables, functions, and aliases between commands. Prefix a line with `?` inside `/shell` to ask a transient shell assistant about the latest output, execute an explicit state change, or prepare a command without running it. External commands receive an interactive PTY by default, so `git log`, `less`, Vim, SSH, and REPLs work without a prefix. Use `capture <command>` inside `/shell` or `/capture <command>` from the normal composer for bounded, transcript-friendly output. See [Embedded shell](shell.md) for built-ins, completion, configuration, ANSI handling, PTY passthrough, and limitations.
|
|
82
82
|
|
|
83
83
|
## Shell commands
|
|
84
84
|
|
|
@@ -136,11 +136,14 @@ Slash commands run local actions in the current session. Most do not send a prom
|
|
|
136
136
|
| `/tab close` | close the active tab. |
|
|
137
137
|
| `/tab new` | open a new tab. |
|
|
138
138
|
| `/tab name <label>` | rename the active tab label. |
|
|
139
|
+
| `/name <name>` | rename the active session and tab together; available while the tab's agent is running. |
|
|
139
140
|
| `/tab worktree` | create or activate the active session tab's linked Git worktree. |
|
|
140
141
|
| `/tab worktree activate` | explicitly create or activate the active session tab's linked Git worktree. |
|
|
141
142
|
| `/tab worktree detach` | return to the original workspace while keeping the linked worktree and branch. |
|
|
142
143
|
| `/tab worktree status` | inspect the active tab's worktree binding and changes. |
|
|
143
144
|
| `/tab worktree merge` | merge a clean worktree branch into the branch checked out in its original workspace. |
|
|
145
|
+
| `/tab worktree merge resolve` | ask the agent to resolve an in-progress merge in the original workspace from the same tab. |
|
|
146
|
+
| `/tab worktree merge continue` | stage resolutions and complete an in-progress merge after confirmation. |
|
|
144
147
|
| `/tab worktree merge abort` | abort a conflicted worktree merge in the original workspace. |
|
|
145
148
|
| `/tab worktree remove` | remove a clean linked worktree while keeping its branch. |
|
|
146
149
|
| `/worktree …` | alias for `/tab worktree …` on the active tab. |
|
|
@@ -160,7 +163,7 @@ Slash commands run local actions in the current session. Most do not send a prom
|
|
|
160
163
|
| `/skill <name>` | activate a configured skill explicitly for the current session. |
|
|
161
164
|
| `/stats [range]` | summarize enabled local telemetry. |
|
|
162
165
|
| `/hooks ...` | inspect, diagnose, trust, or untrust lifecycle hooks. |
|
|
163
|
-
| `/scratchpad [
|
|
166
|
+
| `/scratchpad [language|help]` | open an unsaved, syntax-highlighted editor buffer; `/scratchpad` alone opens plain text. |
|
|
164
167
|
| `/redraw` | fix terminal drawing after resize or glitches. |
|
|
165
168
|
| `/reload` | reload installed plugins. |
|
|
166
169
|
| `/exit` | leave Kward. |
|
|
@@ -191,7 +194,7 @@ Use sessions when work spans more than one terminal sitting, or when you want to
|
|
|
191
194
|
Typical flow:
|
|
192
195
|
|
|
193
196
|
```text
|
|
194
|
-
/
|
|
197
|
+
/name oauth cleanup
|
|
195
198
|
# work with Kward
|
|
196
199
|
/export oauth-notes.md
|
|
197
200
|
/exit
|
|
@@ -263,13 +266,13 @@ This screenshot shows the broken layout. Find the likely CSS issue.
|
|
|
263
266
|
|
|
264
267
|
## Pan mode
|
|
265
268
|
|
|
266
|
-
Pan mode starts a mobile-friendly
|
|
269
|
+
Pan mode starts a mobile-friendly local web UI:
|
|
267
270
|
|
|
268
271
|
```bash
|
|
269
272
|
kward --working-directory ~/code/project pan
|
|
270
273
|
```
|
|
271
274
|
|
|
272
|
-
|
|
275
|
+
Pan binds to loopback by default. Exposing it to a trusted LAN requires an explicit host setting and prints a warning because Pan uses plain HTTP. It exposes file, shell, web, and configured extension tools through a browser UI and requires a configured username plus a config or environment password. Pan saves conversations to the normal workspace session store; its session drawer can create, resume, rename, and delete sessions. Session changes are disabled while turns are active or queued. See [Pan mode](pan.md) for setup, browser workflows, security, and limitations.
|
|
273
276
|
|
|
274
277
|
## RPC backend
|
|
275
278
|
|
data/doc/workspace-tools.md
CHANGED
|
@@ -21,6 +21,15 @@ Important behavior:
|
|
|
21
21
|
- Edits use exact text replacement, so accidental partial or fuzzy changes fail instead of guessing.
|
|
22
22
|
- With sandboxing off (the default), shell commands run as your operating-system user from the workspace. Enable [command sandboxing](sandboxing.md) to apply an OS boundary to model-requested `run_shell_command` workers. Command output is capped at 128 KB.
|
|
23
23
|
|
|
24
|
+
### Worktree targets
|
|
25
|
+
|
|
26
|
+
In an active linked-worktree tab, `list_directory`, `read_file`, `write_file`, `edit_file`, `run_shell_command`, `summarize_file_structure`, and `context_for_task` also advertise an optional `target` argument:
|
|
27
|
+
|
|
28
|
+
- `active` is the default linked worktree.
|
|
29
|
+
- `origin` is the tab's verified original repository worktree.
|
|
30
|
+
|
|
31
|
+
Kward supplies this fixed role map from the tab binding; the model cannot provide an arbitrary filesystem root. Selecting `origin` does not add a separate approval prompt, so the agent can complete a requested inspect/edit/test/commit workflow without interruption. The normal configured tool-permission policy still applies equally to both targets. Each target keeps its own path guard and strict shell sandbox.
|
|
32
|
+
|
|
24
33
|
## Reading the workspace
|
|
25
34
|
|
|
26
35
|
### `list_directory`
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
# The game renders colored sprites and particle-burst explosions inside the
|
|
16
16
|
# composer canvas region using the interactive mode API.
|
|
17
17
|
|
|
18
|
-
Kward.plugin do |plugin|
|
|
18
|
+
Kward.plugin(id: "com.kward.example.space-invaders", version: "1.0.0", api: 1) do |plugin|
|
|
19
19
|
plugin.interactive_command "invaders", rows: 18, fps: 30, description: "Space Invaders arcade game" do |ui, ctx|
|
|
20
20
|
game = SpaceInvadersGame.new(width: ui.width, height: ui.height)
|
|
21
21
|
ui.on_tick { |ui| game.tick(ui) }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Displays the current Federation stardate in Kward's interactive footer.
|
|
2
|
-
Kward.plugin do |plugin|
|
|
3
|
-
plugin.
|
|
2
|
+
Kward.plugin(id: "com.kward.example.stardate-footer", version: "1.0.0", api: 1) do |plugin|
|
|
3
|
+
plugin.status "stardate", priority: :low do |_ctx|
|
|
4
4
|
now = Time.now.utc
|
|
5
5
|
reference = Time.utc(1987, 7, 15)
|
|
6
6
|
stardate = 41_000 + ((now - reference) / (365.25 * 24 * 60 * 60) * 1_000)
|
data/kward.gemspec
CHANGED
|
@@ -6,16 +6,16 @@ Gem::Specification.new do |spec|
|
|
|
6
6
|
spec.authors = ["Kai Wood"]
|
|
7
7
|
spec.email = ["kai.wood@icloud.com"]
|
|
8
8
|
|
|
9
|
-
spec.summary = "An
|
|
10
|
-
spec.description = "Kward is
|
|
11
|
-
spec.homepage = "https://github.
|
|
9
|
+
spec.summary = "An extensible Ruby coding agent for your terminal."
|
|
10
|
+
spec.description = "Kward is an extensible Ruby coding agent with workspace tools, resumable sessions, multiple model providers, a local browser UI, and JSON-RPC integrations."
|
|
11
|
+
spec.homepage = "https://kaiwood.github.io/kward/"
|
|
12
12
|
spec.license = "MIT"
|
|
13
13
|
spec.required_ruby_version = ">= 3.4"
|
|
14
14
|
|
|
15
15
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
16
16
|
spec.metadata["source_code_uri"] = "https://github.com/kaiwood/kward"
|
|
17
17
|
spec.metadata["changelog_uri"] = "https://github.com/kaiwood/kward/blob/main/CHANGELOG.md"
|
|
18
|
-
spec.metadata["documentation_uri"] = "https://github.
|
|
18
|
+
spec.metadata["documentation_uri"] = "https://kaiwood.github.io/kward/"
|
|
19
19
|
spec.metadata["bug_tracker_uri"] = "https://github.com/kaiwood/kward/issues"
|
|
20
20
|
|
|
21
21
|
spec.files = Dir.chdir(__dir__) do
|
|
@@ -34,4 +34,5 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
spec.add_dependency "tty-prompt"
|
|
35
35
|
spec.add_dependency "tty-reader"
|
|
36
36
|
spec.add_dependency "tty-screen"
|
|
37
|
+
spec.add_dependency "unicode-display_width"
|
|
37
38
|
end
|
data/lib/kward/agent.rb
CHANGED
|
@@ -3,6 +3,7 @@ require_relative "model/chat_invocation"
|
|
|
3
3
|
require_relative "compactor"
|
|
4
4
|
require_relative "model/context_overflow"
|
|
5
5
|
require_relative "conversation"
|
|
6
|
+
require_relative "plugins/turn_request"
|
|
6
7
|
require_relative "events"
|
|
7
8
|
require_relative "deep_copy"
|
|
8
9
|
require_relative "hooks"
|
|
@@ -28,10 +29,11 @@ module Kward
|
|
|
28
29
|
# lowest layer that owns the behavior, and use `Agent` only for cross-step turn
|
|
29
30
|
# coordination.
|
|
30
31
|
class Agent
|
|
31
|
-
def initialize(client:, tool_registry: ToolRegistry.new, conversation: Conversation.new, telemetry_logger: nil, warning_sink: nil, hook_manager: nil, hook_context: nil)
|
|
32
|
+
def initialize(client:, tool_registry: ToolRegistry.new, conversation: Conversation.new, telemetry_logger: nil, warning_sink: nil, hook_manager: nil, hook_context: nil, strict_provider: false)
|
|
32
33
|
@client = client
|
|
33
34
|
@tool_registry = tool_registry
|
|
34
35
|
@conversation = conversation
|
|
36
|
+
@strict_provider = strict_provider == true
|
|
35
37
|
@warning_sink = warning_sink
|
|
36
38
|
@telemetry_logger = telemetry_logger || TelemetryLogger.new(warning_sink: warning_sink)
|
|
37
39
|
@hook_manager = hook_manager
|
|
@@ -40,9 +42,9 @@ module Kward
|
|
|
40
42
|
|
|
41
43
|
attr_reader :conversation, :tool_registry
|
|
42
44
|
|
|
43
|
-
# Adds
|
|
45
|
+
# Adds user input or scopes a plugin system request, then runs a normal turn.
|
|
44
46
|
#
|
|
45
|
-
# @param input [String] text
|
|
47
|
+
# @param input [String, PluginTurnRequest] user text or host-staged system instructions
|
|
46
48
|
# @param display_input [String, nil] alternate text kept for transcripts
|
|
47
49
|
# @yieldparam event [Object] streamed turn event for frontends
|
|
48
50
|
# @return [String] final assistant answer
|
|
@@ -51,19 +53,31 @@ module Kward
|
|
|
51
53
|
status = "completed"
|
|
52
54
|
error = nil
|
|
53
55
|
cancellation&.raise_if_cancelled!
|
|
54
|
-
|
|
56
|
+
system_turn = input if input.is_a?(PluginTurnRequest)
|
|
57
|
+
display_input ||= system_turn.to_s if system_turn
|
|
58
|
+
input = system_turn.system if system_turn
|
|
59
|
+
turn_start = run_hook("turn_start", payload: { input: input, display_input: display_input, input_role: system_turn ? "system" : "user" })
|
|
55
60
|
return hook_denied_answer(turn_start) if turn_start.denied?
|
|
56
61
|
|
|
57
62
|
input = turn_start.payload[:input] || turn_start.payload["input"] || input
|
|
58
63
|
display_input = turn_start.payload[:display_input] || turn_start.payload["display_input"] || display_input
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
if system_turn
|
|
65
|
+
system_turn = PluginTurnRequest.new(system: input, command: system_turn.command, plugin_id: system_turn.plugin_id, id: system_turn.id)
|
|
66
|
+
end
|
|
67
|
+
@conversation.with_system_turn(system_turn) do
|
|
68
|
+
@conversation.refresh_system_message_if_workspace_agents_changed!
|
|
69
|
+
if system_turn
|
|
70
|
+
@conversation.append_system_turn(system_turn)
|
|
71
|
+
else
|
|
72
|
+
@conversation.append_user(input, display_content: display_input)
|
|
73
|
+
end
|
|
74
|
+
run_hook("turn_context_build_before", payload: { message_count: @conversation.messages.length })
|
|
75
|
+
auto_compact_if_needed
|
|
76
|
+
run_hook("turn_context_build_after", payload: { message_count: @conversation.messages.length })
|
|
77
|
+
answer = run_turn(on_reasoning_delta: on_reasoning_delta, on_retry: on_retry, cancellation: cancellation, steering: steering, options: options, tool_registry: tool_registry, &block)
|
|
78
|
+
run_hook("turn_end", payload: { input: input, answer: answer })
|
|
79
|
+
answer
|
|
80
|
+
end
|
|
67
81
|
rescue StandardError => e
|
|
68
82
|
status = "failed"
|
|
69
83
|
error = e
|
|
@@ -241,7 +255,8 @@ module Kward
|
|
|
241
255
|
tools: registry.schemas,
|
|
242
256
|
provider: options[:provider] || @conversation.provider,
|
|
243
257
|
model: options[:model] || @conversation.model,
|
|
244
|
-
reasoning: options[:reasoning] || @conversation.reasoning_effort
|
|
258
|
+
reasoning: options[:reasoning] || @conversation.reasoning_effort,
|
|
259
|
+
provider_required: @strict_provider
|
|
245
260
|
}
|
|
246
261
|
before = run_hook("model_request_before", payload: request)
|
|
247
262
|
request = DeepCopy.merge(request, before.payload) if before.decision.modify?
|
|
@@ -261,7 +276,8 @@ module Kward
|
|
|
261
276
|
steering: steering,
|
|
262
277
|
provider: request[:provider] || request["provider"],
|
|
263
278
|
model: request[:model] || request["model"],
|
|
264
|
-
reasoning: request[:reasoning] || request["reasoning"]
|
|
279
|
+
reasoning: request[:reasoning] || request["reasoning"],
|
|
280
|
+
provider_required: request[:provider_required] || request["provider_required"]
|
|
265
281
|
}
|
|
266
282
|
)
|
|
267
283
|
run_hook("model_response_after_parse", payload: { message: response })
|
|
@@ -13,6 +13,8 @@ module Kward
|
|
|
13
13
|
case arguments
|
|
14
14
|
when ["status"]
|
|
15
15
|
print_auth_status
|
|
16
|
+
when ["status", "--all"]
|
|
17
|
+
print_auth_status(show_all: true)
|
|
16
18
|
when ["logout"]
|
|
17
19
|
logout_auth
|
|
18
20
|
else
|
|
@@ -21,23 +23,42 @@ module Kward
|
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
# Writes the auth status output for the terminal CLI flow.
|
|
24
|
-
def print_auth_status
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
lines = [
|
|
28
|
-
lines
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
lines <<
|
|
26
|
+
def print_auth_status(show_all: false)
|
|
27
|
+
credentials = auth_credentials
|
|
28
|
+
configured, missing = credentials.partition { |credential| credential.fetch(:configured) }
|
|
29
|
+
lines = [colored("Authentication", :green, :bold), "", colored("Configured", :blue, :bold)]
|
|
30
|
+
lines.concat(auth_credential_lines(configured, status: :ok, empty_message: "None"))
|
|
31
|
+
|
|
32
|
+
if show_all
|
|
33
|
+
lines << ""
|
|
34
|
+
lines << colored("Not configured", :blue, :bold)
|
|
35
|
+
lines.concat(auth_credential_lines(missing, status: :optional, empty_message: "None"))
|
|
36
|
+
elsif missing.any?
|
|
37
|
+
lines << ""
|
|
38
|
+
lines << "#{missing.length} other provider#{missing.length == 1 ? " is" : "s are"} not configured. Run `kward auth status --all` for details."
|
|
33
39
|
end
|
|
40
|
+
|
|
41
|
+
lines << ""
|
|
42
|
+
lines << colored("Credential directory", :blue, :bold)
|
|
43
|
+
lines << " #{ConfigFiles.config_dir}"
|
|
34
44
|
@prompt.say lines.join("\n")
|
|
35
45
|
end
|
|
36
46
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
def auth_credentials
|
|
48
|
+
store = api_key_store
|
|
49
|
+
store.migrate_openrouter_config_key!
|
|
50
|
+
credentials = [
|
|
51
|
+
{ label: "OpenAI OAuth", configured: File.exist?(OpenAIOAuth.default_auth_path) },
|
|
52
|
+
{ label: "Anthropic OAuth", configured: File.exist?(AnthropicOAuth.default_auth_path) },
|
|
53
|
+
{ label: "GitHub OAuth", configured: File.exist?(GithubOAuth.default_auth_path) }
|
|
54
|
+
]
|
|
55
|
+
credentials.concat ProviderCatalog.api_key_providers.map { |provider| { label: "#{provider.name} API key", configured: store.configured?(provider.id) } }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def auth_credential_lines(credentials, status:, empty_message:)
|
|
59
|
+
return [" #{empty_message}"] if credentials.empty?
|
|
60
|
+
|
|
61
|
+
credentials.map { |credential| " #{doctor_mark(status)} #{credential.fetch(:label)}" }
|
|
41
62
|
end
|
|
42
63
|
|
|
43
64
|
def logout_auth
|
data/lib/kward/cli/commands.rb
CHANGED
|
@@ -38,67 +38,88 @@ module Kward
|
|
|
38
38
|
|
|
39
39
|
# Writes the help output for the terminal CLI flow.
|
|
40
40
|
def print_help
|
|
41
|
-
command = ->(text) { colored(text, :green, :bold) }
|
|
42
|
-
option = ->(text) { colored(text, :cyan) }
|
|
43
41
|
heading = ->(text) { colored(text, :blue, :bold) }
|
|
42
|
+
lines = ["#{colored("Kward", :green, :bold)} - an extensible CLI coding agent", ""]
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
44
|
+
help_sections.each do |title, entries|
|
|
45
|
+
lines << heading.call(title)
|
|
46
|
+
lines.concat formatted_help_rows(entries, color: :green, bold: true)
|
|
47
|
+
lines << ""
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
lines << heading.call("Options")
|
|
51
|
+
lines.concat formatted_help_rows(help_options, color: :cyan)
|
|
52
|
+
lines << ""
|
|
53
|
+
lines << heading.call("Examples")
|
|
54
|
+
lines.concat help_examples.map { |example| " #{colored(example, :green, :bold)}" }
|
|
55
|
+
lines << ""
|
|
56
|
+
lines << "Command names take precedence. Anything else is sent as a one-shot prompt."
|
|
57
|
+
@prompt.say lines.join("\n")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def help_sections
|
|
61
|
+
{
|
|
62
|
+
"Getting started" => [
|
|
63
|
+
["kward", "Start an interactive chat"],
|
|
64
|
+
["kward login [PROVIDER]", "Sign in or save provider credentials"],
|
|
65
|
+
["kward doctor", "Check local Kward setup"],
|
|
66
|
+
["kward init", "Install starter prompts and PRINCIPLES.md"]
|
|
67
|
+
],
|
|
68
|
+
"Work" => [
|
|
69
|
+
["kward \"PROMPT\"", "Run a one-shot prompt"],
|
|
70
|
+
["kward --filter \"INSTRUCTION\"", "Filter standard input"],
|
|
71
|
+
["kward edit <filename>", "Open a file in the integrated editor"],
|
|
72
|
+
["kward sysprompt [--raw]", "Inspect the effective system prompt"]
|
|
73
|
+
],
|
|
74
|
+
"Manage" => [
|
|
75
|
+
["kward auth status [--all]", "Show saved credential status"],
|
|
76
|
+
["kward hooks <command>", "Inspect lifecycle hooks"],
|
|
77
|
+
["kward skills <command>", "Manage project skill trust"],
|
|
78
|
+
["kward openrouter <command>", "Manage cached OpenRouter models"],
|
|
79
|
+
["kward stats tokens [range] [options]", "Export local token telemetry as CSV"]
|
|
80
|
+
],
|
|
81
|
+
"Integrate" => [
|
|
82
|
+
["kward pan", "Start the local Pan web UI"],
|
|
83
|
+
["kward rpc", "Start the JSON-RPC backend"],
|
|
84
|
+
["kward transport <command>", "Manage transport plugins"]
|
|
85
|
+
],
|
|
86
|
+
"Reference" => [
|
|
87
|
+
["kward help [command]", "Show help"],
|
|
88
|
+
["kward version", "Show the installed version"]
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def help_options
|
|
94
|
+
[
|
|
95
|
+
["--working-directory=PATH", "Run Kward from PATH"],
|
|
96
|
+
["--mode=MODE", "Execution mode: auto, chat, oneshot, filter"],
|
|
97
|
+
["--filter", "Shortcut for --mode filter"],
|
|
98
|
+
["--skip-config", "Ignore the main config file for this run"],
|
|
99
|
+
["--help, -h", "Show help"],
|
|
100
|
+
["--version, -v", "Show the installed version"]
|
|
101
|
+
]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def help_examples
|
|
105
|
+
[
|
|
106
|
+
"kward",
|
|
107
|
+
"kward \"Explain this project\"",
|
|
108
|
+
"git diff | kward \"Summarize the main changes\"",
|
|
109
|
+
"echo Hello | kward --filter \"Translate to German\"",
|
|
110
|
+
"kward login openrouter",
|
|
111
|
+
"kward edit lib/main.rb",
|
|
112
|
+
"kward stats tokens today --bucket hour"
|
|
113
|
+
]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def formatted_help_rows(entries, color:, bold: false)
|
|
117
|
+
width = entries.map { |label, _description| label.length }.max
|
|
118
|
+
styles = [color]
|
|
119
|
+
styles << :bold if bold
|
|
120
|
+
entries.map do |label, description|
|
|
121
|
+
" #{colored(label.ljust(width), *styles)} #{description}"
|
|
122
|
+
end
|
|
102
123
|
end
|
|
103
124
|
|
|
104
125
|
def command_help
|
|
@@ -119,9 +140,9 @@ module Kward
|
|
|
119
140
|
examples: ["kward login", "kward login anthropic", "kward login openrouter", "kward login github"]
|
|
120
141
|
},
|
|
121
142
|
"auth" => {
|
|
122
|
-
usage: "kward auth status|logout",
|
|
143
|
+
usage: "kward auth status [--all]|logout",
|
|
123
144
|
description: "Show or clear saved provider credentials without printing secrets.",
|
|
124
|
-
examples: ["kward auth status", "kward auth logout"]
|
|
145
|
+
examples: ["kward auth status", "kward auth status --all", "kward auth logout"]
|
|
125
146
|
},
|
|
126
147
|
"init" => {
|
|
127
148
|
usage: "kward init",
|
|
@@ -165,7 +186,7 @@ module Kward
|
|
|
165
186
|
},
|
|
166
187
|
"pan" => {
|
|
167
188
|
usage: "kward pan",
|
|
168
|
-
description: "Start Pan mode, a mobile-friendly
|
|
189
|
+
description: "Start Pan mode, a mobile-friendly local web UI with persistent sessions.",
|
|
169
190
|
examples: ["kward pan", "kward --working-directory ~/code/project pan"]
|
|
170
191
|
},
|
|
171
192
|
"rpc" => {
|
data/lib/kward/cli/compaction.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Kward
|
|
|
6
6
|
module CompactionCommands
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
-
def compact_context(agent, argument)
|
|
9
|
+
def compact_context(agent, argument, cancellation: nil)
|
|
10
10
|
before = run_lifecycle_hook("session_compact_before", conversation: agent.conversation, payload: { instructions: argument.to_s })
|
|
11
11
|
if before.denied? || before.approval_required?
|
|
12
12
|
runtime_output("Declined: #{before.decision.message || "session compaction denied"}")
|
|
@@ -17,16 +17,22 @@ module Kward
|
|
|
17
17
|
conversation: agent.conversation,
|
|
18
18
|
client: @client,
|
|
19
19
|
tool_result_summarizer: lambda { |tool_call, content| tool_result_summary(tool_call, content) }
|
|
20
|
-
).compact(custom_instructions: argument)
|
|
20
|
+
).compact(custom_instructions: argument, cancellation: cancellation)
|
|
21
21
|
run_lifecycle_hook("session_compact_after", conversation: agent.conversation, payload: { old_message_count: result.old_message_count, new_message_count: result.new_message_count })
|
|
22
22
|
runtime_output("Compacted context: #{result.old_message_count} messages -> #{result.new_message_count} messages.")
|
|
23
|
-
|
|
23
|
+
result.summary
|
|
24
24
|
rescue Compactor::NothingToCompact, Compactor::AlreadyCompacted, Compactor::EmptySummary => e
|
|
25
25
|
runtime_output(e.message)
|
|
26
|
+
rescue Cancellation::CancelledError
|
|
27
|
+
raise
|
|
26
28
|
rescue StandardError => e
|
|
27
29
|
runtime_output("Compaction error: #{e.message}")
|
|
28
30
|
end
|
|
29
31
|
|
|
32
|
+
def render_compaction_summary(summary)
|
|
33
|
+
render_transcript_block("Assistant", summary)
|
|
34
|
+
end
|
|
35
|
+
|
|
30
36
|
end
|
|
31
37
|
end
|
|
32
38
|
end
|